diff --git a/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift b/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift index d9b3c5e48..9512dc7d0 100644 --- a/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift @@ -18,9 +18,9 @@ public final class PDFViewerViewModel: ObservableObject { } public func loadHighlights(completion onComplete: @escaping ([Highlight]) -> Void) { - guard let viewer = services.dataService.currentViewer else { return } + guard let username = services.dataService.currentViewer?.username else { return } - services.dataService.pdfHighlightsPublisher(username: viewer.username, slug: feedItem.slug).sink( + services.dataService.pdfHighlightsPublisher(username: username, slug: feedItem.slug).sink( receiveCompletion: { [weak self] completion in guard case .failure = completion else { return } onComplete(self?.allHighlights(fetchedHighlights: []) ?? []) diff --git a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift index 6ae0f7ad9..4d677e53f 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift @@ -56,7 +56,7 @@ enum PDFProvider { if let viewer = viewer { createWebAppWrapperViewModel( - username: viewer.username, + username: viewer.username ?? "", dataService: dataService, rawAuthCookie: rawAuthCookie ) diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift index aab2f65ab..84dd050e9 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift @@ -21,8 +21,8 @@ import Views guard let viewer = try? await dataService.fetchViewer() else { return } profileCardData = ProfileCardData( - name: viewer.name, - username: viewer.username, + name: viewer.name ?? "", + username: viewer.username ?? "", imageURL: viewer.profileImageURL.flatMap { URL(string: $0) } ) } diff --git a/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift index a8afdf799..9def7c1df 100644 --- a/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift @@ -64,8 +64,8 @@ public final class RootViewModel: ObservableObject { return } - if let viewer = try? await services.dataService.fetchViewer() { - let path = linkRequestPath(username: viewer.username, requestID: linkRequestID) + if let username = try? await services.dataService.fetchViewer().username { + let path = linkRequestPath(username: username, requestID: linkRequestID) webLinkPath = SafariWebLinkPath(id: UUID(), path: path) } } diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift index 081311164..4b7b8f855 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift @@ -25,14 +25,14 @@ final class WebReaderViewModel: ObservableObject { self.slug = slug isLoading = true - guard let viewer = dataService.currentViewer else { return } + guard let username = dataService.currentViewer?.username 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( + dataService.articleContentPublisher(username: username, slug: slug).sink( receiveCompletion: { [weak self] completion in guard case .failure = completion else { return } self?.isLoading = false diff --git a/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents b/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents index 449b5cd29..655f7476c 100644 --- a/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents +++ b/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents @@ -64,10 +64,22 @@ + + + + + + + + + + + - - + + + \ No newline at end of file diff --git a/apple/OmnivoreKit/Sources/Models/CoreData/StorageProvider.swift b/apple/OmnivoreKit/Sources/Models/CoreData/StorageProvider.swift index 1f42124dd..8641865ef 100644 --- a/apple/OmnivoreKit/Sources/Models/CoreData/StorageProvider.swift +++ b/apple/OmnivoreKit/Sources/Models/CoreData/StorageProvider.swift @@ -7,6 +7,14 @@ public class PersistentContainer: NSPersistentContainer { public static func make() -> PersistentContainer { let modelURL = Bundle.module.url(forResource: "CoreDataModel", withExtension: "momd")! let model = NSManagedObjectModel(contentsOf: modelURL)! - return PersistentContainer(name: "DataModel", managedObjectModel: model) + let container = PersistentContainer(name: "DataModel", managedObjectModel: model) + + container.viewContext.automaticallyMergesChangesFromParent = false + container.viewContext.name = "viewContext" + container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + container.viewContext.undoManager = nil + container.viewContext.shouldDeleteInaccessibleFaults = true + + return container } } diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/UserProfile.swift b/apple/OmnivoreKit/Sources/Models/DataModels/UserProfile.swift index d3588df5c..85388588b 100644 --- a/apple/OmnivoreKit/Sources/Models/DataModels/UserProfile.swift +++ b/apple/OmnivoreKit/Sources/Models/DataModels/UserProfile.swift @@ -44,22 +44,3 @@ public extension UserProfile { return nil } } - -public struct Viewer { - public let userID: String - public let username: String - public let name: String - public let profileImageURL: String? - - public init( - username: String, - name: String, - profileImageURL: String?, - userID: String - ) { - self.username = username - self.name = name - self.profileImageURL = profileImageURL - self.userID = userID - } -} diff --git a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift index 83fa20999..a3bfe05d2 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift @@ -2,14 +2,15 @@ import Combine import CoreData import Foundation import Models +import OSLog public final class DataService: ObservableObject { public static var registerIntercomUser: ((String) -> Void)? public static var showIntercomMessenger: (() -> Void)? public let appEnvironment: AppEnvironment - public internal(set) var currentViewer: Viewer? let networker: Networker + static let logger = Logger(subsystem: "app.omnivore", category: "data-service") let persistentContainer: PersistentContainer var subscriptions = Set() @@ -27,6 +28,11 @@ public final class DataService: ObservableObject { } } + public var currentViewer: Viewer? { + let fetchRequest: NSFetchRequest = Viewer.fetchRequest() + return try? persistentContainer.viewContext.fetch(fetchRequest).first + } + public func clearHighlights() { deletedHighlightsIDs.removeAll() @@ -41,7 +47,7 @@ public final class DataService: ObservableObject { do { try persistentContainer.viewContext.save() } catch { - print("failed to delete objects") + DataService.logger.debug("failed to delete objects") } } @@ -57,11 +63,11 @@ public final class DataService: ObservableObject { public extension DataService { func prefetchPages(items: [FeedItem]) { - guard let viewer = currentViewer else { return } + guard let username = currentViewer?.username else { return } for item in items { let slug = item.slug - articleContentPublisher(username: viewer.username, slug: slug).sink( + articleContentPublisher(username: username, slug: slug).sink( receiveCompletion: { _ in }, receiveValue: { _ in } ) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LibraryItemsQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LibraryItemsQuery.swift index f2aaa530d..d6f31e93c 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LibraryItemsQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LibraryItemsQuery.swift @@ -6,7 +6,7 @@ import SwiftGraphQL public extension DataService { func articlePublisher(slug: String) -> AnyPublisher { internalViewerPublisher() - .flatMap { self.internalArticlePublisher(username: $0.username, slug: slug) } + .flatMap { self.internalArticlePublisher(username: $0.username ?? "", slug: slug) } .receive(on: DispatchQueue.main) .eraseToAnyPublisher() } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ViewerFetcher.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ViewerFetcher.swift index 509ff01a7..f526c23f3 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ViewerFetcher.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ViewerFetcher.swift @@ -1,4 +1,5 @@ import Combine +import CoreData import Foundation import Models import SwiftGraphQL @@ -6,16 +7,16 @@ import Utils public extension DataService { func fetchViewer() async throws -> Viewer { - let selection = Selection { - Viewer( + let selection = Selection { + ViewerInternal( + userID: try $0.id(), username: try $0.profile( selection: .init { try $0.username() } ), name: try $0.name(), profileImageURL: try $0.profile( selection: .init { try $0.pictureUrl() } - ), - userID: try $0.id() + ) ) } @@ -30,12 +31,16 @@ public extension DataService { send(query, to: path, headers: headers) { [weak self] result in switch result { case let .success(payload): - self?.currentViewer = payload.data if UserDefaults.standard.string(forKey: Keys.userIdKey) == nil { UserDefaults.standard.setValue(payload.data.userID, forKey: Keys.userIdKey) DataService.registerIntercomUser?(payload.data.userID) } - continuation.resume(returning: payload.data) + + if let self = self, let viewer = payload.data.persist(context: self.persistentContainer.viewContext) { + continuation.resume(returning: viewer) + } else { + continuation.resume(throwing: BasicError.message(messageText: "coredata error")) + } case .failure: continuation.resume(throwing: BasicError.message(messageText: "http error")) } @@ -47,16 +52,16 @@ public extension DataService { extension DataService { @available(*, deprecated, message: "use async version instead") func internalViewerPublisher() -> AnyPublisher { - let selection = Selection { - Viewer( + let selection = Selection { + ViewerInternal( + userID: try $0.id(), username: try $0.profile( selection: .init { try $0.username() } ), name: try $0.name(), profileImageURL: try $0.profile( selection: .init { try $0.pictureUrl() } - ), - userID: try $0.id() + ) ) } @@ -72,8 +77,11 @@ extension DataService { send(query, to: path, headers: headers) { result in switch result { case let .success(payload): - self?.currentViewer = payload.data - promise(.success(payload.data)) + if let self = self, let viewer = payload.data.persist(context: self.persistentContainer.viewContext) { + promise(.success(viewer)) + } else { + promise(.failure(.message(messageText: "coredata error"))) + } case .failure: promise(.failure(.message(messageText: "http error"))) } @@ -83,3 +91,28 @@ extension DataService { .eraseToAnyPublisher() } } + +private struct ViewerInternal { + let userID: String + let username: String + let name: String + let profileImageURL: String? + + func persist(context: NSManagedObjectContext) -> Viewer? { + let viewer = Viewer(context: context) + viewer.userID = userID + viewer.username = username + viewer.name = name + viewer.profileImageURL = profileImageURL + + do { + try context.save() + DataService.logger.debug("Viewer saved succesfully") + return viewer + } catch { + context.rollback() + DataService.logger.debug("Failed to save Viewer: \(error.localizedDescription)") + return nil + } + } +}