From 6cf1f0e0fb5bb12c65860ae29deee8c15ee63f2c Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 27 Apr 2022 13:53:30 -0700 Subject: [PATCH 1/7] convert remonder creation to async --- .../App/Views/Home/HomeFeedViewIOS.swift | 19 +++++++---- .../App/Views/Home/HomeFeedViewModel.swift | 26 ++++++-------- .../Mutations/CreateReminder.swift | 34 +++++++------------ 3 files changed, 35 insertions(+), 44 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 793d9e431..9db2db1a6 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -67,13 +67,18 @@ import Views viewModel.selectedLinkItem = linkedItem } .formSheet(isPresented: $viewModel.snoozePresented) { - SnoozeView(snoozePresented: $viewModel.snoozePresented, itemToSnoozeID: $viewModel.itemToSnoozeID) { - viewModel.snoozeUntil( - dataService: dataService, - linkId: $0.feedItemId, - until: $0.snoozeUntilDate, - successMessage: $0.successMessage - ) + SnoozeView( + snoozePresented: $viewModel.snoozePresented, + itemToSnoozeID: $viewModel.itemToSnoozeID + ) { snoozeParams in + Task { + await viewModel.snoozeUntil( + dataService: dataService, + linkId: snoozeParams.feedItemId, + until: snoozeParams.snoozeUntilDate, + successMessage: snoozeParams.successMessage + ) + } } } .onAppear { // TODO: use task instead diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 1048bb76d..6f4bcaea3 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -158,31 +158,25 @@ import Views dataService.removeLink(objectID: objectID) } - func snoozeUntil(dataService: DataService, linkId: String, until: Date, successMessage: String?) { + func snoozeUntil(dataService: DataService, linkId: String, until: Date, successMessage: String?) async { isLoading = true if let itemIndex = items.firstIndex(where: { $0.id == linkId }) { items.remove(at: itemIndex) } - dataService.createReminderPublisher( + if await (try? dataService.createReminder( reminderItemId: .link(id: linkId), remindAt: until - ) - .sink( - receiveCompletion: { [weak self] completion in - guard case .failure = completion else { return } - self?.isLoading = false - NSNotification.operationFailed(message: "Failed to snooze") - }, - receiveValue: { [weak self] _ in - self?.isLoading = false - if let message = successMessage { - Snackbar.show(message: message) - } + )) != nil { + if let message = successMessage { + Snackbar.show(message: message) } - ) - .store(in: &subscriptions) + } else { + NSNotification.operationFailed(message: "Failed to snooze") + } + + isLoading = false } private var searchQuery: String? { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateReminder.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateReminder.swift index 5adfcd776..3804fcc6e 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateReminder.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateReminder.swift @@ -27,10 +27,10 @@ public enum ReminderItemId { } public extension DataService { - func createReminderPublisher( + func createReminder( reminderItemId: ReminderItemId, remindAt: Date - ) -> AnyPublisher { + ) async throws -> String { enum MutationResult { case complete(id: String) case error(errorCode: Enums.CreateReminderErrorCode) @@ -61,28 +61,20 @@ public extension DataService { let path = appEnvironment.graphqlPath let headers = networker.defaultHeaders - return Deferred { - Future { promise in - send(mutation, to: path, headers: headers) { result in - switch result { - case let .success(payload): - if let graphqlError = payload.errors { - promise(.failure(.message(messageText: "graphql error: \(graphqlError)"))) - } + 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 let .complete(id: id): - promise(.success(id)) - case let .error(errorCode: errorCode): - promise(.failure(.message(messageText: errorCode.rawValue))) - } - case .failure: - promise(.failure(.message(messageText: "graphql error"))) - } + switch payload.data { + case let .complete(id: id): + continuation.resume(returning: id) + case let .error(errorCode: errorCode): + continuation.resume(throwing: BasicError.message(messageText: errorCode.rawValue)) } } } - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() } } From 824c889ec6bf0293477c394963d7615525687ed6 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 27 Apr 2022 15:39:20 -0700 Subject: [PATCH 2/7] update label creator to generate a temp id on the client --- .../App/Views/Home/HomeFeedViewModel.swift | 12 +-- .../App/Views/Labels/ApplyLabelsView.swift | 6 +- .../Sources/App/Views/Labels/LabelsView.swift | 14 +-- .../App/Views/Labels/LabelsViewModel.swift | 75 +++++++--------- .../Mutations/CreateLabelPublisher.swift | 86 +++++++++++++------ .../Mutations/CreateReminder.swift | 6 +- .../Mutations/RemoveLabelPublisher.swift | 4 +- .../UpdateArticleLabelsPublisher.swift | 2 +- .../DataService/Queries/LabelsPublisher.swift | 37 ++++---- .../InternalLinkedItemLabel.swift | 7 +- 10 files changed, 134 insertions(+), 115 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 6f4bcaea3..1a03912ee 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -165,14 +165,16 @@ import Views items.remove(at: itemIndex) } - if await (try? dataService.createReminder( - reminderItemId: .link(id: linkId), - remindAt: until - )) != nil { + do { + try await dataService.createReminder( + reminderItemId: .link(id: linkId), + remindAt: until + ) + if let message = successMessage { Snackbar.show(message: message) } - } else { + } catch { NSNotification.operationFailed(message: "Failed to snooze") } diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift index 46d326e31..0a13fc566 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift @@ -135,12 +135,12 @@ struct ApplyLabelsView: View { #endif } } - .onAppear { + .task { switch mode { case let .item(feedItem): - viewModel.loadLabels(dataService: dataService, item: feedItem) + await viewModel.loadLabels(dataService: dataService, item: feedItem) case let .list(labels): - viewModel.loadLabels(dataService: dataService, initiallySelectedLabels: labels) + await viewModel.loadLabels(dataService: dataService, initiallySelectedLabels: labels) } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift index c86db2087..65208086b 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift @@ -9,7 +9,7 @@ struct LabelsView: View { @EnvironmentObject var dataService: DataService @StateObject var viewModel = LabelsViewModel() @State private var showDeleteConfirmation = false - @State private var labelToRemoveID: String? + @State private var labelToRemove: LinkedItemLabel? let footerText = "Use labels to create curated collections of links." @@ -20,14 +20,14 @@ struct LabelsView: View { innerBody .alert("Are you sure you want to delete this label?", isPresented: $showDeleteConfirmation) { Button("Delete Label", role: .destructive) { - if let labelID = labelToRemoveID { + if let label = labelToRemove { withAnimation { - viewModel.deleteLabel(dataService: dataService, labelID: labelID) + viewModel.deleteLabel(dataService: dataService, labelID: label.unwrappedID, name: label.unwrappedID) } } - self.labelToRemoveID = nil + self.labelToRemove = nil } - Button("Cancel", role: .cancel) { self.labelToRemoveID = nil } + Button("Cancel", role: .cancel) { self.labelToRemove = nil } } } #elseif os(macOS) @@ -37,7 +37,7 @@ struct LabelsView: View { .listStyle(InsetListStyle()) #endif } - .onAppear { viewModel.loadLabels(dataService: dataService, item: nil) } + .task { await viewModel.loadLabels(dataService: dataService, item: nil) } } private var innerBody: some View { @@ -64,7 +64,7 @@ struct LabelsView: View { Spacer() Button( action: { - labelToRemoveID = label.id + labelToRemove = label showDeleteConfirmation = true }, label: { Image(systemName: "trash") } diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift index 84bb1fa46..4be27e667 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift @@ -4,8 +4,7 @@ import Services import SwiftUI import Views -final class LabelsViewModel: ObservableObject { - private var hasLoadedInitialLabels = false +@MainActor final class LabelsViewModel: ObservableObject { @Published var isLoading = false @Published var selectedLabels = [LinkedItemLabel]() @Published var unselectedLabels = [LinkedItemLabel]() @@ -19,65 +18,55 @@ final class LabelsViewModel: ObservableObject { dataService: DataService, item: LinkedItem? = nil, initiallySelectedLabels: [LinkedItemLabel]? = nil - ) { - guard !hasLoadedInitialLabels else { return } + ) async { isLoading = true - dataService.labelsPublisher().sink( - receiveCompletion: { _ in }, - receiveValue: { [weak self] labelIDs in - guard let self = self else { return } - dataService.viewContext.performAndWait { - self.labels = labelIDs.compactMap { dataService.viewContext.object(with: $0) as? LinkedItemLabel } - } - let selLabels = initiallySelectedLabels ?? item?.labels.asArray(of: LinkedItemLabel.self) ?? [] - for label in self.labels { - if selLabels.contains(label) { - self.selectedLabels.append(label) - } else { - self.unselectedLabels.append(label) - } - } - self.hasLoadedInitialLabels = true - self.isLoading = false + if let labelIDs = try? await dataService.labels() { + dataService.viewContext.performAndWait { + self.labels = labelIDs.compactMap { dataService.viewContext.object(with: $0) as? LinkedItemLabel } } - ) - .store(in: &subscriptions) + let selLabels = initiallySelectedLabels ?? item?.labels.asArray(of: LinkedItemLabel.self) ?? [] + for label in labels { + if selLabels.contains(label) { + selectedLabels.append(label) + } else { + unselectedLabels.append(label) + } + } + } + + isLoading = false } func createLabel(dataService: DataService, name: String, color: Color, description: String?) { isLoading = true - dataService.createLabelPublisher( + guard let labelObjectID = try? dataService.createLabel( name: name, color: color.hex ?? "", description: description - ).sink( - receiveCompletion: { [weak self] _ in - self?.isLoading = false - }, - receiveValue: { [weak self] labelID in - if let label = dataService.viewContext.object(with: labelID) as? LinkedItemLabel { - self?.labels.insert(label, at: 0) - self?.unselectedLabels.insert(label, at: 0) - } - self?.isLoading = false - self?.showCreateEmailModal = false - } - ) - .store(in: &subscriptions) + ) else { + return + } + + if let label = dataService.viewContext.object(with: labelObjectID) as? LinkedItemLabel { + labels.insert(label, at: 0) + unselectedLabels.insert(label, at: 0) + } + + showCreateEmailModal = false } - func deleteLabel(dataService: DataService, labelID: String) { + func deleteLabel(dataService: DataService, labelID: String, name: String) { isLoading = true - dataService.removeLabelPublisher(labelID: labelID).sink( + dataService.removeLabelPublisher(labelID: labelID, name: name).sink( receiveCompletion: { [weak self] _ in self?.isLoading = false }, receiveValue: { [weak self] _ in self?.isLoading = false - self?.labels.removeAll { $0.id == labelID } + self?.labels.removeAll { $0.name == name } } ) .store(in: &subscriptions) @@ -104,11 +93,11 @@ final class LabelsViewModel: ObservableObject { func addLabelToItem(_ label: LinkedItemLabel) { selectedLabels.insert(label, at: 0) - unselectedLabels.removeAll { $0.id == label.id } + unselectedLabels.removeAll { $0.name == label.name } } func removeLabelFromItem(_ label: LinkedItemLabel) { unselectedLabels.insert(label, at: 0) - selectedLabels.removeAll { $0.id == label.id } + selectedLabels.removeAll { $0.name == label.name } } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateLabelPublisher.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateLabelPublisher.swift index 6df9d4de0..7c46fcddd 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateLabelPublisher.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateLabelPublisher.swift @@ -4,12 +4,30 @@ import Foundation import Models import SwiftGraphQL -public extension DataService { - func createLabelPublisher( +extension DataService { + public func createLabel( name: String, color: String, description: String? - ) -> AnyPublisher { + ) throws -> NSManagedObjectID { + let internalLabel = InternalLinkedItemLabel( + id: UUID().uuidString, + name: name, + color: color, + createdAt: nil, + labelDescription: description + ) + + if let labelObjectID = internalLabel.persist(context: backgroundContext) { + // Send update to server + syncLabelCreation(label: internalLabel) + return labelObjectID + } else { + throw BasicError.message(messageText: "core data error") + } + } + + func syncLabelCreation(label: InternalLinkedItemLabel) { enum MutationResult { case saved(label: InternalLinkedItemLabel) case error(errorCode: Enums.CreateLabelErrorCode) @@ -25,9 +43,9 @@ public extension DataService { let mutation = Selection.Mutation { try $0.createLabel( input: InputObjects.CreateLabelInput( - name: name, - color: color, - description: OptionalArgument(description) + name: label.name, + color: label.color, + description: OptionalArgument(label.labelDescription) ), selection: selection ) @@ -35,33 +53,45 @@ public extension DataService { let path = appEnvironment.graphqlPath let headers = networker.defaultHeaders + let context = backgroundContext - return Deferred { - Future { promise in - send(mutation, to: path, headers: headers) { result in - switch result { - case let .success(payload): - if let graphqlError = payload.errors { - promise(.failure(.message(messageText: "graphql error: \(graphqlError)"))) - } + send(mutation, to: path, headers: headers) { result in + let payload = try? result.get() - switch payload.data { - case let .saved(label: label): - if let labelObjectID = [label].persist(context: self.backgroundContext)?.first { - promise(.success(labelObjectID)) - } else { - promise(.failure(.message(messageText: "core data error"))) - } - case let .error(errorCode: errorCode): - promise(.failure(.message(messageText: errorCode.rawValue))) - } - case .failure: - promise(.failure(.message(messageText: "graphql error"))) + let updatedLabelID: String? = { + if let payload = try? result.get() { + switch payload.data { + case let .saved(label: label): + return label.id + case .error: + return nil } } + return nil + }() + + let syncStatus: ServerSyncStatus = payload == nil ? .needsCreation : .isNSync + + context.perform { + let fetchRequest: NSFetchRequest = LinkedItemLabel.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(LinkedItemLabel.name), label.name) + + guard let labelObject = (try? context.fetch(fetchRequest))?.first else { return } + labelObject.serverSyncStatus = Int64(syncStatus.rawValue) + + // Updated id with the one generated on the server + if let updatedLabelID = updatedLabelID { + labelObject.id = updatedLabelID + } + + do { + try context.save() + logger.debug("Label created succesfully") + } catch { + context.rollback() + logger.debug("Failed to create Label: \(error.localizedDescription)") + } } } - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateReminder.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateReminder.swift index 3804fcc6e..ee86cfd78 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateReminder.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateReminder.swift @@ -30,7 +30,7 @@ public extension DataService { func createReminder( reminderItemId: ReminderItemId, remindAt: Date - ) async throws -> String { + ) async throws { enum MutationResult { case complete(id: String) case error(errorCode: Enums.CreateReminderErrorCode) @@ -69,8 +69,8 @@ public extension DataService { } switch payload.data { - case let .complete(id: id): - continuation.resume(returning: id) + case .complete: + continuation.resume() case let .error(errorCode: errorCode): continuation.resume(throwing: BasicError.message(messageText: errorCode.rawValue)) } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RemoveLabelPublisher.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RemoveLabelPublisher.swift index 8aa9ab449..ac1a9be7b 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RemoveLabelPublisher.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RemoveLabelPublisher.swift @@ -4,7 +4,7 @@ import Models import SwiftGraphQL public extension DataService { - func removeLabelPublisher(labelID: String) -> AnyPublisher { + func removeLabelPublisher(labelID: String, name: String) -> AnyPublisher { enum MutationResult { case success(labelID: String) case error(errorCode: Enums.DeleteLabelErrorCode) @@ -37,7 +37,7 @@ public extension DataService { switch payload.data { case .success: - if let label = LinkedItemLabel.lookup(byID: labelID, inContext: self.backgroundContext) { + if let label = LinkedItemLabel.lookup(byName: name, inContext: self.backgroundContext) { label.remove(inContext: self.backgroundContext) promise(.success(true)) } else { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleLabelsPublisher.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleLabelsPublisher.swift index 20467443c..12af0de3c 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleLabelsPublisher.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleLabelsPublisher.swift @@ -56,7 +56,7 @@ public extension DataService { linkedItem.removeFromLabels(existingLabels) } for label in labels { - if let labelObject = LinkedItemLabel.lookup(byID: label.id, inContext: self.backgroundContext) { + if let labelObject = LinkedItemLabel.lookup(byName: label.name, inContext: self.backgroundContext) { linkedItem.addToLabels(labelObject) } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LabelsPublisher.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LabelsPublisher.swift index a34de8458..fbf1160bf 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LabelsPublisher.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LabelsPublisher.swift @@ -5,7 +5,7 @@ import Models import SwiftGraphQL public extension DataService { - func labelsPublisher() -> AnyPublisher<[NSManagedObjectID], ServerError> { + func labels() async throws -> [NSManagedObjectID] { enum QueryResult { case success(result: [InternalLinkedItemLabel]) case error(error: String) @@ -26,29 +26,26 @@ public extension DataService { let path = appEnvironment.graphqlPath let headers = networker.defaultHeaders + let context = backgroundContext - return Deferred { - Future { promise in - send(query, to: path, headers: headers) { result in - switch result { - case let .success(payload): - switch payload.data { - case let .success(result: labels): - if let labelObjectIDs = labels.persist(context: self.backgroundContext) { - promise(.success(labelObjectIDs)) - } else { - promise(.failure(.unknown)) - } - case .error: - promise(.failure(.unknown)) - } - case .failure: - promise(.failure(.unknown)) + 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: labels): + if let labelObjectIDs = labels.persist(context: context) { + continuation.resume(returning: labelObjectIDs) + } else { + continuation.resume(throwing: BasicError.message(messageText: "CoreData error")) } + case .error: + continuation.resume(throwing: BasicError.message(messageText: "Newsletter Email fetch error")) } } } - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() } } diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItemLabel.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItemLabel.swift index f8dabe194..ca31ae33e 100644 --- a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItemLabel.swift +++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItemLabel.swift @@ -29,7 +29,7 @@ struct InternalLinkedItemLabel { } func asManagedObject(inContext context: NSManagedObjectContext) -> LinkedItemLabel { - let existingItem = LinkedItemLabel.lookup(byID: id, inContext: context) + let existingItem = LinkedItemLabel.lookup(byName: name, inContext: context) let label = existingItem ?? LinkedItemLabel(entity: LinkedItemLabel.entity(), insertInto: context) label.id = id label.name = name @@ -42,11 +42,12 @@ struct InternalLinkedItemLabel { extension LinkedItemLabel { public var unwrappedID: String { id ?? "" } + public var unwrappedName: String { name ?? "" } - static func lookup(byID labelID: String, inContext context: NSManagedObjectContext) -> LinkedItemLabel? { + static func lookup(byName name: String, inContext context: NSManagedObjectContext) -> LinkedItemLabel? { let fetchRequest: NSFetchRequest = LinkedItemLabel.fetchRequest() fetchRequest.predicate = NSPredicate( - format: "id == %@", labelID + format: "%K == %@", #keyPath(LinkedItemLabel.name), name ) var label: LinkedItemLabel? From 05574a640eab2b3b8c4e8c22f505f11e417e487c Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 27 Apr 2022 17:12:27 -0700 Subject: [PATCH 3/7] remove performActionSubject from web wrapper view model --- .../App/Views/LinkItemDetailView.swift | 12 +---- .../Views/Article/WebAppWrapperView.swift | 52 +++++++------------ 2 files changed, 21 insertions(+), 43 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift index f478035a8..ec06cdbfb 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift @@ -76,21 +76,11 @@ enum PDFProvider { queryParams: ["isAppEmbedView": "true", "highlightBarDisabled": isMacApp ? "false" : "true"] ) - let newWebAppWrapperViewModel = WebAppWrapperViewModel( + webAppWrapperViewModel = WebAppWrapperViewModel( webViewURLRequest: urlRequest, baseURL: baseURL, rawAuthCookie: rawAuthCookie ) - - newWebAppWrapperViewModel.performActionSubject.sink { action in - switch action { - case let .shareHighlight(highlightID): - print("show share modal for highlight with id: \(highlightID)") - } - } - .store(in: &newWebAppWrapperViewModel.subscriptions) - - webAppWrapperViewModel = newWebAppWrapperViewModel } } diff --git a/apple/OmnivoreKit/Sources/Views/Article/WebAppWrapperView.swift b/apple/OmnivoreKit/Sources/Views/Article/WebAppWrapperView.swift index 44d0ae7c4..29b3f2f8f 100644 --- a/apple/OmnivoreKit/Sources/Views/Article/WebAppWrapperView.swift +++ b/apple/OmnivoreKit/Sources/Views/Article/WebAppWrapperView.swift @@ -1,4 +1,3 @@ -import Combine import SafariServices import SwiftUI import WebKit @@ -8,8 +7,6 @@ public final class WebAppWrapperViewModel: ObservableObject { case shareHighlight(highlightID: String) } - public var subscriptions = Set() - public let performActionSubject = PassthroughSubject() let webViewURLRequest: URLRequest let baseURL: URL let rawAuthCookie: String? @@ -43,27 +40,25 @@ public struct WebAppWrapperView: View { } public var body: some View { - let webAppView = WebAppView( - request: viewModel.webViewURLRequest, - baseURL: viewModel.baseURL, - rawAuthCookie: viewModel.rawAuthCookie, - openLinkAction: { - #if os(macOS) - NSWorkspace.shared.open($0) - #elseif os(iOS) - safariWebLink = SafariWebLink(id: UUID(), url: $0) - #endif - }, - webViewActionHandler: webViewActionHandler, - navBarVisibilityRatioUpdater: navBarVisibilityRatioUpdater, - annotation: $annotation, - annotationSaveTransactionID: $annotationSaveTransactionID, - sendIncreaseFontSignal: $viewModel.sendIncreaseFontSignal, - sendDecreaseFontSignal: $viewModel.sendDecreaseFontSignal - ) - - return VStack { - webAppView + VStack { + WebAppView( + request: viewModel.webViewURLRequest, + baseURL: viewModel.baseURL, + rawAuthCookie: viewModel.rawAuthCookie, + openLinkAction: { + #if os(macOS) + NSWorkspace.shared.open($0) + #elseif os(iOS) + safariWebLink = SafariWebLink(id: UUID(), url: $0) + #endif + }, + webViewActionHandler: webViewActionHandler, + navBarVisibilityRatioUpdater: navBarVisibilityRatioUpdater, + annotation: $annotation, + annotationSaveTransactionID: $annotationSaveTransactionID, + sendIncreaseFontSignal: $viewModel.sendIncreaseFontSignal, + sendDecreaseFontSignal: $viewModel.sendDecreaseFontSignal + ) } .sheet(item: $safariWebLink) { SafariView(url: $0.url) @@ -92,16 +87,9 @@ public struct WebAppWrapperView: View { guard let messageBody = message.body as? [String: String] else { return } guard let actionID = messageBody["actionID"] else { return } - switch actionID { - case "share": - if let highlightId = messageBody["highlightID"] { - viewModel.performActionSubject.send(.shareHighlight(highlightID: highlightId)) - } - case "annotate": + if actionID == "annotate" { annotation = messageBody["annotation"] ?? "" showHighlightAnnotationModal = true - default: - break } } } From d7bc70fcd1c6b8acb6911d3cd2893fb6fea3a573 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 27 Apr 2022 17:22:02 -0700 Subject: [PATCH 4/7] pass in correct value for label.name when deleting it --- apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift | 6 +++++- .../Sources/App/Views/Labels/LabelsViewModel.swift | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift index 65208086b..8841ba98b 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift @@ -22,7 +22,11 @@ struct LabelsView: View { Button("Delete Label", role: .destructive) { if let label = labelToRemove { withAnimation { - viewModel.deleteLabel(dataService: dataService, labelID: label.unwrappedID, name: label.unwrappedID) + viewModel.deleteLabel( + dataService: dataService, + labelID: label.unwrappedID, + name: label.unwrappedName + ) } } self.labelToRemove = nil diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift index 4be27e667..eb4ba0001 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift @@ -67,6 +67,8 @@ import Views receiveValue: { [weak self] _ in self?.isLoading = false self?.labels.removeAll { $0.name == name } + self?.selectedLabels.removeAll { $0.name == name } + self?.unselectedLabels.removeAll { $0.name == name } } ) .store(in: &subscriptions) From acb1ae6c530dea20107a81deccad2ef975c249bf Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 27 Apr 2022 18:06:57 -0700 Subject: [PATCH 5/7] use credata for updating item labels --- .../App/Views/Home/HomeFeedViewIOS.swift | 6 +- .../App/Views/Labels/ApplyLabelsView.swift | 10 +-- .../App/Views/Labels/LabelsViewModel.swift | 19 +--- .../UpdateArticleLabelsPublisher.swift | 88 ++++++++----------- 4 files changed, 45 insertions(+), 78 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 9db2db1a6..9e31848d7 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -48,7 +48,7 @@ import Views loadItems(isRefresh: true) } .sheet(item: $viewModel.itemUnderLabelEdit) { item in - ApplyLabelsView(mode: .item(item)) { _ in } + ApplyLabelsView(mode: .item(item)) } } .navigationTitle("Home") @@ -111,9 +111,7 @@ import Views } .padding(.horizontal) .sheet(isPresented: $showLabelsSheet) { - ApplyLabelsView(mode: .list(viewModel.selectedLabels)) { labels in - viewModel.selectedLabels = labels - } + ApplyLabelsView(mode: .list(viewModel.selectedLabels)) } } if prefersListLayout { diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift index 0a13fc566..73742ae8f 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift @@ -28,7 +28,6 @@ struct ApplyLabelsView: View { } let mode: Mode - let commitLabelChanges: ([LinkedItemLabel]) -> Void @EnvironmentObject var dataService: DataService @Environment(\.presentationMode) private var presentationMode @@ -100,14 +99,11 @@ struct ApplyLabelsView: View { action: { switch mode { case let .item(feedItem): - viewModel.saveItemLabelChanges(itemID: feedItem.unwrappedID, dataService: dataService) { labels in - commitLabelChanges(labels) - presentationMode.wrappedValue.dismiss() - } + viewModel.saveItemLabelChanges(itemID: feedItem.unwrappedID, dataService: dataService) case .list: - commitLabelChanges(viewModel.selectedLabels) - presentationMode.wrappedValue.dismiss() + break } + presentationMode.wrappedValue.dismiss() }, label: { Text(mode.confirmButtonText).foregroundColor(.appGrayTextContrast) } ) diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift index eb4ba0001..fdb8a2e75 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift @@ -74,23 +74,8 @@ import Views .store(in: &subscriptions) } - func saveItemLabelChanges( - itemID: String, - dataService: DataService, - onComplete: @escaping ([LinkedItemLabel]) -> Void - ) { - isLoading = true - dataService.updateArticleLabelsPublisher(itemID: itemID, labelIDs: selectedLabels.map(\.unwrappedID)).sink( - receiveCompletion: { [weak self] _ in - self?.isLoading = false - }, - receiveValue: { labelIDs in - onComplete( - labelIDs.compactMap { dataService.viewContext.object(with: $0) as? LinkedItemLabel } - ) - } - ) - .store(in: &subscriptions) + func saveItemLabelChanges(itemID: String, dataService: DataService) { + dataService.updateItemLabels(itemID: itemID, labelNames: selectedLabels.map(\.unwrappedName)) } func addLabelToItem(_ label: LinkedItemLabel) { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleLabelsPublisher.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleLabelsPublisher.swift index 12af0de3c..af944c004 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleLabelsPublisher.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleLabelsPublisher.swift @@ -1,15 +1,33 @@ -import Combine import CoreData import Foundation import Models import SwiftGraphQL -public extension DataService { - // swiftlint:disable:next function_body_length - func updateArticleLabelsPublisher( - itemID: String, - labelIDs: [String] - ) -> AnyPublisher<[NSManagedObjectID], BasicError> { +extension DataService { + public func updateItemLabels(itemID: String, labelNames: [String]) { + backgroundContext.perform { [weak self] in + guard let self = self else { return } + guard let linkedItem = LinkedItem.lookup(byID: itemID, inContext: self.backgroundContext) else { return } + + if let existingLabels = linkedItem.labels { + linkedItem.removeFromLabels(existingLabels) + } + + var labelIDs = [String]() + + for labelName in labelNames { + if let labelObject = LinkedItemLabel.lookup(byName: labelName, inContext: self.backgroundContext) { + linkedItem.addToLabels(labelObject) + labelIDs.append(labelObject.unwrappedID) + } + } + + // Send update to server + self.syncLabelUpdates(itemID: itemID, labelIDs: labelIDs) + } + } + + func syncLabelUpdates(itemID: String, labelIDs: [String]) { enum MutationResult { case saved(feedItem: [InternalLinkedItemLabel]) case error(errorCode: Enums.SetLabelsErrorCode) @@ -34,54 +52,24 @@ public extension DataService { let path = appEnvironment.graphqlPath let headers = networker.defaultHeaders + let context = backgroundContext - return Deferred { - Future { promise in - send(mutation, to: path, headers: headers) { result in - switch result { - case let .success(payload): - if let graphqlError = payload.errors { - promise(.failure(.message(messageText: graphqlError.first.debugDescription))) - } + send(mutation, to: path, headers: headers) { result in + let data = try? result.get() + let syncStatus: ServerSyncStatus = data == nil ? .needsUpdate : .isNSync - switch payload.data { - case let .saved(labels): - self.backgroundContext.perform { - guard let linkedItem = LinkedItem.lookup(byID: itemID, inContext: self.backgroundContext) else { - promise(.failure(.message(messageText: "failed to set labels"))) - return - } + context.perform { + guard let linkedItem = LinkedItem.lookup(byID: itemID, inContext: context) else { return } + linkedItem.serverSyncStatus = Int64(syncStatus.rawValue) - if let existingLabels = linkedItem.labels { - linkedItem.removeFromLabels(existingLabels) - } - for label in labels { - if let labelObject = LinkedItemLabel.lookup(byName: label.name, inContext: self.backgroundContext) { - linkedItem.addToLabels(labelObject) - } - } - - do { - try self.backgroundContext.save() - logger.debug("Item labels updated") - let labelObjects = linkedItem.labels.asArray(of: LinkedItemLabel.self) - promise(.success(labelObjects.map(\.objectID))) - } catch { - self.backgroundContext.rollback() - logger.debug("Failed to update item labels: \(error.localizedDescription)") - promise(.failure(.message(messageText: "failed to set labels"))) - } - } - case .error: - promise(.failure(.message(messageText: "failed to set labels"))) - } - case .failure: - promise(.failure(.message(messageText: "failed to set labels"))) - } + do { + try context.save() + logger.debug("Item labels updated succesfully") + } catch { + context.rollback() + logger.debug("Failed to update item labels: \(error.localizedDescription)") } } } - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() } } From fb4312e52ac688579df32668f9a84e7b37863377 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 27 Apr 2022 20:11:50 -0700 Subject: [PATCH 6/7] update delete labels function to work from core data --- .../App/Views/Labels/LabelsViewModel.swift | 21 ++----- .../Mutations/RemoveLabelPublisher.swift | 60 +++++++++++-------- 2 files changed, 38 insertions(+), 43 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift index fdb8a2e75..7870449e2 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift @@ -1,4 +1,3 @@ -import Combine import Models import Services import SwiftUI @@ -12,8 +11,6 @@ import Views @Published var showCreateEmailModal = false @Published var labelSearchFilter = "" - var subscriptions = Set() - func loadLabels( dataService: DataService, item: LinkedItem? = nil, @@ -58,20 +55,10 @@ import Views } func deleteLabel(dataService: DataService, labelID: String, name: String) { - isLoading = true - - dataService.removeLabelPublisher(labelID: labelID, name: name).sink( - receiveCompletion: { [weak self] _ in - self?.isLoading = false - }, - receiveValue: { [weak self] _ in - self?.isLoading = false - self?.labels.removeAll { $0.name == name } - self?.selectedLabels.removeAll { $0.name == name } - self?.unselectedLabels.removeAll { $0.name == name } - } - ) - .store(in: &subscriptions) + dataService.removeLabel(labelID: labelID, name: name) + labels.removeAll { $0.name == name } + selectedLabels.removeAll { $0.name == name } + unselectedLabels.removeAll { $0.name == name } } func saveItemLabelChanges(itemID: String, dataService: DataService) { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RemoveLabelPublisher.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RemoveLabelPublisher.swift index ac1a9be7b..8b76b609b 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RemoveLabelPublisher.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RemoveLabelPublisher.swift @@ -3,8 +3,20 @@ import Foundation import Models import SwiftGraphQL -public extension DataService { - func removeLabelPublisher(labelID: String, name: String) -> AnyPublisher { +extension DataService { + public func removeLabel(labelID: String, name: String) { + // Update CoreData + backgroundContext.perform { [weak self] in + guard let self = self else { return } + guard let label = LinkedItemLabel.lookup(byName: name, inContext: self.backgroundContext) else { return } + label.remove(inContext: self.backgroundContext) + + // Send update to server + self.syncLabelDeletion(labelID: labelID, labelName: name) + } + } + + func syncLabelDeletion(labelID: String, labelName: String) { enum MutationResult { case success(labelID: String) case error(errorCode: Enums.DeleteLabelErrorCode) @@ -25,34 +37,30 @@ public extension DataService { let path = appEnvironment.graphqlPath let headers = networker.defaultHeaders + let context = backgroundContext - return Deferred { - Future { promise in - send(mutation, to: path, headers: headers) { result in - switch result { - case let .success(payload): - if payload.errors != nil { - promise(.failure(.message(messageText: "Error removing label"))) - } + send(mutation, to: path, headers: headers) { result in + let data = try? result.get() + let isSyncSuccess = data != nil - switch payload.data { - case .success: - if let label = LinkedItemLabel.lookup(byName: name, inContext: self.backgroundContext) { - label.remove(inContext: self.backgroundContext) - promise(.success(true)) - } else { - promise(.failure(.message(messageText: "Error removing label"))) - } - case .error: - promise(.failure(.message(messageText: "Error removing label"))) - } - case .failure: - promise(.failure(.message(messageText: "Error removing label"))) - } + context.perform { + let label = LinkedItemLabel.lookup(byName: labelName, inContext: context) + guard let label = label else { return } + + if isSyncSuccess { + label.remove(inContext: context) + } else { + label.serverSyncStatus = Int64(ServerSyncStatus.needsDeletion.rawValue) + } + + do { + try context.save() + logger.debug("LinkedItem deleted succesfully") + } catch { + context.rollback() + logger.debug("Failed to delete LinkedItem: \(error.localizedDescription)") } } } - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() } } From 0e8d5f23da25663548cc4db34f436c2960409864 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 27 Apr 2022 20:55:57 -0700 Subject: [PATCH 7/7] move article content fetching into fewer functions --- .../App/Views/Home/HomeFeedViewModel.swift | 5 +- .../Views/WebReader/WebReaderContainer.swift | 6 +- .../Views/WebReader/WebReaderViewModel.swift | 29 +----- .../Services/DataService/DataService.swift | 36 ------- .../Queries/ArticleContentQuery.swift | 96 ++++++++++++------- .../Queries/LibraryItemsQuery.swift | 1 - 6 files changed, 67 insertions(+), 106 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 1a03912ee..fb400565a 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -1,4 +1,3 @@ -import Combine import CoreData import Models import Services @@ -26,8 +25,6 @@ import Views var searchIdx = 0 var receivedIdx = 0 - var subscriptions = Set() - init() {} func itemAppeared(item: LinkedItem, dataService: DataService) async { @@ -83,7 +80,7 @@ import Views isLoading = false receivedIdx = thisSearchIdx cursor = queryResult.cursor - dataService.prefetchPages(itemSlugs: newItems.map(\.unwrappedSlug)) + await dataService.prefetchPages(itemSlugs: newItems.map(\.unwrappedSlug)) } else if searchTermIsEmpty { await dataService.viewContext.perform { let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index ebd821451..7307a3aac 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -190,10 +190,8 @@ import WebKit } else { Color.clear .contentShape(Rectangle()) - .onAppear { - if !viewModel.isLoading { - viewModel.loadContent(dataService: dataService, slug: item.unwrappedSlug) - } + .task { + await viewModel.loadContent(dataService: dataService, slug: item.unwrappedSlug) } } if showFontSizePopover { diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift index ce0c9171a..0eaaa1acc 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift @@ -1,4 +1,3 @@ -import Combine import Models import Services import SwiftUI @@ -9,33 +8,15 @@ struct SafariWebLink: Identifiable { let url: URL } -final class WebReaderViewModel: ObservableObject { - @Published var isLoading = false +@MainActor final class WebReaderViewModel: ObservableObject { @Published var articleContent: ArticleContent? var slug: String? - var subscriptions = Set() - func loadContent(dataService: DataService, slug: String) { + func loadContent(dataService: DataService, slug: String) async { self.slug = slug - isLoading = true - guard let username = dataService.currentViewer?.username else { return } - - if let content = dataService.pageFromCache(slug: slug) { - articleContent = content - } else { - dataService.articleContentPublisher(username: username, slug: slug).sink( - receiveCompletion: { [weak self] completion in - guard case .failure = completion else { return } - self?.isLoading = false - }, - receiveValue: { [weak self] articleContent in - self?.articleContent = articleContent - } - ) - .store(in: &subscriptions) - } + articleContent = try? await dataService.articleContent(username: username, slug: slug, useCache: true) } func createHighlight( @@ -141,16 +122,12 @@ final class WebReaderViewModel: ObservableObject { switch actionID { case "deleteHighlight": - dataService.invalidateCachedPage(slug: slug) deleteHighlight(messageBody: messageBody, replyHandler: replyHandler, dataService: dataService) case "createHighlight": - dataService.invalidateCachedPage(slug: slug) createHighlight(messageBody: messageBody, replyHandler: replyHandler, dataService: dataService) case "mergeHighlight": - dataService.invalidateCachedPage(slug: slug) mergeHighlight(messageBody: messageBody, replyHandler: replyHandler, dataService: dataService) case "updateHighlight": - dataService.invalidateCachedPage(slug: slug) updateHighlight(messageBody: messageBody, replyHandler: replyHandler, dataService: dataService) case "articleReadingProgress": updateReadingProgress(messageBody: messageBody, replyHandler: replyHandler, dataService: dataService) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift index 60ec412c1..145e0dade 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift @@ -50,39 +50,3 @@ public final class DataService: ObservableObject { } } } - -public extension DataService { - func prefetchPages(itemSlugs: [String]) { - guard let username = currentViewer?.username else { return } - - for slug in itemSlugs { - articleContentPublisher(username: username, slug: slug).sink( - receiveCompletion: { _ in }, - receiveValue: { _ in } - ) - .store(in: &subscriptions) - } - } - - func pageFromCache(slug: String) -> ArticleContent? { - let linkedItemFetchRequest: NSFetchRequest = LinkedItem.fetchRequest() - linkedItemFetchRequest.predicate = NSPredicate( - format: "slug == %@", slug - ) - - guard let linkedItem = try? persistentContainer.viewContext.fetch(linkedItemFetchRequest).first else { return nil } - guard let htmlContent = linkedItem.htmlContent else { return nil } - - let highlights = linkedItem - .highlights - .asArray(of: Highlight.self) - .filter { $0.serverSyncStatus != ServerSyncStatus.needsDeletion.rawValue } - - return ArticleContent( - htmlContent: htmlContent, - highlightsJSONString: highlights.map { InternalHighlight.make(from: $0) }.asJSONString - ) - } - - func invalidateCachedPage(slug _: String?) {} -} diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift index aa166d058..f9b3b8f46 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift @@ -1,16 +1,28 @@ -import Combine import CoreData import Foundation import Models import SwiftGraphQL -public extension DataService { - struct ArticleProps { - let htmlContent: String - let highlights: [InternalHighlight] +extension DataService { + public func prefetchPages(itemSlugs: [String]) async { + guard let username = currentViewer?.username else { return } + + for slug in itemSlugs { + // TODO: maybe check for cached content before downloading again? check timestamp? + _ = try? await articleContent(username: username, slug: slug, useCache: false) + } } - func articleContentPublisher(username: String, slug: String) -> AnyPublisher { + public func articleContent(username: String, slug: String, useCache: Bool) async throws -> ArticleContent { + struct ArticleProps { + let htmlContent: String + let highlights: [InternalHighlight] + } + + if useCache, let cachedContent = cachedArticleContent(slug: slug) { + return cachedContent + } + enum QueryResult { case success(result: ArticleProps) case error(error: String) @@ -41,40 +53,34 @@ public extension DataService { let path = appEnvironment.graphqlPath let headers = networker.defaultHeaders - return Deferred { - Future { promise in - send(query, to: path, headers: headers) { [weak self] result in - switch result { - case let .success(payload): - switch payload.data { - case let .success(result: result): - // store result in core data - self?.persistArticleContent( - htmlContent: result.htmlContent, - slug: slug, - highlights: result.highlights - ) - promise(.success( - ArticleContent( - htmlContent: result.htmlContent, - highlightsJSONString: result.highlights.asJSONString - )) - ) - case .error: - promise(.failure(.unknown)) - } - case .failure: - promise(.failure(.unknown)) - } + return try await withCheckedThrowingContinuation { continuation in + send(query, to: path, headers: headers) { [weak self] queryResult in + guard let payload = try? queryResult.get() else { + continuation.resume(throwing: BasicError.message(messageText: "network error")) + return + } + + switch payload.data { + case let .success(result: result): + self?.persistArticleContent( + htmlContent: result.htmlContent, + slug: slug, + highlights: result.highlights + ) + + let articleContent = ArticleContent( + htmlContent: result.htmlContent, + highlightsJSONString: result.highlights.asJSONString + ) + + continuation.resume(returning: articleContent) + case .error: + continuation.resume(throwing: BasicError.message(messageText: "LinkedItem fetch error")) } } } - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() } -} -extension DataService { func persistArticleContent(htmlContent: String, slug: String, highlights: [InternalHighlight]) { backgroundContext.perform { let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() @@ -101,4 +107,24 @@ extension DataService { } } } + + func cachedArticleContent(slug: String) -> ArticleContent? { + let linkedItemFetchRequest: NSFetchRequest = LinkedItem.fetchRequest() + linkedItemFetchRequest.predicate = NSPredicate( + format: "slug == %@", slug + ) + + guard let linkedItem = try? persistentContainer.viewContext.fetch(linkedItemFetchRequest).first else { return nil } + guard let htmlContent = linkedItem.htmlContent else { return nil } + + let highlights = linkedItem + .highlights + .asArray(of: Highlight.self) + .filter { $0.serverSyncStatus != ServerSyncStatus.needsDeletion.rawValue } + + return ArticleContent( + htmlContent: htmlContent, + highlightsJSONString: highlights.map { InternalHighlight.make(from: $0) }.asJSONString + ) + } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LibraryItemsQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LibraryItemsQuery.swift index f152d8bc6..518b178f4 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LibraryItemsQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LibraryItemsQuery.swift @@ -1,4 +1,3 @@ -import Combine import CoreData import Foundation import Models