diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/RecommendToView.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/RecommendToView.swift index acb9ad83b..b0c1983f1 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/RecommendToView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/RecommendToView.swift @@ -12,11 +12,14 @@ import Views @Published var showError = false @Published var showNoteView = false @Published var note: String = "" + @Published var withHighlights: Bool = true let pageID: String + let highlightCount: Int - init(pageID: String) { + init(pageID: String, highlightCount: Int) { self.pageID = pageID + self.highlightCount = highlightCount } func loadGroups(dataService: DataService) async { @@ -36,7 +39,10 @@ import Views isRunning = true do { - try await dataService.recommendPage(pageID: pageID, groupIDs: selectedGroups.map(\.id), note: note.isEmpty ? nil : note) + try await dataService.recommendPage(pageID: pageID, + groupIDs: selectedGroups.map(\.id), + note: note.isEmpty ? nil : note, + withHighlights: withHighlights) } catch { showError = true } @@ -111,6 +117,13 @@ struct RecommendToView: View { .padding(.top, 24) .padding(.leading, 16) ) + if viewModel.highlightCount > 0 { + Toggle(isOn: $viewModel.withHighlights, label: { + HStack(alignment: .firstTextBaseline) { + Text("Include \(viewModel.highlightCount) highlight\(viewModel.highlightCount > 1 ? "s" : "")") + } + }) + } Spacer() } .padding(16) diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index 408da9ef7..c03590590 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -362,7 +362,7 @@ struct WebReaderContainerView: View { NavigationView { RecommendToView( dataService: dataService, - viewModel: RecommendToViewModel(pageID: item.unwrappedID) + viewModel: RecommendToViewModel(pageID: item.unwrappedID, highlightCount: item.highlights?.count ?? 0) ) }.onDisappear { showRecommendSheet = false diff --git a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift index 22963144d..5798dde58 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift @@ -9677,6 +9677,7 @@ extension Objects { let moveLabel: [String: Unions.MoveLabelResult] let optInFeature: [String: Unions.OptInFeatureResult] let recommend: [String: Unions.RecommendResult] + let recommendHighlights: [String: Unions.RecommendHighlightsResult] let reportItem: [String: Objects.ReportItemResult] let revokeApiKey: [String: Unions.RevokeApiKeyResult] let saveArticleReadingProgress: [String: Unions.SaveArticleReadingProgressResult] @@ -9851,6 +9852,10 @@ extension Objects.Mutation: Decodable { if let value = try container.decode(Unions.RecommendResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } + case "recommendHighlights": + if let value = try container.decode(Unions.RecommendHighlightsResult?.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) @@ -10016,6 +10021,7 @@ extension Objects.Mutation: Decodable { moveLabel = map["moveLabel"] optInFeature = map["optInFeature"] recommend = map["recommend"] + recommendHighlights = map["recommendHighlights"] reportItem = map["reportItem"] revokeApiKey = map["revokeApiKey"] saveArticleReadingProgress = map["saveArticleReadingProgress"] @@ -10640,6 +10646,25 @@ extension Fields where TypeLock == Objects.Mutation { } } + func recommendHighlights(input: InputObjects.RecommendHighlightsInput, selection: Selection) throws -> Type { + let field = GraphQLField.composite( + name: "recommendHighlights", + arguments: [Argument(name: "input", type: "RecommendHighlightsInput!", value: input)], + selection: selection.selection + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.recommendHighlights[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", @@ -13663,10 +13688,140 @@ extension Selection where TypeLock == Never, Type == Never { typealias RecommendError = Selection } +extension Objects { + struct RecommendHighlightsError { + let __typename: TypeName = .recommendHighlightsError + let errorCodes: [String: [Enums.RecommendHighlightsErrorCode]] + + enum TypeName: String, Codable { + case recommendHighlightsError = "RecommendHighlightsError" + } + } +} + +extension Objects.RecommendHighlightsError: 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.RecommendHighlightsErrorCode]?.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.RecommendHighlightsError { + func errorCodes() throws -> [Enums.RecommendHighlightsErrorCode] { + 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 RecommendHighlightsError = Selection +} + +extension Objects { + struct RecommendHighlightsSuccess { + let __typename: TypeName = .recommendHighlightsSuccess + let success: [String: Bool] + + enum TypeName: String, Codable { + case recommendHighlightsSuccess = "RecommendHighlightsSuccess" + } + } +} + +extension Objects.RecommendHighlightsSuccess: 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 "success": + if let value = try container.decode(Bool?.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)." + ) + ) + } + } + + success = map["success"] + } +} + +extension Fields where TypeLock == Objects.RecommendHighlightsSuccess { + func success() throws -> Bool { + let field = GraphQLField.leaf( + name: "success", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.success[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return Bool.mockValue + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias RecommendHighlightsSuccess = Selection +} + extension Objects { struct RecommendSuccess { let __typename: TypeName = .recommendSuccess - let taskNames: [String: [String]] + let success: [String: Bool] enum TypeName: String, Codable { case recommendSuccess = "RecommendSuccess" @@ -13686,8 +13841,8 @@ extension Objects.RecommendSuccess: Decodable { let field = GraphQLField.getFieldNameFromAlias(alias) switch field { - case "taskNames": - if let value = try container.decode([String]?.self, forKey: codingKey) { + case "success": + if let value = try container.decode(Bool?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } default: @@ -13700,26 +13855,26 @@ extension Objects.RecommendSuccess: Decodable { } } - taskNames = map["taskNames"] + success = map["success"] } } extension Fields where TypeLock == Objects.RecommendSuccess { - func taskNames() throws -> [String] { + func success() throws -> Bool { let field = GraphQLField.leaf( - name: "taskNames", + name: "success", arguments: [] ) select(field) switch response { case let .decoding(data): - if let data = data.taskNames[field.alias!] { + if let data = data.success[field.alias!] { return data } throw HttpError.badpayload case .mocking: - return [] + return Bool.mockValue } } } @@ -25762,11 +25917,85 @@ extension Selection where TypeLock == Never, Type == Never { typealias RecentSearchesResult = Selection } +extension Unions { + struct RecommendHighlightsResult { + let __typename: TypeName + let errorCodes: [String: [Enums.RecommendHighlightsErrorCode]] + let success: [String: Bool] + + enum TypeName: String, Codable { + case recommendHighlightsError = "RecommendHighlightsError" + case recommendHighlightsSuccess = "RecommendHighlightsSuccess" + } + } +} + +extension Unions.RecommendHighlightsResult: 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.RecommendHighlightsErrorCode]?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "success": + if let value = try container.decode(Bool?.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"] + success = map["success"] + } +} + +extension Fields where TypeLock == Unions.RecommendHighlightsResult { + func on(recommendHighlightsError: Selection, recommendHighlightsSuccess: Selection) throws -> Type { + select([GraphQLField.fragment(type: "RecommendHighlightsError", selection: recommendHighlightsError.selection), GraphQLField.fragment(type: "RecommendHighlightsSuccess", selection: recommendHighlightsSuccess.selection)]) + + switch response { + case let .decoding(data): + switch data.__typename { + case .recommendHighlightsError: + let data = Objects.RecommendHighlightsError(errorCodes: data.errorCodes) + return try recommendHighlightsError.decode(data: data) + case .recommendHighlightsSuccess: + let data = Objects.RecommendHighlightsSuccess(success: data.success) + return try recommendHighlightsSuccess.decode(data: data) + } + case .mocking: + return recommendHighlightsError.mock() + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias RecommendHighlightsResult = Selection +} + extension Unions { struct RecommendResult { let __typename: TypeName let errorCodes: [String: [Enums.RecommendErrorCode]] - let taskNames: [String: [String]] + let success: [String: Bool] enum TypeName: String, Codable { case recommendError = "RecommendError" @@ -25791,8 +26020,8 @@ extension Unions.RecommendResult: Decodable { 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) { + case "success": + if let value = try container.decode(Bool?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } default: @@ -25808,7 +26037,7 @@ extension Unions.RecommendResult: Decodable { __typename = try container.decode(TypeName.self, forKey: DynamicCodingKeys(stringValue: "__typename")!) errorCodes = map["errorCodes"] - taskNames = map["taskNames"] + success = map["success"] } } @@ -25823,7 +26052,7 @@ extension Fields where TypeLock == Unions.RecommendResult { let data = Objects.RecommendError(errorCodes: data.errorCodes) return try recommendError.decode(data: data) case .recommendSuccess: - let data = Objects.RecommendSuccess(taskNames: data.taskNames) + let data = Objects.RecommendSuccess(success: data.success) return try recommendSuccess.decode(data: data) } case .mocking: @@ -29262,6 +29491,17 @@ extension Enums { } } +extension Enums { + /// RecommendHighlightsErrorCode + enum RecommendHighlightsErrorCode: String, CaseIterable, Codable { + case badRequest = "BAD_REQUEST" + + case notFound = "NOT_FOUND" + + case unauthorized = "UNAUTHORIZED" + } +} + extension Enums { /// ReminderErrorCode enum ReminderErrorCode: String, CaseIterable, Codable { @@ -30302,9 +30542,11 @@ extension InputObjects { } extension InputObjects { - struct RecommendInput: Encodable, Hashable { + struct RecommendHighlightsInput: Encodable, Hashable { var groupIds: [String] + var highlightIds: [String] + var note: OptionalArgument = .absent() var pageId: String @@ -30312,18 +30554,47 @@ extension InputObjects { func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(groupIds, forKey: .groupIds) + try container.encode(highlightIds, forKey: .highlightIds) if note.hasValue { try container.encode(note, forKey: .note) } try container.encode(pageId, forKey: .pageId) } enum CodingKeys: String, CodingKey { case groupIds + case highlightIds case note case pageId } } } +extension InputObjects { + struct RecommendInput: Encodable, Hashable { + var groupIds: [String] + + var note: OptionalArgument = .absent() + + var pageId: String + + var recommendedWithHighlights: OptionalArgument = .absent() + + 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) + if recommendedWithHighlights.hasValue { try container.encode(recommendedWithHighlights, forKey: .recommendedWithHighlights) } + } + + enum CodingKeys: String, CodingKey { + case groupIds + case note + case pageId + case recommendedWithHighlights + } + } +} + extension InputObjects { struct ReportItemInput: Encodable, Hashable { var itemUrl: String diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RecommendPage.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RecommendPage.swift index 68f604817..dab5dfbeb 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RecommendPage.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RecommendPage.swift @@ -4,9 +4,9 @@ import Models import SwiftGraphQL public extension DataService { - func recommendPage(pageID: String, groupIDs: [String], note: String?) async throws { + func recommendPage(pageID: String, groupIDs: [String], note: String?, withHighlights: Bool?) async throws { enum MutationResult { - case saved(taskNames: [String]) + case saved(success: Bool) case error(errorMessage: String) } @@ -14,14 +14,14 @@ public extension DataService { try $0.on( recommendError: .init { .error(errorMessage: try $0.errorCodes().first.toString()) }, recommendSuccess: .init { - .saved(taskNames: try $0.taskNames()) + .saved(success: try $0.success()) } ) } let mutation = Selection.Mutation { try $0.recommend( - input: .init(groupIds: groupIDs, note: OptionalArgument(note), pageId: pageID), + input: .init(groupIds: groupIDs, note: OptionalArgument(note), pageId: pageID, recommendedWithHighlights: OptionalArgument(withHighlights)), selection: selection ) }