From ddcbb7835fda168a9ed01ddd2576b7cf03d8bb87 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 19 Dec 2023 21:08:03 +0800 Subject: [PATCH] Fetch rules --- .../App/Views/Profile/SubscriptionsView.swift | 50 +++- .../Services/DataService/GQLSchema.swift | 240 ++++++++++++++++++ .../Mutations/SetRuleMutation.swift | 14 +- .../DataService/Queries/RulesQuery.swift | 46 ++++ 4 files changed, 334 insertions(+), 16 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/Services/DataService/Queries/RulesQuery.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/SubscriptionsView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/SubscriptionsView.swift index 5a464b875..7d39df58f 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/SubscriptionsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/SubscriptionsView.swift @@ -23,6 +23,7 @@ typealias OperationStatusHandler = (_: OperationStatus) -> Void @Published var isLoading = true @Published var feeds = [Subscription]() @Published var newsletters = [Subscription]() + @Published var rules = [Rule]() @Published var hasNetworkError = false @Published var subscriptionNameToCancel: String? @Published var presentingSubscription: Subscription? @@ -42,6 +43,9 @@ typealias OperationStatusHandler = (_: OperationStatus) -> Void hasNetworkError = true } + // Also try to get the rules for auto labeling + rules = (try? await dataService.rules()) ?? [] + isLoading = false } @@ -86,6 +90,21 @@ typealias OperationStatusHandler = (_: OperationStatus) -> Void operationStatus = .failure } } + + func setLabelsRule(dataService: DataService, ruleName: String, filter: String, labelIDs: [String]) async { + async { + operationMessage = "Creating label rule..." + operationStatus = .isPerforming + do { + try await dataService.createAddLabelsRule(name: ruleName, filter: filter, labelIDs: labelIDs) + operationMessage = "Rule created" + operationStatus = .success + } catch { + operationMessage = "Failed to create label rule" + operationStatus = .failure + } + } + } } struct OperationToast: View { @@ -362,6 +381,20 @@ struct SubscriptionSettingsView: View { @Environment(\.dismiss) private var dismiss + var ruleName: String { + if let url = subscription.url, subscription.type == .newsletter { + return "system.autoLabel.(\(url))" + } + return "system.autoLabel.(\(subscription.name))" + } + + var ruleFilter: String { + if let url = subscription.url, subscription.type == .newsletter { + return "rss:\"\(url)\"" + } + return "subscription:\"\(subscription.name)\"" + } + var folderRow: some View { HStack { Picker("Destination Folder", selection: $folderSelection) { @@ -395,6 +428,7 @@ struct SubscriptionSettingsView: View { Text("Add Labels") Spacer() Button(action: { showLabelsSelector = true }, label: { + let rule = viewModel.rules.first { $0.name == ruleName } Text("[none]") }) } @@ -451,10 +485,18 @@ struct SubscriptionSettingsView: View { } .sheet(isPresented: $showLabelsSelector) { ApplyLabelsView(mode: .list([]), onSave: { labels in - print("APPLIED LABELSL: ", labels) -// showLabelsModal = false -// item.labels = NSSet(array: labels) -// readerSettingsChangedTransactionID = UUID() + Task { + viewModel.showOperationToast = true + await viewModel.setLabelsRule( + dataService: dataService, + ruleName: ruleName, + filter: ruleFilter, + labelIDs: labels.map(\.unwrappedID) + ) + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1500)) { + viewModel.showOperationToast = false + } + } }) } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift index 71ce9f54b..ba3005e6b 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift @@ -6652,6 +6652,136 @@ extension Selection where TypeLock == Never, Type == Never { typealias FeedsSuccess = Selection } +extension Objects { + struct FetchContentError { + let __typename: TypeName = .fetchContentError + let errorCodes: [String: [Enums.FetchContentErrorCode]] + + enum TypeName: String, Codable { + case fetchContentError = "FetchContentError" + } + } +} + +extension Objects.FetchContentError: 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.FetchContentErrorCode]?.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.FetchContentError { + func errorCodes() throws -> [Enums.FetchContentErrorCode] { + 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 FetchContentError = Selection +} + +extension Objects { + struct FetchContentSuccess { + let __typename: TypeName = .fetchContentSuccess + let success: [String: Bool] + + enum TypeName: String, Codable { + case fetchContentSuccess = "FetchContentSuccess" + } + } +} + +extension Objects.FetchContentSuccess: 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.FetchContentSuccess { + 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 FetchContentSuccess = Selection +} + extension Objects { struct Filter { let __typename: TypeName = .filter @@ -11112,6 +11242,7 @@ extension Objects { let deleteNewsletterEmail: [String: Unions.DeleteNewsletterEmailResult] let deleteRule: [String: Unions.DeleteRuleResult] let deleteWebhook: [String: Unions.DeleteWebhookResult] + let fetchContent: [String: Unions.FetchContentResult] let generateApiKey: [String: Unions.GenerateApiKeyResult] let googleLogin: [String: Unions.LoginResult] let googleSignup: [String: Unions.GoogleSignupResult] @@ -11239,6 +11370,10 @@ extension Objects.Mutation: Decodable { if let value = try container.decode(Unions.DeleteWebhookResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } + case "fetchContent": + if let value = try container.decode(Unions.FetchContentResult?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } case "generateApiKey": if let value = try container.decode(Unions.GenerateApiKeyResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -11441,6 +11576,7 @@ extension Objects.Mutation: Decodable { deleteNewsletterEmail = map["deleteNewsletterEmail"] deleteRule = map["deleteRule"] deleteWebhook = map["deleteWebhook"] + fetchContent = map["fetchContent"] generateApiKey = map["generateApiKey"] googleLogin = map["googleLogin"] googleSignup = map["googleSignup"] @@ -11793,6 +11929,25 @@ extension Fields where TypeLock == Objects.Mutation { } } + func fetchContent(id: String, selection: Selection) throws -> Type { + let field = GraphQLField.composite( + name: "fetchContent", + arguments: [Argument(name: "id", type: "ID!", value: id)], + selection: selection.selection + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.fetchContent[field.alias!] { + return try selection.decode(data: data) + } + throw HttpError.badpayload + case .mocking: + return selection.mock() + } + } + func generateApiKey(input: InputObjects.GenerateApiKeyInput, selection: Selection) throws -> Type { let field = GraphQLField.composite( name: "generateApiKey", @@ -27864,6 +28019,80 @@ extension Selection where TypeLock == Never, Type == Never { typealias FeedsResult = Selection } +extension Unions { + struct FetchContentResult { + let __typename: TypeName + let errorCodes: [String: [Enums.FetchContentErrorCode]] + let success: [String: Bool] + + enum TypeName: String, Codable { + case fetchContentError = "FetchContentError" + case fetchContentSuccess = "FetchContentSuccess" + } + } +} + +extension Unions.FetchContentResult: 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.FetchContentErrorCode]?.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.FetchContentResult { + func on(fetchContentError: Selection, fetchContentSuccess: Selection) throws -> Type { + select([GraphQLField.fragment(type: "FetchContentError", selection: fetchContentError.selection), GraphQLField.fragment(type: "FetchContentSuccess", selection: fetchContentSuccess.selection)]) + + switch response { + case let .decoding(data): + switch data.__typename { + case .fetchContentError: + let data = Objects.FetchContentError(errorCodes: data.errorCodes) + return try fetchContentError.decode(data: data) + case .fetchContentSuccess: + let data = Objects.FetchContentSuccess(success: data.success) + return try fetchContentSuccess.decode(data: data) + } + case .mocking: + return fetchContentError.mock() + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias FetchContentResult = Selection +} + extension Unions { struct FiltersResult { let __typename: TypeName @@ -33101,6 +33330,8 @@ extension Enums { enum ArticleSavingRequestStatus: String, CaseIterable, Codable { case archived = "ARCHIVED" + case contentNotFetched = "CONTENT_NOT_FETCHED" + case deleted = "DELETED" case failed = "FAILED" @@ -33414,6 +33645,15 @@ extension Enums { } } +extension Enums { + /// FetchContentErrorCode + enum FetchContentErrorCode: String, CaseIterable, Codable { + case badRequest = "BAD_REQUEST" + + case unauthorized = "UNAUTHORIZED" + } +} + extension Enums { /// FiltersErrorCode enum FiltersErrorCode: String, CaseIterable, Codable { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SetRuleMutation.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SetRuleMutation.swift index 80a68d73d..e48e6a34f 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SetRuleMutation.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SetRuleMutation.swift @@ -3,19 +3,9 @@ import Foundation import Models import SwiftGraphQL -// input SetRuleInput { -// id: ID -// name: String! -// description: String -// filter: String! -// actions: [RuleActionInput!]! -// enabled: Boolean! -// eventTypes: [RuleEventType!]! -// } - public struct Rule { - let id: String - let name: String + public let id: String + public let name: String } let ruleSelection = Selection.Rule { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/RulesQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/RulesQuery.swift new file mode 100644 index 000000000..a227b28bd --- /dev/null +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/RulesQuery.swift @@ -0,0 +1,46 @@ +import Foundation +import Models +import SwiftGraphQL + +public extension DataService { + func rules() async throws -> [Rule] { + enum QueryResult { + case success(result: [Rule]) + case error(error: String) + } + + let selection = Selection { + try $0.on( + rulesError: .init { + QueryResult.error(error: try $0.errorCodes().description) + }, + rulesSuccess: .init { + QueryResult.success(result: try $0.rules(selection: ruleSelection.list)) + } + ) + } + + let query = Selection.Query { + try $0.rules(selection: selection) + } + + let path = appEnvironment.graphqlPath + let headers = networker.defaultHeaders + + 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): + continuation.resume(returning: result) + case .error: + continuation.resume(throwing: BasicError.message(messageText: "Rules fetch error")) + } + } + } + } +}