Files
omnivore/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift

135 lines
4.3 KiB
Swift

import CoreData
import Models
import Services
import SwiftUI
import Utils
#if os(iOS)
@MainActor final class WebReaderLoadingContainerViewModel: ObservableObject {
@Published var item: LinkedItem?
@Published var errorMessage: String?
func loadItem(dataService: DataService, requestID: String) async {
let username: String? = await {
if let cachedUsername = dataService.currentViewer?.username {
return cachedUsername
}
if let viewerObjectID = try? await dataService.fetchViewer() {
let viewer = dataService.viewContext.object(with: viewerObjectID) as? Viewer
return viewer?.unwrappedUsername
}
return nil
}()
guard let username = username else { return }
let existing = existingItemOrItemId(dataService: dataService, requestID: requestID)
if let existingItem = existing.existingItem {
item = existingItem
return
}
// If the page was locally created, make sure they are synced before we pull content
await dataService.syncUnsyncedArticleContent(itemID: existing.itemID)
await fetchLinkedItem(dataService: dataService, requestID: existing.itemID, username: username)
}
private func fetchLinkedItem(
dataService: DataService,
requestID: String,
username: String,
requestCount: Int = 1
) async {
guard requestCount < 7 else {
errorMessage = "Unable to fetch item."
return
}
if let objectID = try? await dataService.fetchLinkedItem(username: username, itemID: requestID) {
if let linkedItem = dataService.viewContext.object(with: objectID) as? LinkedItem {
item = linkedItem
} else {
errorMessage = "Unable to fetch item."
}
return
}
// Retry on error
do {
let retryDelayInNanoSeconds = UInt64(requestCount * 2 * 1_000_000_000)
try await Task.sleep(nanoseconds: retryDelayInNanoSeconds)
let existing = existingItemOrItemId(dataService: dataService, requestID: requestID)
if let existingItem = existing.existingItem {
item = existingItem
return
}
await fetchLinkedItem(
dataService: dataService,
requestID: existing.itemID,
username: username,
requestCount: requestCount + 1
)
} catch {
errorMessage = "Unable to fetch item."
}
}
private func existingItemOrItemId(dataService: DataService, requestID: String) -> (existingItem: LinkedItem?, itemID: String) {
let fetchRequest: NSFetchRequest<Models.LinkedItem> = LinkedItem.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "createdId == %@ OR id == %@", requestID, requestID)
if let existingItem = try? dataService.viewContext.fetch(fetchRequest).first {
// If the existing item is synced, we can use it
if let itemID = existingItem.id, existingItem.serverSyncStatus == ServerSyncStatus.isNSync.rawValue {
item = existingItem
return (existingItem: item, itemID: itemID)
}
// If the existing item is not synced, we might have an updated request id
if let existingID = existingItem.id {
return (existingItem: nil, itemID: existingID)
}
}
return (existingItem: nil, itemID: requestID)
}
func trackReadEvent() {
guard let item = item else { return }
EventTracker.track(
.linkRead(
linkID: item.unwrappedID,
slug: item.unwrappedSlug,
originalArticleURL: item.unwrappedPageURLString
)
)
}
}
public struct WebReaderLoadingContainer: View {
let requestID: String
@EnvironmentObject var dataService: DataService
@StateObject var viewModel = WebReaderLoadingContainerViewModel()
public var body: some View {
if let item = viewModel.item {
WebReaderContainerView(item: item)
.navigationBarHidden(true)
.navigationViewStyle(.stack)
.accentColor(.appGrayTextContrast)
.task { viewModel.trackReadEvent() }
} else if let errorMessage = viewModel.errorMessage {
Text(errorMessage)
} else {
ProgressView()
.task { await viewModel.loadItem(dataService: dataService, requestID: requestID) }
}
}
}
#endif