diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index e0ebabb8d..6a917f435 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -76,6 +76,9 @@ final class HomeFeedViewModel: ObservableObject { if thisSearchIdx > 0, thisSearchIdx <= self?.receivedIdx ?? 0 { return } + + dataService.prefetchPages(items: result.items) + self?.items = isRefresh ? result.items : (self?.items ?? []) + result.items self?.isLoading = false self?.receivedIdx = thisSearchIdx diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index 503f67417..7cc5bf938 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -204,7 +204,7 @@ struct WebReaderContainerView: View { Color.systemBackground .transition(.opacity) .onAppear { - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250)) { + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { withAnimation(.linear(duration: 0.2)) { showOverlay = false } diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift index 1dd5aa8dd..5764bac83 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift @@ -21,13 +21,20 @@ final class WebReaderViewModel: ObservableObject { @Published var isLoading = false @Published var articleContent: ArticleContent? + var slug: String? var subscriptions = Set() func loadContent(dataService: DataService, slug: String) { + self.slug = slug isLoading = true guard let viewer = dataService.currentViewer else { return } + if let content = dataService.pageFromCache(slug: slug) { + articleContent = content + // continue to load from the web if possible + } + dataService.articleContentPublisher(username: viewer.username, slug: slug).sink( receiveCompletion: { [weak self] completion in guard case .failure = completion else { return } @@ -35,6 +42,7 @@ final class WebReaderViewModel: ObservableObject { }, receiveValue: { [weak self] articleContent in self?.articleContent = articleContent + dataService.pageCache.setObject(CachedPageContent(slug, articleContent), forKey: NSString(string: slug)) } ) .store(in: &subscriptions) @@ -167,12 +175,16 @@ 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/Models/ArticleContent.swift b/apple/OmnivoreKit/Sources/Models/ArticleContent.swift index bc55cf570..032ccf434 100644 --- a/apple/OmnivoreKit/Sources/Models/ArticleContent.swift +++ b/apple/OmnivoreKit/Sources/Models/ArticleContent.swift @@ -1,5 +1,15 @@ import Foundation +public class CachedPageContent: NSObject { + public let slug: String + public let value: ArticleContent + + public init(_ slug: String, _ content: ArticleContent) { + self.slug = slug + self.value = content + } +} + public struct ArticleContent { public let htmlContent: String public let highlights: [Highlight] diff --git a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift index 12deb1da3..12e72e406 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift @@ -1,6 +1,16 @@ +import Combine import Foundation import Models +public class CacheManager: NSObject, NSCacheDelegate { + public func cache(_: NSCache, willEvictObject obj: Any) { + // This is just used for debugging + if let content = obj as? CachedPageContent { + print("evicting page from cache", content.slug) + } + } +} + public final class DataService: ObservableObject { public static var registerIntercomUser: ((String) -> Void)? public static var showIntercomMessenger: (() -> Void)? @@ -9,12 +19,20 @@ public final class DataService: ObservableObject { public internal(set) var currentViewer: Viewer? let networker: Networker + public let pageCache = NSCache() + let pageCacheQueue = DispatchQueue.global(qos: .background) + let highlightsCache = NSCache() let highlightsCacheQueue = DispatchQueue(label: "app.omnivore.highlights.cache.queue", attributes: .concurrent) + let cacheManager: CacheManager + var subscriptions = Set() + public init(appEnvironment: AppEnvironment, networker: Networker) { self.appEnvironment = appEnvironment self.networker = networker + self.cacheManager = CacheManager() + pageCache.delegate = cacheManager } public func clearHighlights() { @@ -30,3 +48,37 @@ public final class DataService: ObservableObject { } } } + +public extension DataService { + func prefetchPages(items: [FeedItem]) { + print("prefetching pages") + guard let viewer = currentViewer else { return } + + for item in items { + let slug = item.slug + articleContentPublisher(username: viewer.username, slug: slug).sink( + receiveCompletion: { _ in }, + receiveValue: { [weak self] articleContent in + self?.pageCache.setObject(CachedPageContent(slug, articleContent), forKey: NSString(string: slug)) + } + ) + .store(in: &subscriptions) + } + } + + func pageFromCache(slug: String) -> ArticleContent? { + if let content = pageCache.object(forKey: NSString(string: slug)) { + print("cache hit", slug) + return content.value + } else { + print("cache miss", slug) + } + return nil + } + + func invalidateCachedPage(slug: String?) { + if let slug = slug { + pageCache.removeObject(forKey: NSString(string: slug)) + } + } +}