diff --git a/apple/Omnivore.xcodeproj/project.pbxproj b/apple/Omnivore.xcodeproj/project.pbxproj
index 3df71e308..0722253ee 100644
--- a/apple/Omnivore.xcodeproj/project.pbxproj
+++ b/apple/Omnivore.xcodeproj/project.pbxproj
@@ -1307,7 +1307,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
- CURRENT_PROJECT_VERSION = 77;
+ CURRENT_PROJECT_VERSION = 78;
DEVELOPMENT_TEAM = QJF2XZ86HB;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = InfoPlists/ShareExtensionMac.plist;
@@ -1317,7 +1317,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 12.0;
- MARKETING_VERSION = 1.18.0;
+ MARKETING_VERSION = 1.22.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "app.omnivore.app.ShareExtension-Mac";
@@ -1339,7 +1339,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
- CURRENT_PROJECT_VERSION = 77;
+ CURRENT_PROJECT_VERSION = 78;
DEVELOPMENT_TEAM = QJF2XZ86HB;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = InfoPlists/ShareExtensionMac.plist;
@@ -1349,7 +1349,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 12.0;
- MARKETING_VERSION = 1.18.0;
+ MARKETING_VERSION = 1.22.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "app.omnivore.app.ShareExtension-Mac";
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1421,7 +1421,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
- CURRENT_PROJECT_VERSION = 77;
+ CURRENT_PROJECT_VERSION = 78;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = QJF2XZ86HB;
ENABLE_HARDENED_RUNTIME = YES;
@@ -1432,7 +1432,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 12.0;
- MARKETING_VERSION = 1.18.0;
+ MARKETING_VERSION = 1.22.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
@@ -1455,7 +1455,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
- CURRENT_PROJECT_VERSION = 77;
+ CURRENT_PROJECT_VERSION = 78;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = QJF2XZ86HB;
ENABLE_HARDENED_RUNTIME = YES;
@@ -1466,7 +1466,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 12.0;
- MARKETING_VERSION = 1.18.0;
+ MARKETING_VERSION = 1.22.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1521,7 +1521,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 1.21.0;
+ MARKETING_VERSION = 1.22.0;
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
PRODUCT_NAME = Omnivore;
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1600,7 +1600,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 1.21.0;
+ MARKETING_VERSION = 1.22.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_LDFLAGS = (
@@ -1639,7 +1639,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 1.21.0;
+ MARKETING_VERSION = 1.22.0;
MTL_FAST_MATH = YES;
OTHER_LDFLAGS = (
"-framework",
@@ -1665,7 +1665,7 @@
CODE_SIGN_ENTITLEMENTS = "Entitlements/SafariExtension-Mac.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 77;
+ CURRENT_PROJECT_VERSION = 78;
DEVELOPMENT_TEAM = QJF2XZ86HB;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -1678,7 +1678,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 12.0;
- MARKETING_VERSION = 1.18.0;
+ MARKETING_VERSION = 1.22.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_LDFLAGS = (
@@ -1704,7 +1704,7 @@
CODE_SIGN_ENTITLEMENTS = "Entitlements/SafariExtension-Mac.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 77;
+ CURRENT_PROJECT_VERSION = 78;
DEVELOPMENT_TEAM = QJF2XZ86HB;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -1717,7 +1717,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 12.0;
- MARKETING_VERSION = 1.18.0;
+ MARKETING_VERSION = 1.22.0;
MTL_FAST_MATH = YES;
OTHER_LDFLAGS = (
"-framework",
@@ -1804,7 +1804,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 1.21.0;
+ MARKETING_VERSION = 1.22.0;
PRODUCT_BUNDLE_IDENTIFIER = "app.omnivore.app.share-extension";
PRODUCT_NAME = ShareExtension;
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1859,7 +1859,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 1.21.0;
+ MARKETING_VERSION = 1.22.0;
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
PRODUCT_NAME = Omnivore;
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1888,7 +1888,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 1.21.0;
+ MARKETING_VERSION = 1.22.0;
PRODUCT_BUNDLE_IDENTIFIER = "app.omnivore.app.share-extension";
PRODUCT_NAME = ShareExtension;
PROVISIONING_PROFILE_SPECIFIER = "";
diff --git a/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/ExpandedPlayer.swift b/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/ExpandedPlayer.swift
index cd6271ef5..1a8e7fa09 100644
--- a/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/ExpandedPlayer.swift
+++ b/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/ExpandedPlayer.swift
@@ -355,42 +355,6 @@
}
}
- var changeVoiceView: some View {
- NavigationView {
- VStack {
- List {
- ForEach(audioController.voiceList ?? [], id: \.key.self) { voice in
- Button(action: {
- audioController.currentVoice = voice.key
- self.showVoiceSheet = false
- }, label: {
- HStack {
- Text(voice.name)
-
- Spacer()
-
- if voice.selected {
- Image(systemName: "checkmark")
- }
- }
- .contentShape(Rectangle())
- }).buttonStyle(PlainButtonStyle())
- }
- }
- .padding(.top, 32)
- .listStyle(.plain)
- Spacer()
- }
- .navigationBarTitle("Voice")
- .navigationBarTitleDisplayMode(.inline)
- .navigationBarItems(leading: Button(action: { self.showVoiceSheet = false }, label: {
- Image(systemName: "chevron.backward")
- .font(.appNavbarIcon)
- .tint(.appGrayTextContrast)
- }))
- }
- }
-
var scrubbing: Bool {
switch audioController.scrubState {
case .scrubStarted:
diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift
index e73aa03f8..ffa30831d 100644
--- a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift
+++ b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift
@@ -97,6 +97,10 @@ struct ProfileView: View {
NavigationLink(destination: SubscriptionsView()) {
Text("Subscriptions")
}
+
+ NavigationLink(destination: GroupsView()) {
+ Text("Recommendation Groups")
+ }
}
#if os(iOS)
diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/RecommendationGroupView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/RecommendationGroupView.swift
new file mode 100644
index 000000000..30356f083
--- /dev/null
+++ b/apple/OmnivoreKit/Sources/App/Views/Profile/RecommendationGroupView.swift
@@ -0,0 +1,146 @@
+import Models
+import Services
+import SwiftUI
+import Views
+
+@MainActor final class RecommendationsGroupViewModel: ObservableObject {
+ @Published var isLoading = false
+ @Published var networkError = true
+ @Published var recommendationGroup: InternalRecommendationGroup
+
+ init(recommendationGroup: InternalRecommendationGroup) {
+ self.recommendationGroup = recommendationGroup
+ }
+
+ var nonAdmins: [InternalUserProfile] {
+ recommendationGroup.members.filter { member in
+ !recommendationGroup.admins.contains(where: { member.id == $0.id })
+ }
+ }
+}
+
+private struct SmallUserCard: View {
+ let data: ProfileCardData
+ var size: CGFloat
+
+ public init(data: ProfileCardData, size: CGFloat = 36) {
+ self.data = data
+ self.size = size
+ }
+
+ public var body: some View {
+ HStack(alignment: .center, spacing: 16) {
+ Group {
+ AsyncImage(
+ url: data.imageURL,
+ content: { $0.resizable() },
+ placeholder: {
+ Image(systemName: "person.crop.circle")
+ .resizable()
+ .foregroundColor(.appGrayTextContrast)
+ }
+ )
+ }
+ .padding(.leading, 0)
+ .aspectRatio(contentMode: .fill)
+ .frame(width: size, height: size, alignment: .center)
+ .clipShape(Circle())
+
+ VStack(alignment: .leading, spacing: 4) {
+ Text(data.name)
+ .font(.appBody)
+ .foregroundColor(.appGrayTextContrast)
+ .lineLimit(1)
+ .frame(maxWidth: .infinity, alignment: .leading)
+
+ Text("@\(data.username)")
+ .font(.appCaption)
+ .foregroundColor(.appGrayText)
+ .lineLimit(1)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+ .frame(maxWidth: .infinity)
+ .multilineTextAlignment(.leading)
+ .padding(0)
+ }
+ .frame(maxWidth: .infinity)
+ }
+}
+
+struct RecommendationGroupView: View {
+ @EnvironmentObject var dataService: DataService
+ @StateObject var viewModel: RecommendationsGroupViewModel
+
+ @State var presentShareSheet = false
+
+ var body: some View {
+ Group {
+ #if os(iOS)
+ Form {
+ innerBody
+ }
+ #elseif os(macOS)
+ List {
+ innerBody
+ }
+ .listStyle(InsetListStyle())
+ #endif
+ }
+ }
+
+ private var shareView: some View {
+ if let shareLink = URL(string: viewModel.recommendationGroup.inviteUrl) {
+ return AnyView(ShareSheet(activityItems: [shareLink]))
+ } else {
+ return AnyView(Text("Error copying invite URL"))
+ }
+ }
+
+ private var adminsSection: some View {
+ Section("Admins") {
+ ForEach(viewModel.recommendationGroup.admins) { admin in
+ SmallUserCard(data: ProfileCardData(
+ name: admin.name,
+ username: admin.username,
+ imageURL: admin.profileImageURL != nil ? URL(string: admin.profileImageURL!) : nil
+ ))
+ }
+ }
+ }
+
+ private var membersSection: some View {
+ Section("Members") {
+ ForEach(viewModel.nonAdmins) { member in
+ SmallUserCard(data: ProfileCardData(
+ name: member.name,
+ username: member.username,
+ imageURL: member.profileImageURL != nil ? URL(string: member.profileImageURL!) : nil
+ ))
+ }
+ }
+ }
+
+ private var innerBody: some View {
+ Group {
+ Section("Name") {
+ Text(viewModel.recommendationGroup.name)
+ }
+
+ Section("Invite Link") {
+ Button(action: {
+ presentShareSheet = true
+ }, label: {
+ Text("[\(viewModel.recommendationGroup.inviteUrl)](\(viewModel.recommendationGroup.inviteUrl))")
+ .font(.appCaption)
+ })
+ }
+
+ adminsSection
+ membersSection
+ }
+ .formSheet(isPresented: $presentShareSheet) {
+ shareView
+ }
+ .navigationTitle(viewModel.recommendationGroup.name)
+ }
+}
diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/RecommendationGroupsView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/RecommendationGroupsView.swift
new file mode 100644
index 000000000..570072b41
--- /dev/null
+++ b/apple/OmnivoreKit/Sources/App/Views/Profile/RecommendationGroupsView.swift
@@ -0,0 +1,145 @@
+import Models
+import Services
+import SwiftUI
+import Views
+
+@MainActor final class RecommendationsGroupsViewModel: ObservableObject {
+ @Published var isLoading = false
+ @Published var isCreating = false
+ @Published var networkError = true
+ @Published var recommendationGroups = [InternalRecommendationGroup]()
+
+ @Published var showCreateSheet = false
+
+ @Published var showCreateError = false
+ @Published var createGroupError: String?
+
+ func loadGroups(dataService: DataService) async {
+ isLoading = true
+
+ do {
+ recommendationGroups = try await dataService.recommendationGroups()
+ } catch {
+ networkError = true
+ }
+
+ isLoading = false
+ }
+
+ func createGroup(dataService: DataService, name: String) async {
+ isCreating = true
+
+ if let group = try? await dataService.createRecommendationGroup(name: name) {
+ print("CREATED GROUP: ", group)
+ await loadGroups(dataService: dataService)
+ showCreateSheet = false
+ } else {
+ createGroupError = "Error creating group"
+ showCreateError = true
+ }
+
+ isCreating = false
+ }
+}
+
+struct CreateRecommendationGroupView: View {
+ @State var name = ""
+ @EnvironmentObject var dataService: DataService
+ @StateObject var viewModel = RecommendationsGroupsViewModel()
+
+ var nextButton: some View {
+ if viewModel.isCreating {
+ return AnyView(ProgressView())
+ } else {
+ return AnyView(Button(action: {
+ Task {
+ await viewModel.createGroup(dataService: dataService, name: self.name)
+ }
+ }, label: {
+ Text("Next")
+ })
+ .disabled(name.isEmpty)
+ )
+ }
+ }
+
+ var body: some View {
+ NavigationView {
+ Form {
+ TextField("Name", text: $name, prompt: Text("Group Name"))
+ }
+ .alert(isPresented: $viewModel.showCreateError) {
+ Alert(
+ title: Text(viewModel.createGroupError ?? "Error creating group"),
+ dismissButton: .cancel(Text("Ok")) {
+ viewModel.createGroupError = nil
+ viewModel.showCreateError = false
+ }
+ )
+ }
+ }
+ .navigationViewStyle(.stack)
+ .navigationTitle("Create Group")
+ .navigationBarTitleDisplayMode(.inline)
+ .navigationBarItems(leading:
+ Button(action: {
+ viewModel.showCreateSheet = false
+ }, label: { Text("Cancel") }),
+ trailing: nextButton)
+ }
+}
+
+struct GroupsView: View {
+ @EnvironmentObject var dataService: DataService
+ @StateObject var viewModel = RecommendationsGroupsViewModel()
+
+ var body: some View {
+ Group {
+ #if os(iOS)
+ Form {
+ innerBody
+ }
+ #elseif os(macOS)
+ List {
+ innerBody
+ }
+ .listStyle(InsetListStyle())
+ #endif
+ }
+ .sheet(isPresented: $viewModel.showCreateSheet) {
+ NavigationView {
+ CreateRecommendationGroupView(viewModel: self.viewModel)
+ }
+ }
+ .task { await viewModel.loadGroups(dataService: dataService) }
+ }
+
+ private var innerBody: some View {
+ Group {
+ Section {
+ Button(
+ action: { viewModel.showCreateSheet = true },
+ label: {
+ HStack {
+ Image(systemName: "plus.circle.fill").foregroundColor(.green)
+ Text("Create a new group")
+ Spacer()
+ }
+ }
+ )
+ .disabled(viewModel.isLoading)
+ }
+
+ Section(header: Text("Your recommendation groups")) {
+ ForEach(viewModel.recommendationGroups) { recommendationGroup in
+ NavigationLink(
+ destination: RecommendationGroupView(viewModel: RecommendationsGroupViewModel(recommendationGroup: recommendationGroup))
+ ) {
+ Text(recommendationGroup.name)
+ }
+ }
+ }
+ }
+ .navigationTitle("Recommendation Groups")
+ }
+}
diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/CreateProfileView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/CreateProfileView.swift
index 4164550fb..bc60ce80e 100644
--- a/apple/OmnivoreKit/Sources/App/Views/Registration/CreateProfileView.swift
+++ b/apple/OmnivoreKit/Sources/App/Views/Registration/CreateProfileView.swift
@@ -6,7 +6,7 @@ import Utils
import Views
@MainActor final class CreateProfileViewModel: ObservableObject {
- private(set) var initialUserProfile = UserProfile(username: "", name: "", bio: nil)
+ private(set) var initialUserProfile = NewUserProfile(username: "", name: "", bio: nil)
var isConfigured = false
var hasSuggestedProfile: Bool {
@@ -31,7 +31,7 @@ import Views
init() {}
func submitProfile(name: String, bio: String, authenticator: Authenticator) {
- let profileOrError = UserProfile.make(
+ let profileOrError = NewUserProfile.make(
username: potentialUsername,
name: name,
bio: bio.isEmpty ? nil : bio
@@ -77,7 +77,7 @@ import Views
}
}
- func submitProfile(userProfile: UserProfile, authenticator: Authenticator) async {
+ func submitProfile(userProfile: NewUserProfile, authenticator: Authenticator) async {
do {
try await authenticator.createAccount(userProfile: userProfile)
} catch {
@@ -87,7 +87,7 @@ import Views
}
}
- func configure(profile: UserProfile, dataService: DataService) {
+ func configure(profile: NewUserProfile, dataService: DataService) {
guard !isConfigured else { return }
isConfigured = true
@@ -104,7 +104,7 @@ import Views
}
struct CreateProfileView: View {
- private let initialUserProfile: UserProfile
+ private let initialUserProfile: NewUserProfile
@State private var isConfigured = false
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@EnvironmentObject var authenticator: Authenticator
@@ -114,7 +114,7 @@ struct CreateProfileView: View {
@State private var name = ""
@State private var bio = ""
- init(userProfile: UserProfile) {
+ init(userProfile: NewUserProfile) {
self.initialUserProfile = userProfile
}
diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/NewAppleSignupView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/NewAppleSignupView.swift
index 3bb561ac5..3eda742f1 100644
--- a/apple/OmnivoreKit/Sources/App/Views/Registration/NewAppleSignupView.swift
+++ b/apple/OmnivoreKit/Sources/App/Views/Registration/NewAppleSignupView.swift
@@ -9,7 +9,7 @@ import Views
init() {}
- func submitProfile(userProfile: UserProfile, authenticator: Authenticator) async {
+ func submitProfile(userProfile: NewUserProfile, authenticator: Authenticator) async {
do {
try await authenticator.createAccount(userProfile: userProfile)
} catch {
@@ -23,7 +23,7 @@ import Views
struct NewAppleSignupView: View {
@EnvironmentObject var authenticator: Authenticator
@StateObject private var viewModel = NewAppleSignupViewModel()
- let userProfile: UserProfile
+ let userProfile: NewUserProfile
let showProfileEditView: () -> Void
var body: some View {
diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/RegistrationView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/RegistrationView.swift
index 8bd5754ab..db6250caf 100644
--- a/apple/OmnivoreKit/Sources/App/Views/Registration/RegistrationView.swift
+++ b/apple/OmnivoreKit/Sources/App/Views/Registration/RegistrationView.swift
@@ -7,8 +7,8 @@ import Views
@MainActor final class RegistrationViewModel: ObservableObject {
enum RegistrationState {
- case createProfile(userProfile: UserProfile)
- case newAppleSignUp(userProfile: UserProfile)
+ case createProfile(userProfile: NewUserProfile)
+ case newAppleSignUp(userProfile: NewUserProfile)
}
@Published var loginError: LoginError?
@@ -65,7 +65,7 @@ import Views
case let .loginError(error):
loginError = error
case .newOmnivoreUser:
- registrationState = .createProfile(userProfile: UserProfile(username: "", name: ""))
+ registrationState = .createProfile(userProfile: NewUserProfile(username: "", name: ""))
case .existingOmnivoreUser:
break
}
diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/RecommendToView.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/RecommendToView.swift
new file mode 100644
index 000000000..20d737d03
--- /dev/null
+++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/RecommendToView.swift
@@ -0,0 +1,119 @@
+import Models
+import Services
+import SwiftUI
+import Views
+
+@MainActor final class RecommendToViewModel: ObservableObject {
+ @Published var isLoading = false
+ @Published var networkError = true
+ @Published var recommendationGroups = [InternalRecommendationGroup]()
+ @Published var selectedGroups = [String]()
+ @Published var isRunning = false
+ @Published var showError = false
+ @Published var note: String?
+
+ let pageID: String
+
+ init(pageID: String) {
+ self.pageID = pageID
+ }
+
+ func loadGroups(dataService: DataService) async {
+ isLoading = true
+
+ do {
+ recommendationGroups = try await dataService.recommendationGroups()
+ } catch {
+ print("ERROR fetching recommendationGroups: ", error)
+ networkError = true
+ }
+
+ isLoading = false
+ }
+
+ func recommend(dataService: DataService) async {
+ isRunning = true
+
+ do {
+ try await dataService.recommendPage(pageID: pageID, groupIDs: selectedGroups, note: note)
+ } catch {
+ showError = true
+ }
+
+ isRunning = false
+ }
+}
+
+struct RecommendToView: View {
+ var dataService: DataService
+ @StateObject var viewModel: RecommendToViewModel
+ @Environment(\.dismiss) private var dismiss
+
+ var nextButton: some View {
+ if viewModel.isRunning {
+ return AnyView(ProgressView())
+ } else {
+ return AnyView(Button(action: {
+ Task {
+ await viewModel.recommend(dataService: dataService)
+ Snackbar.show(message: "Recommendation sent")
+ dismiss()
+ }
+ }, label: {
+ Text("Next")
+ })
+ .disabled(viewModel.selectedGroups.isEmpty)
+ )
+ }
+ }
+
+ var body: some View {
+ VStack {
+ List {
+ Section("Select groups to recommend to") {
+ ForEach(viewModel.recommendationGroups) { group in
+ HStack {
+ Text(group.name)
+
+ Spacer()
+
+ if viewModel.selectedGroups.contains(group.id) {
+ Image(systemName: "checkmark")
+ }
+ }
+ .contentShape(Rectangle())
+ .onTapGesture {
+ let idx = viewModel.selectedGroups.firstIndex(of: group.id)
+ if let idx = idx {
+ viewModel.selectedGroups.remove(at: idx)
+ } else {
+ viewModel.selectedGroups.append(group.id)
+ }
+ }
+ }
+ }
+ }
+ .listStyle(.grouped)
+
+ Spacer()
+ }
+ .alert(isPresented: $viewModel.showError) {
+ Alert(
+ title: Text("Error recommending this page"),
+ dismissButton: .cancel(Text("Ok")) {
+ viewModel.showError = false
+ }
+ )
+ }
+ .navigationBarTitle("Recommend")
+ .navigationBarTitleDisplayMode(.inline)
+ .navigationViewStyle(.stack)
+ .navigationBarItems(leading: Button(action: {
+ dismiss()
+ }, label: { Text("Cancel") }),
+ trailing: nextButton)
+ .task {
+ await viewModel.loadGroups(dataService: dataService)
+ }
+ }
+}
diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift
index 748246a6a..408da9ef7 100644
--- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift
+++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift
@@ -29,6 +29,7 @@ struct WebReaderContainerView: View {
@State private var bottomBarOpacity = 0.0
@State private var errorAlertMessage: String?
@State private var showErrorAlertMessage = false
+ @State private var showRecommendSheet = false
@EnvironmentObject var dataService: DataService
@EnvironmentObject var audioController: AudioController
@@ -150,8 +151,8 @@ struct WebReaderContainerView: View {
}).frame(width: 48, height: 48)
Divider().opacity(0.8)
- Button(action: share, label: {
- Image(systemName: "square.and.arrow.up")
+ Button(action: recommend, label: {
+ Image(systemName: "sparkles")
}).frame(width: 48, height: 48)
// TODO: We don't have a single note function yet
@@ -219,6 +220,12 @@ struct WebReaderContainerView: View {
action: delete,
label: { Label("Delete", systemImage: "trash") }
)
+ Button(
+ action: {
+ showRecommendSheet = true
+ },
+ label: { Label("Recommend", systemImage: "sparkles") }
+ )
}
}
@@ -351,6 +358,16 @@ struct WebReaderContainerView: View {
showErrorAlertMessage = false
})
}
+ .formSheet(isPresented: $showRecommendSheet) {
+ NavigationView {
+ RecommendToView(
+ dataService: dataService,
+ viewModel: RecommendToViewModel(pageID: item.unwrappedID)
+ )
+ }.onDisappear {
+ showRecommendSheet = false
+ }
+ }
.sheet(isPresented: $showHighlightAnnotationModal) {
HighlightAnnotationSheet(
annotation: $annotation,
@@ -440,6 +457,10 @@ struct WebReaderContainerView: View {
Snackbar.show(message: !item.isArchived ? "Link archived" : "Link moved to Inbox")
}
+ func recommend() {
+ showRecommendSheet = true
+ }
+
func share() {
shareActionID = UUID()
}
diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderCoordinator.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderCoordinator.swift
index 99e9952d3..cffc3a7df 100644
--- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderCoordinator.swift
+++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderCoordinator.swift
@@ -62,7 +62,7 @@ extension WebReaderCoordinator: WKNavigationDelegate {
// swiftlint:disable:next line_length
func webView(_: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
if navigationAction.navigationType == .linkActivated {
- if let linkURL = navigationAction.request.url {
+ if let linkURL = navigationAction.request.url, !linkURL.isFileURL {
linkHandler(linkURL)
}
decisionHandler(.cancel)
diff --git a/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents b/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents
index ace07414f..deb3afd4f 100644
--- a/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents
+++ b/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents
@@ -55,6 +55,7 @@
+
@@ -91,6 +92,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apple/OmnivoreKit/Sources/Models/CoreData/StorageProvider.swift b/apple/OmnivoreKit/Sources/Models/CoreData/StorageProvider.swift
index 6198ec852..12a31526a 100644
--- a/apple/OmnivoreKit/Sources/Models/CoreData/StorageProvider.swift
+++ b/apple/OmnivoreKit/Sources/Models/CoreData/StorageProvider.swift
@@ -21,6 +21,7 @@ public class PersistentContainer: NSPersistentContainer {
#endif
let appGroupContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupID)
let appGroupContainerURL = appGroupContainer?.appendingPathComponent("store.sqlite")
+ print("starting with sqlite container", appGroupContainerURL)
container.persistentStoreDescriptions.first!.url = appGroupContainerURL
diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/NewUserProfile.swift b/apple/OmnivoreKit/Sources/Models/DataModels/NewUserProfile.swift
new file mode 100644
index 000000000..a13c52bcb
--- /dev/null
+++ b/apple/OmnivoreKit/Sources/Models/DataModels/NewUserProfile.swift
@@ -0,0 +1,52 @@
+import Foundation
+import Utils
+
+public struct NewUserProfile: Codable {
+ public let username: String
+ public let name: String
+ public let bio: String?
+
+ public init(
+ username: String,
+ name: String,
+ bio: String? = nil
+ ) {
+ self.username = username
+ self.name = name
+ self.bio = bio
+ }
+}
+
+public extension NewUserProfile {
+ static func make(
+ username: String,
+ name: String,
+ bio: String?
+ ) -> Either {
+ let userProfile = NewUserProfile(
+ username: username,
+ name: name,
+ bio: bio
+ )
+
+ if let errorMessage = userProfile.validationErrorMessage {
+ return .right(errorMessage)
+ } else {
+ return .left(userProfile)
+ }
+ }
+
+ private var validationErrorMessage: String? {
+ if name.isEmpty {
+ return "The name field is missing."
+ }
+
+ return nil
+ }
+}
+
+public extension Viewer {
+ var unwrappedUsername: String { username ?? "" }
+ var unwrappedName: String { name ?? "" }
+ var unwrappedUserID: String { userID ?? "" }
+}
diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/Recommendation.swift b/apple/OmnivoreKit/Sources/Models/DataModels/Recommendation.swift
new file mode 100644
index 000000000..279a74268
--- /dev/null
+++ b/apple/OmnivoreKit/Sources/Models/DataModels/Recommendation.swift
@@ -0,0 +1,21 @@
+import CoreData
+import Foundation
+
+public extension Recommendation {
+ var unwrappedID: String { id ?? "" }
+
+ static func lookup(byID recommendationID: String, inContext context: NSManagedObjectContext) -> Recommendation? {
+ let fetchRequest: NSFetchRequest = Recommendation.fetchRequest()
+ fetchRequest.predicate = NSPredicate(
+ format: "id == %@", recommendationID
+ )
+
+ var recommendation: Recommendation?
+
+ context.performAndWait {
+ recommendation = (try? context.fetch(fetchRequest))?.first
+ }
+
+ return recommendation
+ }
+}
diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/UserProfile.swift b/apple/OmnivoreKit/Sources/Models/DataModels/UserProfile.swift
index 18e0a3da1..7c655577f 100644
--- a/apple/OmnivoreKit/Sources/Models/DataModels/UserProfile.swift
+++ b/apple/OmnivoreKit/Sources/Models/DataModels/UserProfile.swift
@@ -1,52 +1,19 @@
+import CoreData
import Foundation
-import Utils
-
-public struct UserProfile: Codable {
- public let username: String
- public let name: String
- public let bio: String?
-
- public init(
- username: String,
- name: String,
- bio: String? = nil
- ) {
- self.username = username
- self.name = name
- self.bio = bio
- }
-}
public extension UserProfile {
- static func make(
- username: String,
- name: String,
- bio: String?
- ) -> Either {
- let userProfile = UserProfile(
- username: username,
- name: name,
- bio: bio
+ static func lookup(byID userID: String, inContext context: NSManagedObjectContext) -> UserProfile? {
+ let fetchRequest: NSFetchRequest = UserProfile.fetchRequest()
+ fetchRequest.predicate = NSPredicate(
+ format: "%K == %@", #keyPath(UserProfile.userID), userID
)
- if let errorMessage = userProfile.validationErrorMessage {
- return .right(errorMessage)
- } else {
- return .left(userProfile)
- }
- }
+ var result: UserProfile?
- private var validationErrorMessage: String? {
- if name.isEmpty {
- return "The name field is missing."
+ context.performAndWait {
+ result = (try? context.fetch(fetchRequest))?.first
}
- return nil
+ return result
}
}
-
-public extension Viewer {
- var unwrappedUsername: String { username ?? "" }
- var unwrappedName: String { name ?? "" }
- var unwrappedUserID: String { userID ?? "" }
-}
diff --git a/apple/OmnivoreKit/Sources/Services/Authentication/AccountCreator.swift b/apple/OmnivoreKit/Sources/Services/Authentication/AccountCreator.swift
index 73e31dbb7..1db37f136 100644
--- a/apple/OmnivoreKit/Sources/Services/Authentication/AccountCreator.swift
+++ b/apple/OmnivoreKit/Sources/Services/Authentication/AccountCreator.swift
@@ -5,12 +5,12 @@ public extension Authenticator {
func createPendingAccountUsingApple(
token: String,
name: PersonNameComponents?
- ) async throws -> UserProfile {
+ ) async throws -> NewUserProfile {
let params = CreatePendingAccountParams(token: token, provider: .apple, fullName: name)
return try await createPendingAccount(params: params)
}
- func createAccount(userProfile: UserProfile) async throws {
+ func createAccount(userProfile: NewUserProfile) async throws {
let params = CreateAccountParams(
pendingUserToken: pendingUserToken ?? "",
userProfile: userProfile
@@ -34,7 +34,7 @@ public extension Authenticator {
}
extension Authenticator {
- func createPendingAccount(params: CreatePendingAccountParams) async throws -> UserProfile {
+ func createPendingAccount(params: CreatePendingAccountParams) async throws -> NewUserProfile {
do {
let encodedParams = (try? JSONEncoder().encode(params)) ?? Data()
let pendingUserAuthPayload = try await networker.createPendingUser(params: encodedParams)
diff --git a/apple/OmnivoreKit/Sources/Services/Authentication/AuthModels.swift b/apple/OmnivoreKit/Sources/Services/Authentication/AuthModels.swift
index 454e06b1e..550389c56 100644
--- a/apple/OmnivoreKit/Sources/Services/Authentication/AuthModels.swift
+++ b/apple/OmnivoreKit/Sources/Services/Authentication/AuthModels.swift
@@ -48,12 +48,12 @@ struct EmailAuthPayload: Decodable {
struct CreateAccountParams: Encodable {
let pendingUserToken: String
- let userProfile: UserProfile
+ let userProfile: NewUserProfile
}
struct PendingUserAuthPayload: Decodable {
let pendingUserToken: String
- let pendingUserProfile: UserProfile
+ let pendingUserProfile: NewUserProfile
}
struct PendingEmailVerificationAuthPayload: Decodable {
diff --git a/apple/OmnivoreKit/Sources/Services/DataService/ContentLoading.swift b/apple/OmnivoreKit/Sources/Services/DataService/ContentLoading.swift
index 4de282df4..b1b620be8 100644
--- a/apple/OmnivoreKit/Sources/Services/DataService/ContentLoading.swift
+++ b/apple/OmnivoreKit/Sources/Services/DataService/ContentLoading.swift
@@ -48,6 +48,7 @@ extension DataService {
do {
objectID = try await persistArticleContent(articleProps: fetchResult)
} catch {
+ print("caught article content error: ", error)
var message = "unknown error"
let basicError = (error as? BasicError) ?? BasicError.message(messageText: "unknown error")
if case let BasicError.message(messageText) = basicError {
@@ -230,7 +231,7 @@ extension DataService {
} catch {
// We don't propogate these errors, we just let it pass through so
// the user can attempt to fetch content again.
- print("Error syncUnsyncedArticleContent")
+ print("Error syncUnsyncedArticleContent", error)
}
}
}
diff --git a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift
index 2b4d961e2..22963144d 100644
--- a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift
+++ b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift
@@ -684,6 +684,7 @@ extension Objects {
let readAt: [String: DateTime]
let readingProgressAnchorIndex: [String: Int]
let readingProgressPercent: [String: Double]
+ let recommendations: [String: [Objects.Recommendation]]
let savedAt: [String: DateTime]
let savedByViewer: [String: Bool]
let shareInfo: [String: Objects.LinkShareInfo]
@@ -806,6 +807,10 @@ extension Objects.Article: Decodable {
if let value = try container.decode(Double?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
}
+ case "recommendations":
+ if let value = try container.decode([Objects.Recommendation]?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
case "savedAt":
if let value = try container.decode(DateTime?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
@@ -898,6 +903,7 @@ extension Objects.Article: Decodable {
readAt = map["readAt"]
readingProgressAnchorIndex = map["readingProgressAnchorIndex"]
readingProgressPercent = map["readingProgressPercent"]
+ recommendations = map["recommendations"]
savedAt = map["savedAt"]
savedByViewer = map["savedByViewer"]
shareInfo = map["shareInfo"]
@@ -1276,6 +1282,22 @@ extension Fields where TypeLock == Objects.Article {
}
}
+ func recommendations(selection: Selection) throws -> Type {
+ let field = GraphQLField.composite(
+ name: "recommendations",
+ arguments: [],
+ selection: selection.selection
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ return try selection.decode(data: data.recommendations[field.alias!])
+ case .mocking:
+ return selection.mock()
+ }
+ }
+
func savedAt() throws -> DateTime {
let field = GraphQLField.leaf(
name: "savedAt",
@@ -2596,6 +2618,137 @@ extension Selection where TypeLock == Never, Type == Never {
typealias CreateArticleSuccess = Selection
}
+extension Objects {
+ struct CreateGroupError {
+ let __typename: TypeName = .createGroupError
+ let errorCodes: [String: [Enums.CreateGroupErrorCode]]
+
+ enum TypeName: String, Codable {
+ case createGroupError = "CreateGroupError"
+ }
+ }
+}
+
+extension Objects.CreateGroupError: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "errorCodes":
+ if let value = try container.decode([Enums.CreateGroupErrorCode]?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ errorCodes = map["errorCodes"]
+ }
+}
+
+extension Fields where TypeLock == Objects.CreateGroupError {
+ func errorCodes() throws -> [Enums.CreateGroupErrorCode] {
+ let field = GraphQLField.leaf(
+ name: "errorCodes",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.errorCodes[field.alias!] {
+ return data
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return []
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias CreateGroupError = Selection
+}
+
+extension Objects {
+ struct CreateGroupSuccess {
+ let __typename: TypeName = .createGroupSuccess
+ let group: [String: Objects.RecommendationGroup]
+
+ enum TypeName: String, Codable {
+ case createGroupSuccess = "CreateGroupSuccess"
+ }
+ }
+}
+
+extension Objects.CreateGroupSuccess: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "group":
+ if let value = try container.decode(Objects.RecommendationGroup?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ group = map["group"]
+ }
+}
+
+extension Fields where TypeLock == Objects.CreateGroupSuccess {
+ func group(selection: Selection) throws -> Type {
+ let field = GraphQLField.composite(
+ name: "group",
+ arguments: [],
+ selection: selection.selection
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.group[field.alias!] {
+ return try selection.decode(data: data)
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return selection.mock()
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias CreateGroupSuccess = Selection
+}
+
extension Objects {
struct CreateHighlightError {
let __typename: TypeName = .createHighlightError
@@ -3512,6 +3665,137 @@ extension Selection where TypeLock == Never, Type == Never {
typealias DeleteAccountSuccess = Selection
}
+extension Objects {
+ struct DeleteFilterError {
+ let __typename: TypeName = .deleteFilterError
+ let errorCodes: [String: [Enums.DeleteFilterErrorCode]]
+
+ enum TypeName: String, Codable {
+ case deleteFilterError = "DeleteFilterError"
+ }
+ }
+}
+
+extension Objects.DeleteFilterError: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "errorCodes":
+ if let value = try container.decode([Enums.DeleteFilterErrorCode]?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ errorCodes = map["errorCodes"]
+ }
+}
+
+extension Fields where TypeLock == Objects.DeleteFilterError {
+ func errorCodes() throws -> [Enums.DeleteFilterErrorCode] {
+ let field = GraphQLField.leaf(
+ name: "errorCodes",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.errorCodes[field.alias!] {
+ return data
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return []
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias DeleteFilterError = Selection
+}
+
+extension Objects {
+ struct DeleteFilterSuccess {
+ let __typename: TypeName = .deleteFilterSuccess
+ let filter: [String: Objects.Filter]
+
+ enum TypeName: String, Codable {
+ case deleteFilterSuccess = "DeleteFilterSuccess"
+ }
+ }
+}
+
+extension Objects.DeleteFilterSuccess: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "filter":
+ if let value = try container.decode(Objects.Filter?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ filter = map["filter"]
+ }
+}
+
+extension Fields where TypeLock == Objects.DeleteFilterSuccess {
+ func filter(selection: Selection) throws -> Type {
+ let field = GraphQLField.composite(
+ name: "filter",
+ arguments: [],
+ selection: selection.selection
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.filter[field.alias!] {
+ return try selection.decode(data: data)
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return selection.mock()
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias DeleteFilterSuccess = Selection
+}
+
extension Objects {
struct DeleteHighlightError {
let __typename: TypeName = .deleteHighlightError
@@ -5654,6 +5938,343 @@ extension Selection where TypeLock == Never, Type == Never {
typealias FeedArticlesSuccess = Selection
}
+extension Objects {
+ struct Filter {
+ let __typename: TypeName = .filter
+ let createdAt: [String: DateTime]
+ let description: [String: String]
+ let filter: [String: String]
+ let id: [String: String]
+ let name: [String: String]
+ let position: [String: Int]
+ let updatedAt: [String: DateTime]
+
+ enum TypeName: String, Codable {
+ case filter = "Filter"
+ }
+ }
+}
+
+extension Objects.Filter: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "createdAt":
+ if let value = try container.decode(DateTime?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ case "description":
+ if let value = try container.decode(String?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ case "filter":
+ if let value = try container.decode(String?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ case "id":
+ if let value = try container.decode(String?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ case "name":
+ if let value = try container.decode(String?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ case "position":
+ if let value = try container.decode(Int?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ case "updatedAt":
+ if let value = try container.decode(DateTime?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ createdAt = map["createdAt"]
+ description = map["description"]
+ filter = map["filter"]
+ id = map["id"]
+ name = map["name"]
+ position = map["position"]
+ updatedAt = map["updatedAt"]
+ }
+}
+
+extension Fields where TypeLock == Objects.Filter {
+ func createdAt() throws -> DateTime {
+ let field = GraphQLField.leaf(
+ name: "createdAt",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.createdAt[field.alias!] {
+ return data
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return DateTime.mockValue
+ }
+ }
+
+ func description() throws -> String? {
+ let field = GraphQLField.leaf(
+ name: "description",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ return data.description[field.alias!]
+ case .mocking:
+ return nil
+ }
+ }
+
+ func filter() throws -> String {
+ let field = GraphQLField.leaf(
+ name: "filter",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.filter[field.alias!] {
+ return data
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return String.mockValue
+ }
+ }
+
+ func id() throws -> String {
+ let field = GraphQLField.leaf(
+ name: "id",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.id[field.alias!] {
+ return data
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return String.mockValue
+ }
+ }
+
+ func name() throws -> String {
+ let field = GraphQLField.leaf(
+ name: "name",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.name[field.alias!] {
+ return data
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return String.mockValue
+ }
+ }
+
+ func position() throws -> Int {
+ let field = GraphQLField.leaf(
+ name: "position",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.position[field.alias!] {
+ return data
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return Int.mockValue
+ }
+ }
+
+ func updatedAt() throws -> DateTime {
+ let field = GraphQLField.leaf(
+ name: "updatedAt",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.updatedAt[field.alias!] {
+ return data
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return DateTime.mockValue
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias Filter = Selection
+}
+
+extension Objects {
+ struct FiltersError {
+ let __typename: TypeName = .filtersError
+ let errorCodes: [String: [Enums.FiltersErrorCode]]
+
+ enum TypeName: String, Codable {
+ case filtersError = "FiltersError"
+ }
+ }
+}
+
+extension Objects.FiltersError: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "errorCodes":
+ if let value = try container.decode([Enums.FiltersErrorCode]?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ errorCodes = map["errorCodes"]
+ }
+}
+
+extension Fields where TypeLock == Objects.FiltersError {
+ func errorCodes() throws -> [Enums.FiltersErrorCode] {
+ let field = GraphQLField.leaf(
+ name: "errorCodes",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.errorCodes[field.alias!] {
+ return data
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return []
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias FiltersError = Selection
+}
+
+extension Objects {
+ struct FiltersSuccess {
+ let __typename: TypeName = .filtersSuccess
+ let filters: [String: [Objects.Filter]]
+
+ enum TypeName: String, Codable {
+ case filtersSuccess = "FiltersSuccess"
+ }
+ }
+}
+
+extension Objects.FiltersSuccess: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "filters":
+ if let value = try container.decode([Objects.Filter]?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ filters = map["filters"]
+ }
+}
+
+extension Fields where TypeLock == Objects.FiltersSuccess {
+ func filters(selection: Selection) throws -> Type {
+ let field = GraphQLField.composite(
+ name: "filters",
+ arguments: [],
+ selection: selection.selection
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.filters[field.alias!] {
+ return try selection.decode(data: data)
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return selection.mock()
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias FiltersSuccess = Selection
+}
+
extension Objects {
struct GenerateApiKeyError {
let __typename: TypeName = .generateApiKeyError
@@ -6306,6 +6927,137 @@ extension Selection where TypeLock == Never, Type == Never {
typealias GoogleSignupSuccess = Selection
}
+extension Objects {
+ struct GroupsError {
+ let __typename: TypeName = .groupsError
+ let errorCodes: [String: [Enums.GroupsErrorCode]]
+
+ enum TypeName: String, Codable {
+ case groupsError = "GroupsError"
+ }
+ }
+}
+
+extension Objects.GroupsError: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "errorCodes":
+ if let value = try container.decode([Enums.GroupsErrorCode]?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ errorCodes = map["errorCodes"]
+ }
+}
+
+extension Fields where TypeLock == Objects.GroupsError {
+ func errorCodes() throws -> [Enums.GroupsErrorCode] {
+ let field = GraphQLField.leaf(
+ name: "errorCodes",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.errorCodes[field.alias!] {
+ return data
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return []
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias GroupsError = Selection
+}
+
+extension Objects {
+ struct GroupsSuccess {
+ let __typename: TypeName = .groupsSuccess
+ let groups: [String: [Objects.RecommendationGroup]]
+
+ enum TypeName: String, Codable {
+ case groupsSuccess = "GroupsSuccess"
+ }
+ }
+}
+
+extension Objects.GroupsSuccess: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "groups":
+ if let value = try container.decode([Objects.RecommendationGroup]?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ groups = map["groups"]
+ }
+}
+
+extension Fields where TypeLock == Objects.GroupsSuccess {
+ func groups(selection: Selection) throws -> Type {
+ let field = GraphQLField.composite(
+ name: "groups",
+ arguments: [],
+ selection: selection.selection
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.groups[field.alias!] {
+ return try selection.decode(data: data)
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return selection.mock()
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias GroupsSuccess = Selection
+}
+
extension Objects {
struct Highlight {
let __typename: TypeName = .highlight
@@ -7306,6 +8058,137 @@ extension Selection where TypeLock == Never, Type == Never {
typealias IntegrationsSuccess = Selection
}
+extension Objects {
+ struct JoinGroupError {
+ let __typename: TypeName = .joinGroupError
+ let errorCodes: [String: [Enums.JoinGroupErrorCode]]
+
+ enum TypeName: String, Codable {
+ case joinGroupError = "JoinGroupError"
+ }
+ }
+}
+
+extension Objects.JoinGroupError: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "errorCodes":
+ if let value = try container.decode([Enums.JoinGroupErrorCode]?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ errorCodes = map["errorCodes"]
+ }
+}
+
+extension Fields where TypeLock == Objects.JoinGroupError {
+ func errorCodes() throws -> [Enums.JoinGroupErrorCode] {
+ let field = GraphQLField.leaf(
+ name: "errorCodes",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.errorCodes[field.alias!] {
+ return data
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return []
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias JoinGroupError = Selection
+}
+
+extension Objects {
+ struct JoinGroupSuccess {
+ let __typename: TypeName = .joinGroupSuccess
+ let group: [String: Objects.RecommendationGroup]
+
+ enum TypeName: String, Codable {
+ case joinGroupSuccess = "JoinGroupSuccess"
+ }
+ }
+}
+
+extension Objects.JoinGroupSuccess: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "group":
+ if let value = try container.decode(Objects.RecommendationGroup?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ group = map["group"]
+ }
+}
+
+extension Fields where TypeLock == Objects.JoinGroupSuccess {
+ func group(selection: Selection) throws -> Type {
+ let field = GraphQLField.composite(
+ name: "group",
+ arguments: [],
+ selection: selection.selection
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.group[field.alias!] {
+ return try selection.decode(data: data)
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return selection.mock()
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias JoinGroupSuccess = Selection
+}
+
extension Objects {
struct Label {
let __typename: TypeName = .label
@@ -8498,6 +9381,137 @@ extension Selection where TypeLock == Never, Type == Never {
typealias MergeHighlightSuccess = Selection
}
+extension Objects {
+ struct MoveFilterError {
+ let __typename: TypeName = .moveFilterError
+ let errorCodes: [String: [Enums.MoveFilterErrorCode]]
+
+ enum TypeName: String, Codable {
+ case moveFilterError = "MoveFilterError"
+ }
+ }
+}
+
+extension Objects.MoveFilterError: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "errorCodes":
+ if let value = try container.decode([Enums.MoveFilterErrorCode]?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ errorCodes = map["errorCodes"]
+ }
+}
+
+extension Fields where TypeLock == Objects.MoveFilterError {
+ func errorCodes() throws -> [Enums.MoveFilterErrorCode] {
+ let field = GraphQLField.leaf(
+ name: "errorCodes",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.errorCodes[field.alias!] {
+ return data
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return []
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias MoveFilterError = Selection
+}
+
+extension Objects {
+ struct MoveFilterSuccess {
+ let __typename: TypeName = .moveFilterSuccess
+ let filter: [String: Objects.Filter]
+
+ enum TypeName: String, Codable {
+ case moveFilterSuccess = "MoveFilterSuccess"
+ }
+ }
+}
+
+extension Objects.MoveFilterSuccess: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "filter":
+ if let value = try container.decode(Objects.Filter?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ filter = map["filter"]
+ }
+}
+
+extension Fields where TypeLock == Objects.MoveFilterSuccess {
+ func filter(selection: Selection) throws -> Type {
+ let field = GraphQLField.composite(
+ name: "filter",
+ arguments: [],
+ selection: selection.selection
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.filter[field.alias!] {
+ return try selection.decode(data: data)
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return selection.mock()
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias MoveFilterSuccess = Selection
+}
+
extension Objects {
struct MoveLabelError {
let __typename: TypeName = .moveLabelError
@@ -8635,6 +9649,7 @@ extension Objects {
let addPopularRead: [String: Unions.AddPopularReadResult]
let createArticle: [String: Unions.CreateArticleResult]
let createArticleSavingRequest: [String: Unions.CreateArticleSavingRequestResult]
+ let createGroup: [String: Unions.CreateGroupResult]
let createHighlight: [String: Unions.CreateHighlightResult]
let createHighlightReply: [String: Unions.CreateHighlightReplyResult]
let createLabel: [String: Unions.CreateLabelResult]
@@ -8642,6 +9657,7 @@ extension Objects {
let createReaction: [String: Unions.CreateReactionResult]
let createReminder: [String: Unions.CreateReminderResult]
let deleteAccount: [String: Unions.DeleteAccountResult]
+ let deleteFilter: [String: Unions.DeleteFilterResult]
let deleteHighlight: [String: Unions.DeleteHighlightResult]
let deleteHighlightReply: [String: Unions.DeleteHighlightReplyResult]
let deleteIntegration: [String: Unions.DeleteIntegrationResult]
@@ -8654,14 +9670,18 @@ extension Objects {
let generateApiKey: [String: Unions.GenerateApiKeyResult]
let googleLogin: [String: Unions.LoginResult]
let googleSignup: [String: Unions.GoogleSignupResult]
+ let joinGroup: [String: Unions.JoinGroupResult]
let logOut: [String: Unions.LogOutResult]
let mergeHighlight: [String: Unions.MergeHighlightResult]
+ let moveFilter: [String: Unions.MoveFilterResult]
let moveLabel: [String: Unions.MoveLabelResult]
let optInFeature: [String: Unions.OptInFeatureResult]
+ let recommend: [String: Unions.RecommendResult]
let reportItem: [String: Objects.ReportItemResult]
let revokeApiKey: [String: Unions.RevokeApiKeyResult]
let saveArticleReadingProgress: [String: Unions.SaveArticleReadingProgressResult]
let saveFile: [String: Unions.SaveResult]
+ let saveFilter: [String: Unions.SaveFilterResult]
let savePage: [String: Unions.SaveResult]
let saveUrl: [String: Unions.SaveResult]
let setBookmarkArticle: [String: Unions.SetBookmarkArticleResult]
@@ -8719,6 +9739,10 @@ extension Objects.Mutation: Decodable {
if let value = try container.decode(Unions.CreateArticleSavingRequestResult?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
}
+ case "createGroup":
+ if let value = try container.decode(Unions.CreateGroupResult?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
case "createHighlight":
if let value = try container.decode(Unions.CreateHighlightResult?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
@@ -8747,6 +9771,10 @@ extension Objects.Mutation: Decodable {
if let value = try container.decode(Unions.DeleteAccountResult?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
}
+ case "deleteFilter":
+ if let value = try container.decode(Unions.DeleteFilterResult?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
case "deleteHighlight":
if let value = try container.decode(Unions.DeleteHighlightResult?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
@@ -8795,6 +9823,10 @@ extension Objects.Mutation: Decodable {
if let value = try container.decode(Unions.GoogleSignupResult?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
}
+ case "joinGroup":
+ if let value = try container.decode(Unions.JoinGroupResult?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
case "logOut":
if let value = try container.decode(Unions.LogOutResult?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
@@ -8803,6 +9835,10 @@ extension Objects.Mutation: Decodable {
if let value = try container.decode(Unions.MergeHighlightResult?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
}
+ case "moveFilter":
+ if let value = try container.decode(Unions.MoveFilterResult?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
case "moveLabel":
if let value = try container.decode(Unions.MoveLabelResult?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
@@ -8811,6 +9847,10 @@ extension Objects.Mutation: Decodable {
if let value = try container.decode(Unions.OptInFeatureResult?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
}
+ case "recommend":
+ if let value = try container.decode(Unions.RecommendResult?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
case "reportItem":
if let value = try container.decode(Objects.ReportItemResult?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
@@ -8827,6 +9867,10 @@ extension Objects.Mutation: Decodable {
if let value = try container.decode(Unions.SaveResult?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
}
+ case "saveFilter":
+ if let value = try container.decode(Unions.SaveFilterResult?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
case "savePage":
if let value = try container.decode(Unions.SaveResult?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
@@ -8944,6 +9988,7 @@ extension Objects.Mutation: Decodable {
addPopularRead = map["addPopularRead"]
createArticle = map["createArticle"]
createArticleSavingRequest = map["createArticleSavingRequest"]
+ createGroup = map["createGroup"]
createHighlight = map["createHighlight"]
createHighlightReply = map["createHighlightReply"]
createLabel = map["createLabel"]
@@ -8951,6 +9996,7 @@ extension Objects.Mutation: Decodable {
createReaction = map["createReaction"]
createReminder = map["createReminder"]
deleteAccount = map["deleteAccount"]
+ deleteFilter = map["deleteFilter"]
deleteHighlight = map["deleteHighlight"]
deleteHighlightReply = map["deleteHighlightReply"]
deleteIntegration = map["deleteIntegration"]
@@ -8963,14 +10009,18 @@ extension Objects.Mutation: Decodable {
generateApiKey = map["generateApiKey"]
googleLogin = map["googleLogin"]
googleSignup = map["googleSignup"]
+ joinGroup = map["joinGroup"]
logOut = map["logOut"]
mergeHighlight = map["mergeHighlight"]
+ moveFilter = map["moveFilter"]
moveLabel = map["moveLabel"]
optInFeature = map["optInFeature"]
+ recommend = map["recommend"]
reportItem = map["reportItem"]
revokeApiKey = map["revokeApiKey"]
saveArticleReadingProgress = map["saveArticleReadingProgress"]
saveFile = map["saveFile"]
+ saveFilter = map["saveFilter"]
savePage = map["savePage"]
saveUrl = map["saveUrl"]
setBookmarkArticle = map["setBookmarkArticle"]
@@ -9058,6 +10108,25 @@ extension Fields where TypeLock == Objects.Mutation {
}
}
+ func createGroup(input: InputObjects.CreateGroupInput, selection: Selection) throws -> Type {
+ let field = GraphQLField.composite(
+ name: "createGroup",
+ arguments: [Argument(name: "input", type: "CreateGroupInput!", value: input)],
+ selection: selection.selection
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.createGroup[field.alias!] {
+ return try selection.decode(data: data)
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return selection.mock()
+ }
+ }
+
func createHighlight(input: InputObjects.CreateHighlightInput, selection: Selection) throws -> Type {
let field = GraphQLField.composite(
name: "createHighlight",
@@ -9191,6 +10260,25 @@ extension Fields where TypeLock == Objects.Mutation {
}
}
+ func deleteFilter(id: String, selection: Selection) throws -> Type {
+ let field = GraphQLField.composite(
+ name: "deleteFilter",
+ arguments: [Argument(name: "id", type: "ID!", value: id)],
+ selection: selection.selection
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.deleteFilter[field.alias!] {
+ return try selection.decode(data: data)
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return selection.mock()
+ }
+ }
+
func deleteHighlight(highlightId: String, selection: Selection) throws -> Type {
let field = GraphQLField.composite(
name: "deleteHighlight",
@@ -9419,6 +10507,25 @@ extension Fields where TypeLock == Objects.Mutation {
}
}
+ func joinGroup(inviteCode: String, selection: Selection) throws -> Type {
+ let field = GraphQLField.composite(
+ name: "joinGroup",
+ arguments: [Argument(name: "inviteCode", type: "String!", value: inviteCode)],
+ selection: selection.selection
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.joinGroup[field.alias!] {
+ return try selection.decode(data: data)
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return selection.mock()
+ }
+ }
+
func logOut(selection: Selection) throws -> Type {
let field = GraphQLField.composite(
name: "logOut",
@@ -9457,6 +10564,25 @@ extension Fields where TypeLock == Objects.Mutation {
}
}
+ func moveFilter(input: InputObjects.MoveFilterInput, selection: Selection) throws -> Type {
+ let field = GraphQLField.composite(
+ name: "moveFilter",
+ arguments: [Argument(name: "input", type: "MoveFilterInput!", value: input)],
+ selection: selection.selection
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.moveFilter[field.alias!] {
+ return try selection.decode(data: data)
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return selection.mock()
+ }
+ }
+
func moveLabel(input: InputObjects.MoveLabelInput, selection: Selection) throws -> Type {
let field = GraphQLField.composite(
name: "moveLabel",
@@ -9495,6 +10621,25 @@ extension Fields where TypeLock == Objects.Mutation {
}
}
+ func recommend(input: InputObjects.RecommendInput, selection: Selection) throws -> Type {
+ let field = GraphQLField.composite(
+ name: "recommend",
+ arguments: [Argument(name: "input", type: "RecommendInput!", value: input)],
+ selection: selection.selection
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.recommend[field.alias!] {
+ return try selection.decode(data: data)
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return selection.mock()
+ }
+ }
+
func reportItem(input: InputObjects.ReportItemInput, selection: Selection) throws -> Type {
let field = GraphQLField.composite(
name: "reportItem",
@@ -9571,6 +10716,25 @@ extension Fields where TypeLock == Objects.Mutation {
}
}
+ func saveFilter(input: InputObjects.SaveFilterInput, selection: Selection) throws -> Type {
+ let field = GraphQLField.composite(
+ name: "saveFilter",
+ arguments: [Argument(name: "input", type: "SaveFilterInput!", value: input)],
+ selection: selection.selection
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.saveFilter[field.alias!] {
+ return try selection.decode(data: data)
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return selection.mock()
+ }
+ }
+
func savePage(input: InputObjects.SavePageInput, selection: Selection) throws -> Type {
let field = GraphQLField.composite(
name: "savePage",
@@ -11126,9 +12290,11 @@ extension Objects {
let articles: [String: Unions.ArticlesResult]
let deviceTokens: [String: Unions.DeviceTokensResult]
let feedArticles: [String: Unions.FeedArticlesResult]
+ let filters: [String: Unions.FiltersResult]
let getFollowers: [String: Unions.GetFollowersResult]
let getFollowing: [String: Unions.GetFollowingResult]
let getUserPersonalization: [String: Unions.GetUserPersonalizationResult]
+ let groups: [String: Unions.GroupsResult]
let hello: [String: String]
let integrations: [String: Unions.IntegrationsResult]
let labels: [String: Unions.LabelsResult]
@@ -11191,6 +12357,10 @@ extension Objects.Query: Decodable {
if let value = try container.decode(Unions.FeedArticlesResult?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
}
+ case "filters":
+ if let value = try container.decode(Unions.FiltersResult?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
case "getFollowers":
if let value = try container.decode(Unions.GetFollowersResult?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
@@ -11203,6 +12373,10 @@ extension Objects.Query: Decodable {
if let value = try container.decode(Unions.GetUserPersonalizationResult?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
}
+ case "groups":
+ if let value = try container.decode(Unions.GroupsResult?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
case "hello":
if let value = try container.decode(String?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
@@ -11295,9 +12469,11 @@ extension Objects.Query: Decodable {
articles = map["articles"]
deviceTokens = map["deviceTokens"]
feedArticles = map["feedArticles"]
+ filters = map["filters"]
getFollowers = map["getFollowers"]
getFollowing = map["getFollowing"]
getUserPersonalization = map["getUserPersonalization"]
+ groups = map["groups"]
hello = map["hello"]
integrations = map["integrations"]
labels = map["labels"]
@@ -11435,6 +12611,25 @@ extension Fields where TypeLock == Objects.Query {
}
}
+ func filters(selection: Selection) throws -> Type {
+ let field = GraphQLField.composite(
+ name: "filters",
+ arguments: [],
+ selection: selection.selection
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.filters[field.alias!] {
+ return try selection.decode(data: data)
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return selection.mock()
+ }
+ }
+
func getFollowers(userId: OptionalArgument = .absent(), selection: Selection) throws -> Type {
let field = GraphQLField.composite(
name: "getFollowers",
@@ -11492,6 +12687,25 @@ extension Fields where TypeLock == Objects.Query {
}
}
+ func groups(selection: Selection) throws -> Type {
+ let field = GraphQLField.composite(
+ name: "groups",
+ arguments: [],
+ selection: selection.selection
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.groups[field.alias!] {
+ return try selection.decode(data: data)
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return selection.mock()
+ }
+ }
+
func hello() throws -> String? {
let field = GraphQLField.leaf(
name: "hello",
@@ -12384,6 +13598,637 @@ extension Selection where TypeLock == Never, Type == Never {
typealias RecentSearchesSuccess = Selection
}
+extension Objects {
+ struct RecommendError {
+ let __typename: TypeName = .recommendError
+ let errorCodes: [String: [Enums.RecommendErrorCode]]
+
+ enum TypeName: String, Codable {
+ case recommendError = "RecommendError"
+ }
+ }
+}
+
+extension Objects.RecommendError: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "errorCodes":
+ if let value = try container.decode([Enums.RecommendErrorCode]?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ errorCodes = map["errorCodes"]
+ }
+}
+
+extension Fields where TypeLock == Objects.RecommendError {
+ func errorCodes() throws -> [Enums.RecommendErrorCode] {
+ let field = GraphQLField.leaf(
+ name: "errorCodes",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.errorCodes[field.alias!] {
+ return data
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return []
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias RecommendError = Selection
+}
+
+extension Objects {
+ struct RecommendSuccess {
+ let __typename: TypeName = .recommendSuccess
+ let taskNames: [String: [String]]
+
+ enum TypeName: String, Codable {
+ case recommendSuccess = "RecommendSuccess"
+ }
+ }
+}
+
+extension Objects.RecommendSuccess: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "taskNames":
+ if let value = try container.decode([String]?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ taskNames = map["taskNames"]
+ }
+}
+
+extension Fields where TypeLock == Objects.RecommendSuccess {
+ func taskNames() throws -> [String] {
+ let field = GraphQLField.leaf(
+ name: "taskNames",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.taskNames[field.alias!] {
+ return data
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return []
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias RecommendSuccess = Selection
+}
+
+extension Objects {
+ struct Recommendation {
+ let __typename: TypeName = .recommendation
+ let id: [String: String]
+ let name: [String: String]
+ let note: [String: String]
+ let recommendedAt: [String: DateTime]
+ let user: [String: Objects.RecommendingUser]
+
+ enum TypeName: String, Codable {
+ case recommendation = "Recommendation"
+ }
+ }
+}
+
+extension Objects.Recommendation: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "id":
+ if let value = try container.decode(String?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ case "name":
+ if let value = try container.decode(String?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ case "note":
+ if let value = try container.decode(String?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ case "recommendedAt":
+ if let value = try container.decode(DateTime?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ case "user":
+ if let value = try container.decode(Objects.RecommendingUser?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ id = map["id"]
+ name = map["name"]
+ note = map["note"]
+ recommendedAt = map["recommendedAt"]
+ user = map["user"]
+ }
+}
+
+extension Fields where TypeLock == Objects.Recommendation {
+ func id() throws -> String {
+ let field = GraphQLField.leaf(
+ name: "id",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.id[field.alias!] {
+ return data
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return String.mockValue
+ }
+ }
+
+ func name() throws -> String {
+ let field = GraphQLField.leaf(
+ name: "name",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.name[field.alias!] {
+ return data
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return String.mockValue
+ }
+ }
+
+ func note() throws -> String? {
+ let field = GraphQLField.leaf(
+ name: "note",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ return data.note[field.alias!]
+ case .mocking:
+ return nil
+ }
+ }
+
+ func recommendedAt() throws -> DateTime {
+ let field = GraphQLField.leaf(
+ name: "recommendedAt",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.recommendedAt[field.alias!] {
+ return data
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return DateTime.mockValue
+ }
+ }
+
+ func user(selection: Selection) throws -> Type {
+ let field = GraphQLField.composite(
+ name: "user",
+ arguments: [],
+ selection: selection.selection
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ return try selection.decode(data: data.user[field.alias!])
+ case .mocking:
+ return selection.mock()
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias Recommendation = Selection
+}
+
+extension Objects {
+ struct RecommendationGroup {
+ let __typename: TypeName = .recommendationGroup
+ let admins: [String: [Objects.User]]
+ let createdAt: [String: DateTime]
+ let id: [String: String]
+ let inviteUrl: [String: String]
+ let members: [String: [Objects.User]]
+ let name: [String: String]
+ let updatedAt: [String: DateTime]
+
+ enum TypeName: String, Codable {
+ case recommendationGroup = "RecommendationGroup"
+ }
+ }
+}
+
+extension Objects.RecommendationGroup: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "admins":
+ if let value = try container.decode([Objects.User]?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ case "createdAt":
+ if let value = try container.decode(DateTime?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ case "id":
+ if let value = try container.decode(String?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ case "inviteUrl":
+ if let value = try container.decode(String?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ case "members":
+ if let value = try container.decode([Objects.User]?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ case "name":
+ if let value = try container.decode(String?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ case "updatedAt":
+ if let value = try container.decode(DateTime?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ admins = map["admins"]
+ createdAt = map["createdAt"]
+ id = map["id"]
+ inviteUrl = map["inviteUrl"]
+ members = map["members"]
+ name = map["name"]
+ updatedAt = map["updatedAt"]
+ }
+}
+
+extension Fields where TypeLock == Objects.RecommendationGroup {
+ func admins(selection: Selection) throws -> Type {
+ let field = GraphQLField.composite(
+ name: "admins",
+ arguments: [],
+ selection: selection.selection
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.admins[field.alias!] {
+ return try selection.decode(data: data)
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return selection.mock()
+ }
+ }
+
+ func createdAt() throws -> DateTime {
+ let field = GraphQLField.leaf(
+ name: "createdAt",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.createdAt[field.alias!] {
+ return data
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return DateTime.mockValue
+ }
+ }
+
+ func id() throws -> String {
+ let field = GraphQLField.leaf(
+ name: "id",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.id[field.alias!] {
+ return data
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return String.mockValue
+ }
+ }
+
+ func inviteUrl() throws -> String {
+ let field = GraphQLField.leaf(
+ name: "inviteUrl",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.inviteUrl[field.alias!] {
+ return data
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return String.mockValue
+ }
+ }
+
+ func members(selection: Selection) throws -> Type {
+ let field = GraphQLField.composite(
+ name: "members",
+ arguments: [],
+ selection: selection.selection
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.members[field.alias!] {
+ return try selection.decode(data: data)
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return selection.mock()
+ }
+ }
+
+ func name() throws -> String {
+ let field = GraphQLField.leaf(
+ name: "name",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.name[field.alias!] {
+ return data
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return String.mockValue
+ }
+ }
+
+ func updatedAt() throws -> DateTime {
+ let field = GraphQLField.leaf(
+ name: "updatedAt",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.updatedAt[field.alias!] {
+ return data
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return DateTime.mockValue
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias RecommendationGroup = Selection
+}
+
+extension Objects {
+ struct RecommendingUser {
+ let __typename: TypeName = .recommendingUser
+ let name: [String: String]
+ let profileImageUrl: [String: String]
+ let userId: [String: String]
+ let username: [String: String]
+
+ enum TypeName: String, Codable {
+ case recommendingUser = "RecommendingUser"
+ }
+ }
+}
+
+extension Objects.RecommendingUser: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "name":
+ if let value = try container.decode(String?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ case "profileImageUrl":
+ if let value = try container.decode(String?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ case "userId":
+ if let value = try container.decode(String?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ case "username":
+ if let value = try container.decode(String?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ name = map["name"]
+ profileImageUrl = map["profileImageUrl"]
+ userId = map["userId"]
+ username = map["username"]
+ }
+}
+
+extension Fields where TypeLock == Objects.RecommendingUser {
+ func name() throws -> String {
+ let field = GraphQLField.leaf(
+ name: "name",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.name[field.alias!] {
+ return data
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return String.mockValue
+ }
+ }
+
+ func profileImageUrl() throws -> String? {
+ let field = GraphQLField.leaf(
+ name: "profileImageURL",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ return data.profileImageUrl[field.alias!]
+ case .mocking:
+ return nil
+ }
+ }
+
+ func userId() throws -> String {
+ let field = GraphQLField.leaf(
+ name: "userId",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.userId[field.alias!] {
+ return data
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return String.mockValue
+ }
+ }
+
+ func username() throws -> String {
+ let field = GraphQLField.leaf(
+ name: "username",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.username[field.alias!] {
+ return data
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return String.mockValue
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias RecommendingUser = Selection
+}
+
extension Objects {
struct Reminder {
let __typename: TypeName = .reminder
@@ -13495,6 +15340,137 @@ extension Selection where TypeLock == Never, Type == Never {
typealias SaveError = Selection
}
+extension Objects {
+ struct SaveFilterError {
+ let __typename: TypeName = .saveFilterError
+ let errorCodes: [String: [Enums.SaveFilterErrorCode]]
+
+ enum TypeName: String, Codable {
+ case saveFilterError = "SaveFilterError"
+ }
+ }
+}
+
+extension Objects.SaveFilterError: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "errorCodes":
+ if let value = try container.decode([Enums.SaveFilterErrorCode]?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ errorCodes = map["errorCodes"]
+ }
+}
+
+extension Fields where TypeLock == Objects.SaveFilterError {
+ func errorCodes() throws -> [Enums.SaveFilterErrorCode] {
+ let field = GraphQLField.leaf(
+ name: "errorCodes",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.errorCodes[field.alias!] {
+ return data
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return []
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias SaveFilterError = Selection
+}
+
+extension Objects {
+ struct SaveFilterSuccess {
+ let __typename: TypeName = .saveFilterSuccess
+ let filter: [String: Objects.Filter]
+
+ enum TypeName: String, Codable {
+ case saveFilterSuccess = "SaveFilterSuccess"
+ }
+ }
+}
+
+extension Objects.SaveFilterSuccess: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "filter":
+ if let value = try container.decode(Objects.Filter?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ filter = map["filter"]
+ }
+}
+
+extension Fields where TypeLock == Objects.SaveFilterSuccess {
+ func filter(selection: Selection) throws -> Type {
+ let field = GraphQLField.composite(
+ name: "filter",
+ arguments: [],
+ selection: selection.selection
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.filter[field.alias!] {
+ return try selection.decode(data: data)
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return selection.mock()
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias SaveFilterSuccess = Selection
+}
+
extension Objects {
struct SaveSuccess {
let __typename: TypeName = .saveSuccess
@@ -13672,6 +15648,7 @@ extension Objects {
let readAt: [String: DateTime]
let readingProgressAnchorIndex: [String: Int]
let readingProgressPercent: [String: Double]
+ let recommendations: [String: [Objects.Recommendation]]
let savedAt: [String: DateTime]
let shortId: [String: String]
let siteIcon: [String: String]
@@ -13784,6 +15761,10 @@ extension Objects.SearchItem: Decodable {
if let value = try container.decode(Double?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
}
+ case "recommendations":
+ if let value = try container.decode([Objects.Recommendation]?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
case "savedAt":
if let value = try container.decode(DateTime?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
@@ -13866,6 +15847,7 @@ extension Objects.SearchItem: Decodable {
readAt = map["readAt"]
readingProgressAnchorIndex = map["readingProgressAnchorIndex"]
readingProgressPercent = map["readingProgressPercent"]
+ recommendations = map["recommendations"]
savedAt = map["savedAt"]
shortId = map["shortId"]
siteIcon = map["siteIcon"]
@@ -14206,6 +16188,22 @@ extension Fields where TypeLock == Objects.SearchItem {
}
}
+ func recommendations(selection: Selection) throws -> Type {
+ let field = GraphQLField.composite(
+ name: "recommendations",
+ arguments: [],
+ selection: selection.selection
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ return try selection.decode(data: data.recommendations[field.alias!])
+ case .mocking:
+ return selection.mock()
+ }
+ }
+
func savedAt() throws -> DateTime {
let field = GraphQLField.leaf(
name: "savedAt",
@@ -20940,6 +22938,80 @@ extension Selection where TypeLock == Never, Type == Never {
typealias CreateArticleSavingRequestResult = Selection
}
+extension Unions {
+ struct CreateGroupResult {
+ let __typename: TypeName
+ let errorCodes: [String: [Enums.CreateGroupErrorCode]]
+ let group: [String: Objects.RecommendationGroup]
+
+ enum TypeName: String, Codable {
+ case createGroupError = "CreateGroupError"
+ case createGroupSuccess = "CreateGroupSuccess"
+ }
+ }
+}
+
+extension Unions.CreateGroupResult: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "errorCodes":
+ if let value = try container.decode([Enums.CreateGroupErrorCode]?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ case "group":
+ if let value = try container.decode(Objects.RecommendationGroup?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ __typename = try container.decode(TypeName.self, forKey: DynamicCodingKeys(stringValue: "__typename")!)
+
+ errorCodes = map["errorCodes"]
+ group = map["group"]
+ }
+}
+
+extension Fields where TypeLock == Unions.CreateGroupResult {
+ func on(createGroupError: Selection, createGroupSuccess: Selection) throws -> Type {
+ select([GraphQLField.fragment(type: "CreateGroupError", selection: createGroupError.selection), GraphQLField.fragment(type: "CreateGroupSuccess", selection: createGroupSuccess.selection)])
+
+ switch response {
+ case let .decoding(data):
+ switch data.__typename {
+ case .createGroupError:
+ let data = Objects.CreateGroupError(errorCodes: data.errorCodes)
+ return try createGroupError.decode(data: data)
+ case .createGroupSuccess:
+ let data = Objects.CreateGroupSuccess(group: data.group)
+ return try createGroupSuccess.decode(data: data)
+ }
+ case .mocking:
+ return createGroupError.mock()
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias CreateGroupResult = Selection
+}
+
extension Unions {
struct CreateHighlightReplyResult {
let __typename: TypeName
@@ -21458,6 +23530,80 @@ extension Selection where TypeLock == Never, Type == Never {
typealias DeleteAccountResult = Selection
}
+extension Unions {
+ struct DeleteFilterResult {
+ let __typename: TypeName
+ let errorCodes: [String: [Enums.DeleteFilterErrorCode]]
+ let filter: [String: Objects.Filter]
+
+ enum TypeName: String, Codable {
+ case deleteFilterError = "DeleteFilterError"
+ case deleteFilterSuccess = "DeleteFilterSuccess"
+ }
+ }
+}
+
+extension Unions.DeleteFilterResult: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "errorCodes":
+ if let value = try container.decode([Enums.DeleteFilterErrorCode]?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ case "filter":
+ if let value = try container.decode(Objects.Filter?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ __typename = try container.decode(TypeName.self, forKey: DynamicCodingKeys(stringValue: "__typename")!)
+
+ errorCodes = map["errorCodes"]
+ filter = map["filter"]
+ }
+}
+
+extension Fields where TypeLock == Unions.DeleteFilterResult {
+ func on(deleteFilterError: Selection, deleteFilterSuccess: Selection) throws -> Type {
+ select([GraphQLField.fragment(type: "DeleteFilterError", selection: deleteFilterError.selection), GraphQLField.fragment(type: "DeleteFilterSuccess", selection: deleteFilterSuccess.selection)])
+
+ switch response {
+ case let .decoding(data):
+ switch data.__typename {
+ case .deleteFilterError:
+ let data = Objects.DeleteFilterError(errorCodes: data.errorCodes)
+ return try deleteFilterError.decode(data: data)
+ case .deleteFilterSuccess:
+ let data = Objects.DeleteFilterSuccess(filter: data.filter)
+ return try deleteFilterSuccess.decode(data: data)
+ }
+ case .mocking:
+ return deleteFilterError.mock()
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias DeleteFilterResult = Selection
+}
+
extension Unions {
struct DeleteHighlightReplyResult {
let __typename: TypeName
@@ -22278,6 +24424,80 @@ extension Selection where TypeLock == Never, Type == Never {
typealias FeedArticlesResult = Selection
}
+extension Unions {
+ struct FiltersResult {
+ let __typename: TypeName
+ let errorCodes: [String: [Enums.FiltersErrorCode]]
+ let filters: [String: [Objects.Filter]]
+
+ enum TypeName: String, Codable {
+ case filtersError = "FiltersError"
+ case filtersSuccess = "FiltersSuccess"
+ }
+ }
+}
+
+extension Unions.FiltersResult: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "errorCodes":
+ if let value = try container.decode([Enums.FiltersErrorCode]?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ case "filters":
+ if let value = try container.decode([Objects.Filter]?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ __typename = try container.decode(TypeName.self, forKey: DynamicCodingKeys(stringValue: "__typename")!)
+
+ errorCodes = map["errorCodes"]
+ filters = map["filters"]
+ }
+}
+
+extension Fields where TypeLock == Unions.FiltersResult {
+ func on(filtersError: Selection, filtersSuccess: Selection) throws -> Type {
+ select([GraphQLField.fragment(type: "FiltersError", selection: filtersError.selection), GraphQLField.fragment(type: "FiltersSuccess", selection: filtersSuccess.selection)])
+
+ switch response {
+ case let .decoding(data):
+ switch data.__typename {
+ case .filtersError:
+ let data = Objects.FiltersError(errorCodes: data.errorCodes)
+ return try filtersError.decode(data: data)
+ case .filtersSuccess:
+ let data = Objects.FiltersSuccess(filters: data.filters)
+ return try filtersSuccess.decode(data: data)
+ }
+ case .mocking:
+ return filtersError.mock()
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias FiltersResult = Selection
+}
+
extension Unions {
struct GenerateApiKeyResult {
let __typename: TypeName
@@ -22648,6 +24868,80 @@ extension Selection where TypeLock == Never, Type == Never {
typealias GoogleSignupResult = Selection
}
+extension Unions {
+ struct GroupsResult {
+ let __typename: TypeName
+ let errorCodes: [String: [Enums.GroupsErrorCode]]
+ let groups: [String: [Objects.RecommendationGroup]]
+
+ enum TypeName: String, Codable {
+ case groupsError = "GroupsError"
+ case groupsSuccess = "GroupsSuccess"
+ }
+ }
+}
+
+extension Unions.GroupsResult: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "errorCodes":
+ if let value = try container.decode([Enums.GroupsErrorCode]?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ case "groups":
+ if let value = try container.decode([Objects.RecommendationGroup]?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ __typename = try container.decode(TypeName.self, forKey: DynamicCodingKeys(stringValue: "__typename")!)
+
+ errorCodes = map["errorCodes"]
+ groups = map["groups"]
+ }
+}
+
+extension Fields where TypeLock == Unions.GroupsResult {
+ func on(groupsError: Selection, groupsSuccess: Selection) throws -> Type {
+ select([GraphQLField.fragment(type: "GroupsError", selection: groupsError.selection), GraphQLField.fragment(type: "GroupsSuccess", selection: groupsSuccess.selection)])
+
+ switch response {
+ case let .decoding(data):
+ switch data.__typename {
+ case .groupsError:
+ let data = Objects.GroupsError(errorCodes: data.errorCodes)
+ return try groupsError.decode(data: data)
+ case .groupsSuccess:
+ let data = Objects.GroupsSuccess(groups: data.groups)
+ return try groupsSuccess.decode(data: data)
+ }
+ case .mocking:
+ return groupsError.mock()
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias GroupsResult = Selection
+}
+
extension Unions {
struct IntegrationsResult {
let __typename: TypeName
@@ -22722,6 +25016,80 @@ extension Selection where TypeLock == Never, Type == Never {
typealias IntegrationsResult = Selection
}
+extension Unions {
+ struct JoinGroupResult {
+ let __typename: TypeName
+ let errorCodes: [String: [Enums.JoinGroupErrorCode]]
+ let group: [String: Objects.RecommendationGroup]
+
+ enum TypeName: String, Codable {
+ case joinGroupError = "JoinGroupError"
+ case joinGroupSuccess = "JoinGroupSuccess"
+ }
+ }
+}
+
+extension Unions.JoinGroupResult: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "errorCodes":
+ if let value = try container.decode([Enums.JoinGroupErrorCode]?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ case "group":
+ if let value = try container.decode(Objects.RecommendationGroup?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ __typename = try container.decode(TypeName.self, forKey: DynamicCodingKeys(stringValue: "__typename")!)
+
+ errorCodes = map["errorCodes"]
+ group = map["group"]
+ }
+}
+
+extension Fields where TypeLock == Unions.JoinGroupResult {
+ func on(joinGroupError: Selection, joinGroupSuccess: Selection) throws -> Type {
+ select([GraphQLField.fragment(type: "JoinGroupError", selection: joinGroupError.selection), GraphQLField.fragment(type: "JoinGroupSuccess", selection: joinGroupSuccess.selection)])
+
+ switch response {
+ case let .decoding(data):
+ switch data.__typename {
+ case .joinGroupError:
+ let data = Objects.JoinGroupError(errorCodes: data.errorCodes)
+ return try joinGroupError.decode(data: data)
+ case .joinGroupSuccess:
+ let data = Objects.JoinGroupSuccess(group: data.group)
+ return try joinGroupSuccess.decode(data: data)
+ }
+ case .mocking:
+ return joinGroupError.mock()
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias JoinGroupResult = Selection
+}
+
extension Unions {
struct LabelsResult {
let __typename: TypeName
@@ -23024,6 +25392,80 @@ extension Selection where TypeLock == Never, Type == Never {
typealias MergeHighlightResult = Selection
}
+extension Unions {
+ struct MoveFilterResult {
+ let __typename: TypeName
+ let errorCodes: [String: [Enums.MoveFilterErrorCode]]
+ let filter: [String: Objects.Filter]
+
+ enum TypeName: String, Codable {
+ case moveFilterError = "MoveFilterError"
+ case moveFilterSuccess = "MoveFilterSuccess"
+ }
+ }
+}
+
+extension Unions.MoveFilterResult: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "errorCodes":
+ if let value = try container.decode([Enums.MoveFilterErrorCode]?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ case "filter":
+ if let value = try container.decode(Objects.Filter?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ __typename = try container.decode(TypeName.self, forKey: DynamicCodingKeys(stringValue: "__typename")!)
+
+ errorCodes = map["errorCodes"]
+ filter = map["filter"]
+ }
+}
+
+extension Fields where TypeLock == Unions.MoveFilterResult {
+ func on(moveFilterError: Selection, moveFilterSuccess: Selection) throws -> Type {
+ select([GraphQLField.fragment(type: "MoveFilterError", selection: moveFilterError.selection), GraphQLField.fragment(type: "MoveFilterSuccess", selection: moveFilterSuccess.selection)])
+
+ switch response {
+ case let .decoding(data):
+ switch data.__typename {
+ case .moveFilterError:
+ let data = Objects.MoveFilterError(errorCodes: data.errorCodes)
+ return try moveFilterError.decode(data: data)
+ case .moveFilterSuccess:
+ let data = Objects.MoveFilterSuccess(filter: data.filter)
+ return try moveFilterSuccess.decode(data: data)
+ }
+ case .mocking:
+ return moveFilterError.mock()
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias MoveFilterResult = Selection
+}
+
extension Unions {
struct MoveLabelResult {
let __typename: TypeName
@@ -23320,6 +25762,80 @@ extension Selection where TypeLock == Never, Type == Never {
typealias RecentSearchesResult = Selection
}
+extension Unions {
+ struct RecommendResult {
+ let __typename: TypeName
+ let errorCodes: [String: [Enums.RecommendErrorCode]]
+ let taskNames: [String: [String]]
+
+ enum TypeName: String, Codable {
+ case recommendError = "RecommendError"
+ case recommendSuccess = "RecommendSuccess"
+ }
+ }
+}
+
+extension Unions.RecommendResult: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "errorCodes":
+ if let value = try container.decode([Enums.RecommendErrorCode]?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ case "taskNames":
+ if let value = try container.decode([String]?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ __typename = try container.decode(TypeName.self, forKey: DynamicCodingKeys(stringValue: "__typename")!)
+
+ errorCodes = map["errorCodes"]
+ taskNames = map["taskNames"]
+ }
+}
+
+extension Fields where TypeLock == Unions.RecommendResult {
+ func on(recommendError: Selection, recommendSuccess: Selection) throws -> Type {
+ select([GraphQLField.fragment(type: "RecommendError", selection: recommendError.selection), GraphQLField.fragment(type: "RecommendSuccess", selection: recommendSuccess.selection)])
+
+ switch response {
+ case let .decoding(data):
+ switch data.__typename {
+ case .recommendError:
+ let data = Objects.RecommendError(errorCodes: data.errorCodes)
+ return try recommendError.decode(data: data)
+ case .recommendSuccess:
+ let data = Objects.RecommendSuccess(taskNames: data.taskNames)
+ return try recommendSuccess.decode(data: data)
+ }
+ case .mocking:
+ return recommendError.mock()
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias RecommendResult = Selection
+}
+
extension Unions {
struct ReminderResult {
let __typename: TypeName
@@ -23616,6 +26132,80 @@ extension Selection where TypeLock == Never, Type == Never {
typealias SaveArticleReadingProgressResult = Selection
}
+extension Unions {
+ struct SaveFilterResult {
+ let __typename: TypeName
+ let errorCodes: [String: [Enums.SaveFilterErrorCode]]
+ let filter: [String: Objects.Filter]
+
+ enum TypeName: String, Codable {
+ case saveFilterError = "SaveFilterError"
+ case saveFilterSuccess = "SaveFilterSuccess"
+ }
+ }
+}
+
+extension Unions.SaveFilterResult: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "errorCodes":
+ if let value = try container.decode([Enums.SaveFilterErrorCode]?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ case "filter":
+ if let value = try container.decode(Objects.Filter?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ __typename = try container.decode(TypeName.self, forKey: DynamicCodingKeys(stringValue: "__typename")!)
+
+ errorCodes = map["errorCodes"]
+ filter = map["filter"]
+ }
+}
+
+extension Fields where TypeLock == Unions.SaveFilterResult {
+ func on(saveFilterError: Selection, saveFilterSuccess: Selection) throws -> Type {
+ select([GraphQLField.fragment(type: "SaveFilterError", selection: saveFilterError.selection), GraphQLField.fragment(type: "SaveFilterSuccess", selection: saveFilterSuccess.selection)])
+
+ switch response {
+ case let .decoding(data):
+ switch data.__typename {
+ case .saveFilterError:
+ let data = Objects.SaveFilterError(errorCodes: data.errorCodes)
+ return try saveFilterError.decode(data: data)
+ case .saveFilterSuccess:
+ let data = Objects.SaveFilterSuccess(filter: data.filter)
+ return try saveFilterSuccess.decode(data: data)
+ }
+ case .mocking:
+ return saveFilterError.mock()
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias SaveFilterResult = Selection
+}
+
extension Unions {
struct SaveResult {
let __typename: TypeName
@@ -26227,6 +28817,15 @@ extension Enums {
}
}
+extension Enums {
+ /// CreateGroupErrorCode
+ enum CreateGroupErrorCode: String, CaseIterable, Codable {
+ case badRequest = "BAD_REQUEST"
+
+ case unauthorized = "UNAUTHORIZED"
+ }
+}
+
extension Enums {
/// CreateHighlightErrorCode
enum CreateHighlightErrorCode: String, CaseIterable, Codable {
@@ -26314,6 +28913,17 @@ extension Enums {
}
}
+extension Enums {
+ /// DeleteFilterErrorCode
+ enum DeleteFilterErrorCode: String, CaseIterable, Codable {
+ case badRequest = "BAD_REQUEST"
+
+ case notFound = "NOT_FOUND"
+
+ case unauthorized = "UNAUTHORIZED"
+ }
+}
+
extension Enums {
/// DeleteHighlightErrorCode
enum DeleteHighlightErrorCode: String, CaseIterable, Codable {
@@ -26429,6 +29039,15 @@ extension Enums {
}
}
+extension Enums {
+ /// FiltersErrorCode
+ enum FiltersErrorCode: String, CaseIterable, Codable {
+ case badRequest = "BAD_REQUEST"
+
+ case unauthorized = "UNAUTHORIZED"
+ }
+}
+
extension Enums {
/// GenerateApiKeyErrorCode
enum GenerateApiKeyErrorCode: String, CaseIterable, Codable {
@@ -26461,6 +29080,15 @@ extension Enums {
}
}
+extension Enums {
+ /// GroupsErrorCode
+ enum GroupsErrorCode: String, CaseIterable, Codable {
+ case badRequest = "BAD_REQUEST"
+
+ case unauthorized = "UNAUTHORIZED"
+ }
+}
+
extension Enums {
/// IntegrationType
enum IntegrationType: String, CaseIterable, Codable {
@@ -26477,6 +29105,17 @@ extension Enums {
}
}
+extension Enums {
+ /// JoinGroupErrorCode
+ enum JoinGroupErrorCode: String, CaseIterable, Codable {
+ case badRequest = "BAD_REQUEST"
+
+ case notFound = "NOT_FOUND"
+
+ case unauthorized = "UNAUTHORIZED"
+ }
+}
+
extension Enums {
/// LabelsErrorCode
enum LabelsErrorCode: String, CaseIterable, Codable {
@@ -26527,6 +29166,17 @@ extension Enums {
}
}
+extension Enums {
+ /// MoveFilterErrorCode
+ enum MoveFilterErrorCode: String, CaseIterable, Codable {
+ case badRequest = "BAD_REQUEST"
+
+ case notFound = "NOT_FOUND"
+
+ case unauthorized = "UNAUTHORIZED"
+ }
+}
+
extension Enums {
/// MoveLabelErrorCode
enum MoveLabelErrorCode: String, CaseIterable, Codable {
@@ -26601,6 +29251,17 @@ extension Enums {
}
}
+extension Enums {
+ /// RecommendErrorCode
+ enum RecommendErrorCode: String, CaseIterable, Codable {
+ case badRequest = "BAD_REQUEST"
+
+ case notFound = "NOT_FOUND"
+
+ case unauthorized = "UNAUTHORIZED"
+ }
+}
+
extension Enums {
/// ReminderErrorCode
enum ReminderErrorCode: String, CaseIterable, Codable {
@@ -26680,6 +29341,17 @@ extension Enums {
}
}
+extension Enums {
+ /// SaveFilterErrorCode
+ enum SaveFilterErrorCode: String, CaseIterable, Codable {
+ case badRequest = "BAD_REQUEST"
+
+ case notFound = "NOT_FOUND"
+
+ case unauthorized = "UNAUTHORIZED"
+ }
+}
+
extension Enums {
/// SearchErrorCode
enum SearchErrorCode: String, CaseIterable, Codable {
@@ -27208,6 +29880,29 @@ extension InputObjects {
}
}
+extension InputObjects {
+ struct CreateGroupInput: Encodable, Hashable {
+ var expiresInDays: OptionalArgument = .absent()
+
+ var maxMembers: OptionalArgument = .absent()
+
+ var name: String
+
+ func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+ if expiresInDays.hasValue { try container.encode(expiresInDays, forKey: .expiresInDays) }
+ if maxMembers.hasValue { try container.encode(maxMembers, forKey: .maxMembers) }
+ try container.encode(name, forKey: .name)
+ }
+
+ enum CodingKeys: String, CodingKey {
+ case expiresInDays
+ case maxMembers
+ case name
+ }
+ }
+}
+
extension InputObjects {
struct CreateHighlightInput: Encodable, Hashable {
var annotation: OptionalArgument = .absent()
@@ -27495,6 +30190,25 @@ extension InputObjects {
}
}
+extension InputObjects {
+ struct MoveFilterInput: Encodable, Hashable {
+ var afterFilterId: OptionalArgument = .absent()
+
+ var filterId: String
+
+ func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+ if afterFilterId.hasValue { try container.encode(afterFilterId, forKey: .afterFilterId) }
+ try container.encode(filterId, forKey: .filterId)
+ }
+
+ enum CodingKeys: String, CodingKey {
+ case afterFilterId
+ case filterId
+ }
+ }
+}
+
extension InputObjects {
struct MoveLabelInput: Encodable, Hashable {
var afterLabelId: OptionalArgument = .absent()
@@ -27587,6 +30301,29 @@ extension InputObjects {
}
}
+extension InputObjects {
+ struct RecommendInput: Encodable, Hashable {
+ var groupIds: [String]
+
+ var note: OptionalArgument = .absent()
+
+ var pageId: String
+
+ func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+ try container.encode(groupIds, forKey: .groupIds)
+ if note.hasValue { try container.encode(note, forKey: .note) }
+ try container.encode(pageId, forKey: .pageId)
+ }
+
+ enum CodingKeys: String, CodingKey {
+ case groupIds
+ case note
+ case pageId
+ }
+ }
+}
+
extension InputObjects {
struct ReportItemInput: Encodable, Hashable {
var itemUrl: String
@@ -27687,6 +30424,33 @@ extension InputObjects {
}
}
+extension InputObjects {
+ struct SaveFilterInput: Encodable, Hashable {
+ var description: OptionalArgument = .absent()
+
+ var filter: String
+
+ var id: OptionalArgument = .absent()
+
+ var name: String
+
+ func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+ if description.hasValue { try container.encode(description, forKey: .description) }
+ try container.encode(filter, forKey: .filter)
+ if id.hasValue { try container.encode(id, forKey: .id) }
+ try container.encode(name, forKey: .name)
+ }
+
+ enum CodingKeys: String, CodingKey {
+ case description
+ case filter
+ case id
+ case name
+ }
+ }
+}
+
extension InputObjects {
struct SavePageInput: Encodable, Hashable {
var clientRequestId: String
@@ -28149,6 +30913,8 @@ extension InputObjects {
var pageId: String
+ var savedAt: OptionalArgument = .absent()
+
var title: OptionalArgument = .absent()
func encode(to encoder: Encoder) throws {
@@ -28156,6 +30922,7 @@ extension InputObjects {
if byline.hasValue { try container.encode(byline, forKey: .byline) }
if description.hasValue { try container.encode(description, forKey: .description) }
try container.encode(pageId, forKey: .pageId)
+ if savedAt.hasValue { try container.encode(savedAt, forKey: .savedAt) }
if title.hasValue { try container.encode(title, forKey: .title) }
}
@@ -28163,6 +30930,7 @@ extension InputObjects {
case byline
case description
case pageId
+ case savedAt
case title
}
}
diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateRecommendationGroup.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateRecommendationGroup.swift
new file mode 100644
index 000000000..646cf45dc
--- /dev/null
+++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateRecommendationGroup.swift
@@ -0,0 +1,53 @@
+import CoreData
+import Foundation
+import Models
+import SwiftGraphQL
+
+public extension DataService {
+ func createRecommendationGroup(name: String) async throws -> InternalRecommendationGroup {
+ enum MutationResult {
+ case saved(recommendationGroup: InternalRecommendationGroup)
+ case error(errorCode: Enums.CreateGroupErrorCode)
+ }
+
+ let selection = Selection {
+ try $0.on(
+ createGroupError: .init {
+ .error(errorCode: try $0.errorCodes().first ?? .badRequest)
+ },
+ createGroupSuccess: .init {
+ .saved(recommendationGroup: try $0.group(selection: recommendationGroupSelection))
+ }
+ )
+ }
+
+ let input = InputObjects.CreateGroupInput(name: name)
+
+ let mutation = Selection.Mutation {
+ try $0.createGroup(input: input, selection: selection)
+ }
+
+ let path = appEnvironment.graphqlPath
+ let headers = networker.defaultHeaders
+
+ return try await withCheckedThrowingContinuation { continuation in
+ send(mutation, to: path, headers: headers) { [weak self] queryResult in
+ guard let payload = try? queryResult.get(), let self = self else {
+ continuation.resume(throwing: BasicError.message(messageText: "network error"))
+ return
+ }
+
+ switch payload.data {
+ case let .saved(recommendationGroup: recommendationGroup):
+ if [recommendationGroup].persist(context: self.backgroundContext) != nil {
+ continuation.resume(returning: recommendationGroup)
+ } else {
+ continuation.resume(throwing: BasicError.message(messageText: "CoreData error"))
+ }
+ case let .error(errorCode: errorCode):
+ continuation.resume(throwing: BasicError.message(messageText: errorCode.rawValue))
+ }
+ }
+ }
+ }
+}
diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RecommendPage.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RecommendPage.swift
new file mode 100644
index 000000000..68f604817
--- /dev/null
+++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RecommendPage.swift
@@ -0,0 +1,48 @@
+import CoreData
+import Foundation
+import Models
+import SwiftGraphQL
+
+public extension DataService {
+ func recommendPage(pageID: String, groupIDs: [String], note: String?) async throws {
+ enum MutationResult {
+ case saved(taskNames: [String])
+ case error(errorMessage: String)
+ }
+
+ let selection = Selection {
+ try $0.on(
+ recommendError: .init { .error(errorMessage: try $0.errorCodes().first.toString()) },
+ recommendSuccess: .init {
+ .saved(taskNames: try $0.taskNames())
+ }
+ )
+ }
+
+ let mutation = Selection.Mutation {
+ try $0.recommend(
+ input: .init(groupIds: groupIDs, note: OptionalArgument(note), pageId: pageID),
+ selection: selection
+ )
+ }
+
+ let path = appEnvironment.graphqlPath
+ let headers = networker.defaultHeaders
+
+ return try await withCheckedThrowingContinuation { continuation in
+ send(mutation, to: path, headers: headers) { queryResult in
+ guard let payload = try? queryResult.get() else {
+ continuation.resume(throwing: BasicError.message(messageText: "network error"))
+ return
+ }
+
+ switch payload.data {
+ case .saved:
+ continuation.resume()
+ case let .error(errorMessage: errorMessage):
+ continuation.resume(throwing: BasicError.message(messageText: errorMessage))
+ }
+ }
+ }
+ }
+}
diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePage.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePage.swift
index f8b427b37..bf4d9ffef 100644
--- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePage.swift
+++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePage.swift
@@ -72,6 +72,7 @@ extension SaveArticleError {
case .network, .timeout:
return .network
case .badpayload, .badURL, .badstatus, .cancelled:
+ print("HTTP ERROR", httpError)
return .unknown(description: httpError.localizedDescription)
}
}
diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift
index 46c4160db..a9170e38e 100644
--- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift
+++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift
@@ -44,6 +44,7 @@ extension DataService {
contentReader: try $0.contentReader().rawValue,
originalHtml: nil,
language: try $0.language(),
+ recommendations: try $0.recommendations(selection: recommendationSelection.list.nullable) ?? [],
labels: try $0.labels(selection: feedItemLabelSelection.list.nullable) ?? []
),
htmlContent: try $0.content(),
diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift
index ce5d1fb7e..51ded19f7 100644
--- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift
+++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift
@@ -224,6 +224,40 @@ extension DataService {
}
}
+let recommendingUserSelection = Selection.RecommendingUser {
+ do {
+ return InternalUserProfile(
+ userID: try $0.userId(),
+ name: try $0.name(),
+ username: try $0.username(),
+ profileImageURL: nil // try $0.profileImageUrl() ?? nil
+ )
+ } catch {
+ print("ERROR WITH recommendingUserSelection", error)
+ throw error
+ }
+}
+
+let recommendationSelection = Selection.Recommendation {
+ do {
+ let result = InternalRecommendation(
+ id: try $0.id(),
+ name: try $0.name(),
+ user: try $0.user(selection: recommendingUserSelection.nullable),
+ recommendedAt: try $0.recommendedAt().value ?? Date()
+ )
+ return result
+ } catch {
+ print("ERROR WITH recommendationSelection", error)
+ throw error
+ }
+}
+
+private func emptyrecommended() -> [InternalRecommendation] {
+ print("got the empty InternalRecommendation")
+ return []
+}
+
private let libraryArticleSelection = Selection.Article {
InternalLinkedItem(
id: try $0.id(),
@@ -249,6 +283,7 @@ private let libraryArticleSelection = Selection.Article {
contentReader: try $0.contentReader().rawValue,
originalHtml: nil,
language: try $0.language(),
+ recommendations: try $0.recommendations(selection: recommendationSelection.list.nullable) ?? emptyrecommended(),
labels: try $0.labels(selection: feedItemLabelSelection.list.nullable) ?? []
)
}
@@ -286,6 +321,7 @@ private let searchItemSelection = Selection.SearchItem {
contentReader: try $0.contentReader().rawValue,
originalHtml: nil,
language: try $0.language(),
+ recommendations: try $0.recommendations(selection: recommendationSelection.list.nullable) ?? [],
labels: try $0.labels(selection: feedItemLabelSelection.list.nullable) ?? []
)
}
diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/RecommendationGroupsQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/RecommendationGroupsQuery.swift
new file mode 100644
index 000000000..a17a5c52c
--- /dev/null
+++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/RecommendationGroupsQuery.swift
@@ -0,0 +1,52 @@
+import CoreData
+import Foundation
+import Models
+import SwiftGraphQL
+
+public extension DataService {
+ func recommendationGroups() async throws -> [InternalRecommendationGroup] {
+ enum QueryResult {
+ case success(result: [InternalRecommendationGroup])
+ case error(error: String)
+ }
+
+ let selection = Selection {
+ try $0.on(
+ groupsError: .init {
+ QueryResult.error(error: try $0.errorCodes().description)
+ },
+ groupsSuccess: .init {
+ QueryResult.success(result: try $0.groups(selection: recommendationGroupSelection.list))
+ }
+ )
+ }
+
+ let query = Selection.Query {
+ try $0.groups(selection: selection)
+ }
+
+ let path = appEnvironment.graphqlPath
+ let headers = networker.defaultHeaders
+ let context = backgroundContext
+
+ return try await withCheckedThrowingContinuation { continuation in
+ send(query, to: path, headers: headers) { queryResult in
+ guard let payload = try? queryResult.get() else {
+ continuation.resume(throwing: BasicError.message(messageText: "network request failed"))
+ return
+ }
+
+ switch payload.data {
+ case let .success(result: result):
+ if result.persist(context: context) != nil {
+ continuation.resume(returning: result)
+ } else {
+ continuation.resume(throwing: BasicError.message(messageText: "CoreData error"))
+ }
+ case .error:
+ continuation.resume(throwing: BasicError.message(messageText: "Recommendation Groups fetch error"))
+ }
+ }
+ }
+ }
+}
diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Selections/RecommendationGroupSelection.swift b/apple/OmnivoreKit/Sources/Services/DataService/Selections/RecommendationGroupSelection.swift
new file mode 100644
index 000000000..7759f9814
--- /dev/null
+++ b/apple/OmnivoreKit/Sources/Services/DataService/Selections/RecommendationGroupSelection.swift
@@ -0,0 +1,12 @@
+import Models
+import SwiftGraphQL
+
+let recommendationGroupSelection = Selection.RecommendationGroup {
+ InternalRecommendationGroup(
+ id: try $0.id(),
+ name: try $0.name(),
+ inviteUrl: try $0.inviteUrl(),
+ admins: try $0.admins(selection: userProfileSelection.list),
+ members: try $0.members(selection: userProfileSelection.list)
+ )
+}
diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Selections/UserProfileSelection.swift b/apple/OmnivoreKit/Sources/Services/DataService/Selections/UserProfileSelection.swift
new file mode 100644
index 000000000..ad9562955
--- /dev/null
+++ b/apple/OmnivoreKit/Sources/Services/DataService/Selections/UserProfileSelection.swift
@@ -0,0 +1,18 @@
+import Models
+import SwiftGraphQL
+
+let profileSelection = Selection.Profile {
+ (
+ username: try $0.username(),
+ profileImageURL: try $0.pictureUrl()
+ )
+}
+
+let userProfileSelection = Selection.User {
+ InternalUserProfile(
+ userID: try $0.id(),
+ name: try $0.name(),
+ username: try $0.profile(selection: profileSelection).username,
+ profileImageURL: try $0.profile(selection: profileSelection).profileImageURL
+ )
+}
diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift
index 1950ef3c6..46827adf8 100644
--- a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift
+++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift
@@ -26,6 +26,7 @@ struct InternalLinkedItem {
let contentReader: String?
let originalHtml: String?
let language: String?
+ let recommendations: [InternalRecommendation]
var labels: [InternalLinkedItemLabel]
var isPDF: Bool {
@@ -72,6 +73,14 @@ struct InternalLinkedItem {
linkedItem.addToLabels(label.asManagedObject(inContext: context))
}
+ if let existingRecommendation = linkedItem.recommendations {
+ linkedItem.removeFromRecommendations(existingRecommendation)
+ }
+
+ for recommendation in recommendations {
+ linkedItem.addToRecommendations(recommendation.asManagedObject(inContext: context))
+ }
+
return linkedItem
}
}
@@ -133,6 +142,7 @@ extension JSONArticle {
contentReader: contentReader,
originalHtml: nil,
language: language,
+ recommendations: [], // TODO:
labels: []
)
diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalRecommendation.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalRecommendation.swift
new file mode 100644
index 000000000..80a33b0cc
--- /dev/null
+++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalRecommendation.swift
@@ -0,0 +1,39 @@
+import CoreData
+import Foundation
+import Models
+
+public struct InternalRecommendation {
+ let id: String
+ let name: String
+ let user: InternalUserProfile?
+ let recommendedAt: Date
+
+ func asManagedObject(inContext context: NSManagedObjectContext) -> Recommendation {
+ let existing = Recommendation.lookup(byID: id, inContext: context)
+ let recommendation = existing ?? Recommendation(entity: Recommendation.entity(), insertInto: context)
+ recommendation.id = id
+ recommendation.name = name
+ recommendation.recommendedAt = recommendedAt
+ recommendation.user = user?.asManagedObject(inContext: context)
+ return recommendation
+ }
+
+ public static func make(_ recommendations: NSSet?) -> [InternalRecommendation] {
+ recommendations?
+ .compactMap { recommendation in
+ if let recommendation = recommendation as? Recommendation,
+ let id = recommendation.id,
+ let name = recommendation.name,
+ let recommendedAt = recommendation.recommendedAt
+ {
+ return InternalRecommendation(
+ id: id,
+ name: name,
+ user: InternalUserProfile.makeSingle(recommendation.user),
+ recommendedAt: recommendedAt
+ )
+ }
+ return nil
+ } ?? []
+ }
+}
diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalRecommendationGroup.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalRecommendationGroup.swift
new file mode 100644
index 000000000..ff90cc253
--- /dev/null
+++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalRecommendationGroup.swift
@@ -0,0 +1,69 @@
+//
+// InternalRecommendationGroup.swift
+//
+//
+// Created by Jackson Harper on 12/5/22.
+//
+
+import CoreData
+import Foundation
+import Models
+
+public struct InternalRecommendationGroup: Identifiable {
+ public let id: String
+ public let name: String
+ public let inviteUrl: String
+ public let admins: [InternalUserProfile]
+ public let members: [InternalUserProfile]
+
+ func asManagedObject(inContext context: NSManagedObjectContext) -> RecommendationGroup {
+ let fetchRequest: NSFetchRequest = RecommendationGroup.fetchRequest()
+ fetchRequest.predicate = NSPredicate(
+ format: "id == %@", id
+ )
+ let existing = (try? context.fetch(fetchRequest))?.first
+ let recommendationGroup = existing ?? RecommendationGroup(entity: RecommendationGroup.entity(), insertInto: context)
+
+ recommendationGroup.id = id
+ recommendationGroup.name = name
+ recommendationGroup.inviteUrl = inviteUrl
+
+ return recommendationGroup
+ }
+
+ static func make(from recommendationGroup: RecommendationGroup) -> InternalRecommendationGroup? {
+ if let id = recommendationGroup.id,
+ let name = recommendationGroup.name,
+ let inviteUrl = recommendationGroup.inviteUrl
+ {
+ return InternalRecommendationGroup(
+ id: id,
+ name: name,
+ inviteUrl: inviteUrl,
+ admins: InternalUserProfile.make(recommendationGroup.admins),
+ members: InternalUserProfile.make(recommendationGroup.members)
+ )
+ }
+ return nil
+ }
+}
+
+extension Sequence where Element == InternalRecommendationGroup {
+ func persist(context: NSManagedObjectContext) -> [NSManagedObjectID]? {
+ var result: [NSManagedObjectID]?
+
+ context.performAndWait {
+ let recommendationGroups = map { $0.asManagedObject(inContext: context) }
+ do {
+ try context.save()
+ logger.debug("InternalRecommendationGroup saved succesfully")
+ result = recommendationGroups.map(\.objectID)
+ } catch {
+ context.rollback()
+ logger.debug("Failed to save InternalRecommendationGroups: \(error.localizedDescription)")
+ }
+ }
+
+ return result
+ }
+}
diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalUserProfile.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalUserProfile.swift
new file mode 100644
index 000000000..53b656890
--- /dev/null
+++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalUserProfile.swift
@@ -0,0 +1,79 @@
+import CoreData
+import Foundation
+import Models
+
+public struct InternalUserProfile: Identifiable {
+ let userID: String
+ public let name: String
+ public let username: String
+ public let profileImageURL: String?
+
+ public var id: String {
+ userID
+ }
+
+ func persist(context: NSManagedObjectContext) -> NSManagedObjectID? {
+ var objectID: NSManagedObjectID?
+
+ context.performAndWait {
+ let user = asManagedObject(inContext: context)
+
+ do {
+ try context.save()
+ logger.debug("User saved succesfully")
+ objectID = user.objectID
+ } catch {
+ context.rollback()
+ logger.debug("Failed to save User: \(error.localizedDescription)")
+ }
+ }
+
+ return objectID
+ }
+
+ func asManagedObject(inContext context: NSManagedObjectContext) -> UserProfile {
+ let existing = UserProfile.lookup(byID: userID, inContext: context)
+ let userProfile = existing ?? UserProfile(entity: UserProfile.entity(), insertInto: context)
+
+ userProfile.userID = userID
+ userProfile.name = name
+ userProfile.username = username
+ userProfile.profileImageURL = profileImageURL
+ return userProfile
+ }
+
+ public static func makeSingle(_ user: UserProfile?) -> InternalUserProfile? {
+ if let user = user,
+ let userID = user.userID,
+ let name = user.name,
+ let username = user.username
+ {
+ return InternalUserProfile(
+ userID: userID,
+ name: name,
+ username: username,
+ profileImageURL: user.profileImageURL
+ )
+ }
+ return nil
+ }
+
+ public static func make(_ users: NSSet?) -> [InternalUserProfile] {
+ users?
+ .compactMap { user in
+ if let user = user as? UserProfile,
+ let userID = user.userID,
+ let name = user.name,
+ let username = user.username
+ {
+ return InternalUserProfile(
+ userID: userID,
+ name: name,
+ username: username,
+ profileImageURL: user.profileImageURL
+ )
+ }
+ return nil
+ } ?? []
+ }
+}
diff --git a/apple/OmnivoreKit/Sources/Views/FeedItem/HomeFeedCardView.swift b/apple/OmnivoreKit/Sources/Views/FeedItem/HomeFeedCardView.swift
index 42af43b98..027bc4427 100644
--- a/apple/OmnivoreKit/Sources/Views/FeedItem/HomeFeedCardView.swift
+++ b/apple/OmnivoreKit/Sources/Views/FeedItem/HomeFeedCardView.swift
@@ -86,6 +86,27 @@ public struct FeedCard: View {
}
#endif
}
+
+ if let recommendations = item.recommendations, recommendations.count > 0 {
+ let byStr = recommendations.reduce("") { str, item in
+ if let item = item as? Recommendation, let name = item.user?.name {
+ return str + name
+ }
+ return str
+ }
+ let inStr = recommendations.reduce("") { str, item in
+ if let item = item as? Recommendation, let name = item.name {
+ return str + name
+ }
+ return str
+ }
+ HStack {
+ Text("Recommended by \(byStr) in \(inStr)")
+ .font(.appCaption)
+ .frame(alignment: .leading)
+ Spacer()
+ }
+ }
}
.padding(.top, 0)
.padding(.bottom, 8)
diff --git a/apple/OmnivoreKit/Sources/Views/ProfileCard.swift b/apple/OmnivoreKit/Sources/Views/ProfileCard.swift
index 9e923a560..dc1dc69aa 100644
--- a/apple/OmnivoreKit/Sources/Views/ProfileCard.swift
+++ b/apple/OmnivoreKit/Sources/Views/ProfileCard.swift
@@ -1,9 +1,9 @@
import SwiftUI
public struct ProfileCardData {
- let name: String
- let username: String
- let imageURL: URL?
+ public let name: String
+ public let username: String
+ public let imageURL: URL?
public init(name: String = "", username: String = "", imageURL: URL? = nil) {
self.name = name
diff --git a/packages/api/src/elastic/pages.ts b/packages/api/src/elastic/pages.ts
index 8cb48fbbf..bce0dba98 100644
--- a/packages/api/src/elastic/pages.ts
+++ b/packages/api/src/elastic/pages.ts
@@ -189,10 +189,10 @@ const appendIdsFilter = (body: SearchBody, ids: string[]): void => {
const appendRecommendedBy = (body: SearchBody, recommendedBy: string): void => {
body.query.bool.must.push({
nested: {
- path: 'recommendedBy',
+ path: 'recommendations',
query: {
term: {
- 'recommendedBy.name': recommendedBy,
+ 'recommendations.name': recommendedBy,
},
},
},
diff --git a/packages/api/src/elastic/recommendation.ts b/packages/api/src/elastic/recommendation.ts
index 511b3fae1..924f444fe 100644
--- a/packages/api/src/elastic/recommendation.ts
+++ b/packages/api/src/elastic/recommendation.ts
@@ -13,19 +13,19 @@ export const addRecommendation = async (
url: page.url,
})
if (existingPage) {
- if (existingPage.recommendedBy?.includes(recommendation)) {
+ if (existingPage.recommendations?.includes(recommendation)) {
return existingPage._id
}
// update recommendedBy in the existing page
- const recommendedBy = (existingPage.recommendedBy || []).concat(
+ const recommendations = (existingPage.recommendations || []).concat(
recommendation
)
await updatePage(
existingPage.id,
{
- recommendedBy,
+ recommendations,
},
ctx
)
@@ -36,7 +36,7 @@ export const addRecommendation = async (
const newPage: Page = {
...page,
id: '',
- recommendedBy: [recommendation],
+ recommendations: [recommendation],
userId: ctx.uid,
readingProgressPercent: 0,
readingProgressAnchorIndex: 0,
diff --git a/packages/api/src/elastic/types.ts b/packages/api/src/elastic/types.ts
index de2a0dd58..9b7122e57 100644
--- a/packages/api/src/elastic/types.ts
+++ b/packages/api/src/elastic/types.ts
@@ -53,10 +53,10 @@ export interface SearchBody {
}
| {
nested: {
- path: 'recommendedBy'
+ path: 'recommendations'
query: {
term: {
- 'recommendedBy.name': string
+ 'recommendations.name': string
}
}
}
@@ -198,9 +198,18 @@ export interface Highlight {
highlightPositionAnchorIndex?: number | null
}
+export interface RecommendingUser {
+ userId: string
+ name: string
+ username: string
+ profileImageURL: string | null
+}
+
export interface Recommendation {
id: string
name: string
+ note: string | null
+ user: RecommendingUser
recommendedAt: Date
}
@@ -240,7 +249,7 @@ export interface Page {
readAt?: Date
listenedAt?: Date
wordsCount?: number
- recommendedBy?: Recommendation[]
+ recommendations?: Recommendation[]
}
export interface SearchItem {
@@ -273,7 +282,7 @@ export interface SearchItem {
wordsCount?: number
siteName?: string
siteIcon?: string
- recommendedBy?: Recommendation[]
+ recommendations?: Recommendation[]
}
const keys = ['_id', 'url', 'slug', 'userId', 'uploadFileId', 'state'] as const
diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts
index d254c1547..8728bb795 100644
--- a/packages/api/src/generated/graphql.ts
+++ b/packages/api/src/generated/graphql.ts
@@ -110,7 +110,7 @@ export type Article = {
readAt?: Maybe;
readingProgressAnchorIndex: Scalars['Int'];
readingProgressPercent: Scalars['Float'];
- recommendedBy?: Maybe>;
+ recommendations?: Maybe>;
savedAt: Scalars['Date'];
savedByViewer?: Maybe;
shareInfo?: Maybe;
@@ -1787,6 +1787,7 @@ export enum RecommendErrorCode {
export type RecommendInput = {
groupIds: Array;
+ note?: InputMaybe;
pageId: Scalars['ID'];
};
@@ -1801,7 +1802,9 @@ export type Recommendation = {
__typename?: 'Recommendation';
id: Scalars['ID'];
name: Scalars['String'];
+ note?: Maybe;
recommendedAt: Scalars['Date'];
+ user?: Maybe;
};
export type RecommendationGroup = {
@@ -1815,6 +1818,14 @@ export type RecommendationGroup = {
updatedAt: Scalars['Date'];
};
+export type RecommendingUser = {
+ __typename?: 'RecommendingUser';
+ name: Scalars['String'];
+ profileImageURL?: Maybe;
+ userId: Scalars['String'];
+ username: Scalars['String'];
+};
+
export type Reminder = {
__typename?: 'Reminder';
archiveUntil: Scalars['Boolean'];
@@ -2046,7 +2057,7 @@ export type SearchItem = {
readAt?: Maybe;
readingProgressAnchorIndex: Scalars['Int'];
readingProgressPercent: Scalars['Float'];
- recommendedBy?: Maybe>;
+ recommendations?: Maybe>;
savedAt: Scalars['Date'];
shortId?: Maybe;
siteIcon?: Maybe;
@@ -3230,6 +3241,7 @@ export type ResolversTypes = {
RecommendSuccess: ResolverTypeWrapper;
Recommendation: ResolverTypeWrapper;
RecommendationGroup: ResolverTypeWrapper;
+ RecommendingUser: ResolverTypeWrapper;
Reminder: ResolverTypeWrapper;
ReminderError: ResolverTypeWrapper;
ReminderErrorCode: ReminderErrorCode;
@@ -3631,6 +3643,7 @@ export type ResolversParentTypes = {
RecommendSuccess: RecommendSuccess;
Recommendation: Recommendation;
RecommendationGroup: RecommendationGroup;
+ RecommendingUser: RecommendingUser;
Reminder: Reminder;
ReminderError: ReminderError;
ReminderResult: ResolversParentTypes['ReminderError'] | ResolversParentTypes['ReminderSuccess'];
@@ -3876,7 +3889,7 @@ export type ArticleResolvers, ParentType, ContextType>;
readingProgressAnchorIndex?: Resolver;
readingProgressPercent?: Resolver;
- recommendedBy?: Resolver>, ParentType, ContextType>;
+ recommendations?: Resolver>, ParentType, ContextType>;
savedAt?: Resolver;
savedByViewer?: Resolver, ParentType, ContextType>;
shareInfo?: Resolver, ParentType, ContextType>;
@@ -4836,7 +4849,9 @@ export type RecommendSuccessResolvers = {
id?: Resolver;
name?: Resolver;
+ note?: Resolver, ParentType, ContextType>;
recommendedAt?: Resolver;
+ user?: Resolver, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn;
};
@@ -4851,6 +4866,14 @@ export type RecommendationGroupResolvers;
};
+export type RecommendingUserResolvers = {
+ name?: Resolver;
+ profileImageURL?: Resolver, ParentType, ContextType>;
+ userId?: Resolver;
+ username?: Resolver;
+ __isTypeOf?: IsTypeOfResolverFn;
+};
+
export type ReminderResolvers = {
archiveUntil?: Resolver;
id?: Resolver;
@@ -4993,7 +5016,7 @@ export type SearchItemResolvers, ParentType, ContextType>;
readingProgressAnchorIndex?: Resolver;
readingProgressPercent?: Resolver;
- recommendedBy?: Resolver>, ParentType, ContextType>;
+ recommendations?: Resolver>, ParentType, ContextType>;
savedAt?: Resolver;
shortId?: Resolver, ParentType, ContextType>;
siteIcon?: Resolver, ParentType, ContextType>;
@@ -5718,6 +5741,7 @@ export type Resolvers = {
RecommendSuccess?: RecommendSuccessResolvers;
Recommendation?: RecommendationResolvers;
RecommendationGroup?: RecommendationGroupResolvers;
+ RecommendingUser?: RecommendingUserResolvers;
Reminder?: ReminderResolvers;
ReminderError?: ReminderErrorResolvers;
ReminderResult?: ReminderResultResolvers;
diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql
index 00e4a1c98..89f6cbf6b 100644
--- a/packages/api/src/generated/schema.graphql
+++ b/packages/api/src/generated/schema.graphql
@@ -86,7 +86,7 @@ type Article {
readAt: Date
readingProgressAnchorIndex: Int!
readingProgressPercent: Float!
- recommendedBy: [Recommendation!]
+ recommendations: [Recommendation!]
savedAt: Date!
savedByViewer: Boolean
shareInfo: LinkShareInfo
@@ -1250,6 +1250,7 @@ enum RecommendErrorCode {
input RecommendInput {
groupIds: [ID!]!
+ note: String
pageId: ID!
}
@@ -1262,7 +1263,9 @@ type RecommendSuccess {
type Recommendation {
id: ID!
name: String!
+ note: String
recommendedAt: Date!
+ user: RecommendingUser
}
type RecommendationGroup {
@@ -1275,6 +1278,13 @@ type RecommendationGroup {
updatedAt: Date!
}
+type RecommendingUser {
+ name: String!
+ profileImageURL: String
+ userId: String!
+ username: String!
+}
+
type Reminder {
archiveUntil: Boolean!
id: ID!
@@ -1488,7 +1498,7 @@ type SearchItem {
readAt: Date
readingProgressAnchorIndex: Int!
readingProgressPercent: Float!
- recommendedBy: [Recommendation!]
+ recommendations: [Recommendation!]
savedAt: Date!
shortId: String
siteIcon: String
diff --git a/packages/api/src/resolvers/article_saving_request/index.ts b/packages/api/src/resolvers/article_saving_request/index.ts
index 1b8f2764e..6a44d2b1a 100644
--- a/packages/api/src/resolvers/article_saving_request/index.ts
+++ b/packages/api/src/resolvers/article_saving_request/index.ts
@@ -42,7 +42,7 @@ export const createArticleSavingRequestResolver = authorized<
articleSavingRequest: request,
}
} catch (err) {
- console.log('error', err)
+ console.log('error saving article', err)
if (isErrorWithCode(err)) {
return {
errorCodes: [err.errorCode as CreateArticleSavingRequestErrorCode],
diff --git a/packages/api/src/resolvers/labels/index.ts b/packages/api/src/resolvers/labels/index.ts
index 31daf9cb9..78cb75703 100644
--- a/packages/api/src/resolvers/labels/index.ts
+++ b/packages/api/src/resolvers/labels/index.ts
@@ -201,7 +201,7 @@ export const deleteLabelResolver = authorized<
label,
}
} catch (error) {
- log.error('error', error)
+ log.error('error deleting label', error)
return {
errorCodes: [DeleteLabelErrorCode.BadRequest],
}
diff --git a/packages/api/src/resolvers/recommendations/index.ts b/packages/api/src/resolvers/recommendations/index.ts
index b9083844c..d3e90df89 100644
--- a/packages/api/src/resolvers/recommendations/index.ts
+++ b/packages/api/src/resolvers/recommendations/index.ts
@@ -146,8 +146,9 @@ export const recommendResolver = authorized<
})
try {
- const user = await getRepository(User).findOneBy({
- id: uid,
+ const user = await getRepository(User).findOne({
+ where: { id: uid },
+ relations: ['profile'],
})
if (!user) {
return {
@@ -184,7 +185,15 @@ export const recommendResolver = authorized<
member.user.id,
page.id,
{
- ...group,
+ id: group.id,
+ name: group.name,
+ note: input.note ?? null,
+ user: {
+ userId: user.id,
+ name: user.name,
+ username: user.profile.username,
+ profileImageURL: user.profile.pictureUrl,
+ },
recommendedAt: new Date(),
},
auth
@@ -198,6 +207,7 @@ export const recommendResolver = authorized<
taskNames,
}
} catch (error) {
+ console.log('Error recommending: ', error)
log.error('Error recommending', {
error,
labels: {
diff --git a/packages/api/src/resolvers/user/index.ts b/packages/api/src/resolvers/user/index.ts
index d9a6f3828..4450ad910 100644
--- a/packages/api/src/resolvers/user/index.ts
+++ b/packages/api/src/resolvers/user/index.ts
@@ -211,7 +211,7 @@ export const googleSignupResolver: ResolverFn<
me: userDataToUser({ ...user, profile: { ...profile, private: false } }),
}
} catch (err) {
- console.log('error', err)
+ console.log('error signing up with google', err)
if (isErrorWithCode(err)) {
return { errorCodes: [err.errorCode as SignupErrorCode] }
}
diff --git a/packages/api/src/routers/auth/mobile/sign_up.ts b/packages/api/src/routers/auth/mobile/sign_up.ts
index 9bfc36c78..55d9e6dea 100644
--- a/packages/api/src/routers/auth/mobile/sign_up.ts
+++ b/packages/api/src/routers/auth/mobile/sign_up.ts
@@ -72,7 +72,7 @@ export async function createMobileEmailSignUpResponse(
json: {},
}
} catch (e) {
- console.log('error', e)
+ console.log('error creating mobile email sign up response', e)
return signUpFailedPayload
}
}
diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts
index fc1267142..de1a182e3 100755
--- a/packages/api/src/schema.ts
+++ b/packages/api/src/schema.ts
@@ -329,10 +329,19 @@ const schema = gql`
updatedAt: Date!
}
+ type RecommendingUser {
+ userId: String!
+ name: String!
+ username: String!
+ profileImageURL: String
+ }
+
type Recommendation {
id: ID!
name: String!
+ user: RecommendingUser
recommendedAt: Date!
+ note: String
}
type Article {
@@ -375,7 +384,7 @@ const schema = gql`
state: ArticleSavingRequestStatus
language: String
readAt: Date
- recommendedBy: [Recommendation!]
+ recommendations: [Recommendation!]
}
# Query: article
@@ -1522,7 +1531,7 @@ const schema = gql`
savedAt: Date!
highlights: [Highlight!]
siteIcon: String
- recommendedBy: [Recommendation!]
+ recommendations: [Recommendation!]
}
type SearchItemEdge {
@@ -2194,6 +2203,7 @@ const schema = gql`
input RecommendInput {
pageId: ID!
groupIds: [ID!]!
+ note: String
}
union RecommendResult = RecommendSuccess | RecommendError
diff --git a/packages/api/src/utils/createTask.ts b/packages/api/src/utils/createTask.ts
index 66ade68e5..0d1256e56 100644
--- a/packages/api/src/utils/createTask.ts
+++ b/packages/api/src/utils/createTask.ts
@@ -423,7 +423,7 @@ export const enqueueRecommendation = async (
}
const headers = {
- Authorization: authToken,
+ Cookie: `auth=${authToken}`,
}
// If there is no Google Cloud Project Id exposed, it means that we are in local environment
if (env.dev.isLocal || !GOOGLE_CLOUD_PROJECT) {
diff --git a/packages/db/elastic_migrations/index_settings.json b/packages/db/elastic_migrations/index_settings.json
index 5d4018d4b..cebf4c58b 100644
--- a/packages/db/elastic_migrations/index_settings.json
+++ b/packages/db/elastic_migrations/index_settings.json
@@ -7,13 +7,19 @@
"analyzer": {
"strip_html_analyzer": {
"tokenizer": "standard",
- "char_filter": ["html_strip"],
- "filter": ["lowercase"]
+ "char_filter": [
+ "html_strip"
+ ],
+ "filter": [
+ "lowercase"
+ ]
}
},
"normalizer": {
"lowercase_normalizer": {
- "filter": ["lowercase"]
+ "filter": [
+ "lowercase"
+ ]
}
}
}
@@ -161,7 +167,7 @@
"wordsCount": {
"type": "integer"
},
- "recommendedBy": {
+ "recommendations": {
"type": "nested",
"properties": {
"id": {
@@ -173,9 +179,31 @@
},
"recommendedAt": {
"type": "date"
+ },
+ "note": {
+ "type": "text"
+ },
+ "user": {
+ "type": "nested",
+ "properties": {
+ "userID": {
+ "type": "keyword"
+ },
+ "name": {
+ "type": "keyword",
+ "normalizer": "lowercase_normalizer"
+ },
+ "username": {
+ "type": "keyword",
+ "normalizer": "lowercase_normalizer"
+ },
+ "profileImageURL": {
+ "type": "keyword"
+ }
+ }
}
}
}
}
}
-}
+}
\ No newline at end of file
diff --git a/packages/web/lib/networking/mutations/joinGroupMutation.ts b/packages/web/lib/networking/mutations/joinGroupMutation.ts
new file mode 100644
index 000000000..86c7b9000
--- /dev/null
+++ b/packages/web/lib/networking/mutations/joinGroupMutation.ts
@@ -0,0 +1,51 @@
+import { gql } from 'graphql-request'
+import { gqlFetcher } from '../networkHelpers'
+
+export interface RecommendationGroup {
+ id: string
+ name: string
+ createdAt: Date
+ updatedAt: Date
+}
+
+type JoinGroupResponse = {
+ joinGroup?: JoinGroupData
+ errorCodes?: string[]
+}
+
+type JoinGroupData = {
+ group: RecommendationGroup | undefined
+}
+
+export async function joinGroupMutation(
+ inviteCode: string
+): Promise {
+ const mutation = gql`
+ mutation JoinGroup($inviteCode: String!) {
+ joinGroup(inviteCode: $inviteCode) {
+ ... on JoinGroupSuccess {
+ group {
+ id
+ name
+ createdAt
+ updatedAt
+ }
+ }
+ ... on JoinGroupError {
+ errorCodes
+ }
+ }
+ }
+ `
+
+ console.log('JoinGroupMutation', mutation)
+
+ const response = await gqlFetcher(mutation, { inviteCode })
+ console.log(' -- response', response)
+ const data = response as JoinGroupResponse | undefined
+ const error = data?.errorCodes?.find(() => true)
+ if (error) {
+ throw error
+ }
+ return data?.joinGroup?.group
+}
diff --git a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx
index 001842abe..be7893b41 100644
--- a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx
+++ b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx
@@ -80,8 +80,24 @@ export type LibraryItemNode = {
state: State
pageType: PageType
siteName?: string
- subscription?: string,
+ subscription?: string
readAt?: string
+ recommendations?: Recommendation[]
+}
+
+export type Recommendation = {
+ id: string
+ name: string
+ note?: string
+ user?: RecommendingUser
+ recommendedAt: Date
+}
+
+export type RecommendingUser = {
+ userId: string
+ name: string
+ username: string
+ profileImageURL?: string
}
export type PageInfo = {
@@ -135,6 +151,18 @@ export function useGetLibraryItemsQuery({
siteName
subscription
readAt
+ recommendations {
+ id
+ name
+ note
+ user {
+ userId
+ name
+ username
+ profileImageURL
+ }
+ recommendedAt
+ }
}
}
pageInfo {
@@ -198,7 +226,7 @@ export function useGetLibraryItemsQuery({
}
const getIndexOf = (page: LibraryItems, item: LibraryItem) => {
- return page.edges.findIndex(i => i.node.id === item.node.id)
+ return page.edges.findIndex((i) => i.node.id === item.node.id)
}
const performActionOnItem = async (
@@ -329,7 +357,9 @@ export function useGetLibraryItemsQuery({
})
unsubscribeMutation(item.node.subscription).then((res) => {
if (res) {
- showSuccessToast('Unsubscribed successfully', { position: 'bottom-right' })
+ showSuccessToast('Unsubscribed successfully', {
+ position: 'bottom-right',
+ })
} else {
showErrorToast('Error unsubscribing', {
position: 'bottom-right',
@@ -337,8 +367,8 @@ export function useGetLibraryItemsQuery({
}
})
}
- case 'update-item':
- updateData(item)
+ case 'update-item':
+ updateData(item)
break
case 'refresh':
await mutate()
@@ -353,6 +383,6 @@ export function useGetLibraryItemsQuery({
performActionOnItem,
size,
setSize,
- mutate
+ mutate,
}
}
diff --git a/packages/web/pages/home.tsx b/packages/web/pages/home.tsx
index 1e1201f72..f222eb42f 100644
--- a/packages/web/pages/home.tsx
+++ b/packages/web/pages/home.tsx
@@ -1,7 +1,6 @@
import { PrimaryLayout } from '../components/templates/PrimaryLayout'
import { HomeFeedContainer } from '../components/templates/homeFeed/HomeFeedContainer'
import { VStack } from './../components/elements/LayoutPrimitives'
-import { useRef } from 'react'
export default function Home(): JSX.Element {
return
diff --git a/packages/web/pages/invite/[inviteCode].tsx b/packages/web/pages/invite/[inviteCode].tsx
new file mode 100644
index 000000000..6e68a1684
--- /dev/null
+++ b/packages/web/pages/invite/[inviteCode].tsx
@@ -0,0 +1,45 @@
+import { useRouter } from 'next/router'
+import { useEffect, useState } from 'react'
+import { PageMetaData } from '../../components/patterns/PageMetaData'
+import { ProfileLayout } from '../../components/templates/ProfileLayout'
+import { joinGroupMutation } from '../../lib/networking/mutations/joinGroupMutation'
+import { showSuccessToast } from '../../lib/toastHelpers'
+
+export default function InvitePage(): JSX.Element {
+ const router = useRouter()
+ const { inviteCode } = router.query
+
+ const [error, setError] = useState(undefined)
+
+ useEffect(() => {
+ if (!router.isReady) {
+ return
+ }
+ if (!inviteCode || typeof inviteCode != 'string') {
+ setError('No invite code provided')
+ }
+
+ const joinGroup = async () => {
+ try {
+ const result = await joinGroupMutation(inviteCode as string)
+ if (!result) {
+ throw 'Unknown error occurred.'
+ }
+ showSuccessToast(`You have been added to the ${result.name} group`)
+ } catch (error) {
+ setError('Unable to join group')
+ }
+ }
+
+ joinGroup().catch((error) => {
+ setError(error)
+ })
+ }, [router, inviteCode])
+ return (
+ <>
+
+ Accepting invite to join
+
+ >
+ )
+}