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