move article content fetching into fewer functions

This commit is contained in:
Satindar Dhillon 2022-04-27 20:55:57 -07:00
parent fb4312e52a
commit 0e8d5f23da
6 changed files with 67 additions and 106 deletions

View file

@ -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<AnyCancellable>()
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<Models.LinkedItem> = LinkedItem.fetchRequest()

View file

@ -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 {

View file

@ -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<AnyCancellable>()
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)

View file

@ -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<Models.LinkedItem> = 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?) {}
}

View file

@ -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<ArticleContent, ServerError> {
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<Models.LinkedItem> = LinkedItem.fetchRequest()
@ -101,4 +107,24 @@ extension DataService {
}
}
}
func cachedArticleContent(slug: String) -> ArticleContent? {
let linkedItemFetchRequest: NSFetchRequest<Models.LinkedItem> = 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
)
}
}

View file

@ -1,4 +1,3 @@
import Combine
import CoreData
import Foundation
import Models