From 01632c531701aa1c34154b82fe8375de026c6381 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 15 Jun 2022 16:48:14 -0700 Subject: [PATCH 01/13] lint fixes --- .../Queries/ArticleContentQuery.swift | 62 ++++++++++--------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift index 842242e05..be661d0e3 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift @@ -4,21 +4,20 @@ import Models import SwiftGraphQL import Utils -public extension DataService { - internal struct PendingLink { +extension DataService { + struct PendingLink { let itemID: String let retryCount: Int } - func prefetchPages(itemIDs: [String], username: String) async { + public func prefetchPages(itemIDs: [String], username: String) async { // TODO: make this concurrent - // TODO: make a non-pending page option for BG tasks for itemID in itemIDs { await prefetchPage(pendingLink: PendingLink(itemID: itemID, retryCount: 1), username: username) } } - internal func prefetchPage(pendingLink: PendingLink, username: String) async { + func prefetchPage(pendingLink: PendingLink, username: String) async { let content = try? await articleContent(username: username, itemID: pendingLink.itemID, useCache: false) if content?.contentStatus == .processing, pendingLink.retryCount < 7 { @@ -41,7 +40,7 @@ public extension DataService { } } - func fetchArticleContent( + public func fetchArticleContent( itemID: String, username: String? = nil, requestCount: Int = 1 @@ -154,8 +153,6 @@ public extension DataService { switch payload.data { case let .success(result: result): - // Default to suceeded since older links will return a nil status - // (but the content is almost always there) let status = result.contentStatus ?? .succeeded if status == .failed { continuation.resume(throwing: ContentFetchError.badData) @@ -195,10 +192,15 @@ public extension DataService { return articleContent } - internal func persistArticleContent(item: InternalLinkedItem, htmlContent: String, highlights: [InternalHighlight]) async throws { + // swiftlint:disable:next function_body_length + func persistArticleContent( + item: InternalLinkedItem, + htmlContent: String, + highlights: [InternalHighlight] + ) async throws { var needsPDFDownload = false - try await backgroundContext.perform { [weak self] in + await backgroundContext.perform { [weak self] in guard let self = self else { return } let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() fetchRequest.predicate = NSPredicate(format: "id == %@", item.id) @@ -245,7 +247,7 @@ public extension DataService { if let tempPDFURL = existingItem?.tempPDFURL { linkedItem.localPDF = try? PDFUtils.moveToLocal(url: tempPDFURL) - PDFUtils.exists(filename: linkedItem.localPDF) + _ = PDFUtils.exists(filename: linkedItem.localPDF) if linkedItem.localPDF != nil { needsPDFDownload = false } @@ -254,7 +256,7 @@ public extension DataService { } if item.isPDF, needsPDFDownload { - try await fetchPDFData(slug: item.slug, pageURLString: item.pageURLString) + _ = try await fetchPDFData(slug: item.slug, pageURLString: item.pageURLString) } try await backgroundContext.perform { [weak self] in @@ -269,7 +271,7 @@ public extension DataService { } } - func fetchPDFData(slug: String, pageURLString: String) async throws -> URL? { + public func fetchPDFData(slug: String, pageURLString: String) async throws -> URL? { guard let url = URL(string: pageURLString) else { throw BasicError.message(messageText: "No PDF URL found") } @@ -313,7 +315,7 @@ public extension DataService { return localPdfURL } - internal func cachedArticleContent(itemID: String) async -> ArticleContent? { + func cachedArticleContent(itemID: String) async -> ArticleContent? { let linkedItemFetchRequest: NSFetchRequest = LinkedItem.fetchRequest() linkedItemFetchRequest.predicate = NSPredicate( format: "id == %@", itemID @@ -339,7 +341,7 @@ public extension DataService { } } - func syncUnsyncedArticleContent(itemID: String) async { + public func syncUnsyncedArticleContent(itemID: String) async { let linkedItemFetchRequest: NSFetchRequest = LinkedItem.fetchRequest() linkedItemFetchRequest.predicate = NSPredicate( format: "id == %@", itemID @@ -362,21 +364,23 @@ public extension DataService { serverSyncStatus = linkedItem.serverSyncStatus } - if let id = id, let url = url, let title = title, - let serverSyncStatus = serverSyncStatus, - serverSyncStatus == ServerSyncStatus.needsCreation.rawValue - { - do { - if let originalHtml = originalHtml { - try await savePage(id: id, url: url, title: title, originalHtml: originalHtml) - } else { - try await saveURL(id: id, url: url) - } - } catch { - // We don't propogate these errors, we just let it pass through so - // the user can attempt to fetch content again. - print("Error syncUnsyncedArticleContent") + guard let id = id, let url = url, let title = title, + let serverSyncStatus = serverSyncStatus, + serverSyncStatus == ServerSyncStatus.needsCreation.rawValue + else { + return + } + + do { + if let originalHtml = originalHtml { + _ = try await savePage(id: id, url: url, title: title, originalHtml: originalHtml) + } else { + _ = try await saveURL(id: id, url: url) } + } catch { + // We don't propogate these errors, we just let it pass through so + // the user can attempt to fetch content again. + print("Error syncUnsyncedArticleContent") } } } From 96b09fec8e04b476162a8e2176c0f259e7624417 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 16 Jun 2022 11:04:15 -0700 Subject: [PATCH 02/13] separate linkeditem network fetches from persistence functions --- .../App/Views/Home/HomeFeedViewModel.swift | 4 +- .../WebReader/WebReaderLoadingContainer.swift | 5 +- .../Sources/Models/DataModels/FeedItem.swift | 8 +- .../Public/LinkedItemLoading.swift | 43 +++++++++ ...ery.swift => LinkedItemNetworkQuery.swift} | 91 +++++++------------ .../InternalModels/InternalLinkedItem.swift | 9 +- 6 files changed, 89 insertions(+), 71 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift rename apple/OmnivoreKit/Sources/Services/DataService/Queries/{LibraryItemsQuery.swift => LinkedItemNetworkQuery.swift} (59%) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 2d104d0ee..837b41376 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -62,7 +62,7 @@ import Views Task { _ = try? await dataService.fetchViewer() } } - let queryResult = try? await dataService.fetchLinkedItems( + let queryResult = try? await dataService.loadLinkedItems( limit: 10, searchQuery: searchQuery, cursor: isRefresh ? nil : cursor @@ -81,7 +81,7 @@ import Views let newItems: [LinkedItem] = { var itemObjects = [LinkedItem]() dataService.viewContext.performAndWait { - itemObjects = queryResult.items.compactMap { dataService.viewContext.object(with: $0) as? LinkedItem } + itemObjects = queryResult.itemIDs.compactMap { dataService.viewContext.object(with: $0) as? LinkedItem } } return itemObjects }() diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift index adb28284f..45065ca63 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift @@ -64,11 +64,8 @@ import Utils return nil } - print("FETCHING", requestID, requestCount) - - if let objectID = try? await dataService.fetchLinkedItem(username: username, itemID: requestID) { + if let objectID = try? await dataService.loadLinkedItem(username: username, itemID: requestID) { if let linkedItem = dataService.viewContext.object(with: objectID) as? LinkedItem { - print(" - FROM DATA SERVICE", linkedItem) return linkedItem } else { errorMessage = "Unable to fetch item." diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift b/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift index 09b375545..765c9f329 100644 --- a/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift +++ b/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift @@ -2,12 +2,12 @@ import CoreData import Foundation import Utils -public struct HomeFeedData { // TODO: rename this - public let items: [NSManagedObjectID] +public struct LinkedItemQueryResult { + public let itemIDs: [NSManagedObjectID] public let cursor: String? - public init(items: [NSManagedObjectID], cursor: String?) { - self.items = items + public init(itemIDs: [NSManagedObjectID], cursor: String?) { + self.itemIDs = itemIDs self.cursor = cursor } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift b/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift new file mode 100644 index 000000000..b3db826aa --- /dev/null +++ b/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift @@ -0,0 +1,43 @@ +import CoreData +import Foundation +import Models + +public extension DataService { + /// Requests `LinkedItem`s from the server and stores it in CoreData. + /// - Parameters: + /// - limit: max count of items + /// - searchQuery: search terms and filters + /// - cursor: cursor when loading batch for infinite list + /// - Returns: `LinkedItemQueryResult` (managed object IDs and an optional cursor) + func loadLinkedItems( + limit: Int, + searchQuery: String?, + cursor: String? + ) async throws -> LinkedItemQueryResult { + // Send offline changes to server before fetching items + try? await syncOfflineItemsWithServerIfNeeded() + + let fetchResult = try await fetchLinkedItems(limit: limit, searchQuery: searchQuery, cursor: cursor) + + guard let itemIDs = fetchResult.items.persist(context: backgroundContext) else { + throw BasicError.message(messageText: "CoreData error") + } + + return LinkedItemQueryResult(itemIDs: itemIDs, cursor: fetchResult.cursor) + } + + /// Requests a single `LinkedItem` from the server and stores it in CoreData + /// - Parameters: + /// - username: the Viewer's username + /// - itemID: id of item being requested + /// - Returns: The `NSManagedObjectID` of the `LinkedItem` + func loadLinkedItem(username: String, itemID: String) async throws -> NSManagedObjectID { + let item = try await fetchLinkedItem(username: username, itemID: itemID) + + guard let persistedItemID = [item].persist(context: backgroundContext)?.first else { + throw BasicError.message(messageText: "CoreData error") + } + + return persistedItemID + } +} diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LibraryItemsQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift similarity index 59% rename from apple/OmnivoreKit/Sources/Services/DataService/Queries/LibraryItemsQuery.swift rename to apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift index 99c9fcece..e5f614589 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LibraryItemsQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift @@ -3,22 +3,25 @@ import Foundation import Models import SwiftGraphQL -public extension DataService { +struct InternalLinkedItemQueryResult { + let items: [InternalLinkedItem] + let cursor: String? +} + +extension DataService { + /// Performs GraphQL request to fetch `InternalLinkedItem`s and a cursor value + /// - Parameters: + /// - limit: max number of items to return + /// - searchQuery: search query used by server to narrow search + /// - cursor: cursor to indicate batch cutoff + /// - Returns: `InternalLinkedItemQueryResult` or a `ContentFetchError` if request fails. func fetchLinkedItems( limit: Int, searchQuery: String?, cursor: String? - ) async throws -> HomeFeedData { - // Send offline changes to server before fetching items - try? await syncOfflineItemsWithServerIfNeeded() - - struct InternalHomeFeedData { - let items: [InternalLinkedItem] - let cursor: String? - } - + ) async throws -> InternalLinkedItemQueryResult { enum QueryResult { - case success(result: InternalHomeFeedData) + case success(result: InternalLinkedItemQueryResult) case error(error: String) } @@ -29,7 +32,7 @@ public extension DataService { }, articlesSuccess: .init { QueryResult.success( - result: InternalHomeFeedData( + result: InternalLinkedItemQueryResult( items: try $0.edges(selection: articleEdgeSelection.list), cursor: try $0.pageInfo(selection: Selection.PageInfo { try $0.endCursor() @@ -61,27 +64,29 @@ public extension DataService { let headers = networker.defaultHeaders return try await withCheckedThrowingContinuation { continuation in - send(query, to: path, headers: headers) { [weak self] queryResult in + send(query, to: path, headers: headers) { queryResult in guard let payload = try? queryResult.get() else { - continuation.resume(throwing: BasicError.message(messageText: "network error")) + continuation.resume(throwing: ContentFetchError.network) return } switch payload.data { case let .success(result: result): - if let context = self?.backgroundContext, let items = result.items.persist(context: context) { - continuation.resume(returning: HomeFeedData(items: items.map(\.objectID), cursor: result.cursor)) - } else { - continuation.resume(throwing: BasicError.message(messageText: "CoreData error")) - } - case .error: - continuation.resume(throwing: BasicError.message(messageText: "LinkedItem fetch error")) + continuation.resume(returning: result) + case let .error(error): + continuation.resume(throwing: ContentFetchError.unknown(description: error.description)) } } } } - func fetchLinkedItem(username: String, itemID: String) async throws -> NSManagedObjectID { + /// Performs GraphQL request to fetch a single `InternalLinkedItem` + /// - Parameters: + /// - username: the Viewer's username + /// - itemID: id of the item being requested + /// - Returns: Returns an `InternalLinkedItem` or throws a `ContentFetchError` if + /// request could not be completed + func fetchLinkedItem(username: String, itemID: String) async throws -> InternalLinkedItem { struct ArticleProps { let item: InternalLinkedItem } @@ -91,41 +96,13 @@ public extension DataService { case error(error: String) } - let articleSelection = Selection.Article { - InternalLinkedItem( - id: try $0.id(), - title: try $0.title(), - createdAt: try $0.createdAt().value ?? Date(), - savedAt: try $0.savedAt().value ?? Date(), - readAt: try $0.readAt()?.value, - updatedAt: try $0.updatedAt().value ?? Date(), - state: try $0.state()?.rawValue ?? "SUCCEEDED", - readingProgress: try $0.readingProgressPercent(), - readingProgressAnchor: try $0.readingProgressAnchorIndex(), - imageURLString: try $0.image(), - onDeviceImageURLString: nil, - documentDirectoryPath: nil, - pageURLString: try $0.url(), - descriptionText: try $0.description(), - publisherURLString: try $0.originalArticleUrl(), - siteName: try $0.siteName(), - author: try $0.author(), - publishDate: try $0.publishedAt()?.value, - slug: try $0.slug(), - isArchived: try $0.isArchived(), - contentReader: try $0.contentReader().rawValue, - originalHtml: nil, - labels: try $0.labels(selection: feedItemLabelSelection.list.nullable) ?? [] - ) - } - let selection = Selection { try $0.on( articleError: .init { QueryResult.error(error: try $0.errorCodes().description) }, articleSuccess: .init { - QueryResult.success(result: try $0.article(selection: articleSelection)) + QueryResult.success(result: try $0.article(selection: libraryArticleSelection)) } ) } @@ -139,20 +116,16 @@ public extension DataService { let headers = networker.defaultHeaders return try await withCheckedThrowingContinuation { continuation in - send(query, to: path, headers: headers) { [weak self] queryResult in + send(query, to: path, headers: headers) { queryResult in guard let payload = try? queryResult.get() else { continuation.resume(throwing: ContentFetchError.network) return } switch payload.data { case let .success(result: result): - if let context = self?.backgroundContext, let item = [result].persist(context: context)?.first { - continuation.resume(returning: item.objectID) - } else { - continuation.resume(throwing: BasicError.message(messageText: "CoreData error")) - } - case .error: - continuation.resume(throwing: BasicError.message(messageText: "LinkedItem fetch error")) + continuation.resume(returning: result) + case let .error(error): + continuation.resume(throwing: ContentFetchError.unknown(description: error.description)) } } } diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift index dde255691..115f6aa03 100644 --- a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift +++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift @@ -70,7 +70,7 @@ struct InternalLinkedItem { } extension Sequence where Element == InternalLinkedItem { - func persist(context: NSManagedObjectContext) -> [LinkedItem]? { + func persist(context: NSManagedObjectContext) -> [NSManagedObjectID]? { var linkedItems: [LinkedItem]? context.performAndWait { linkedItems = map { $0.asManagedObject(inContext: context) } @@ -83,7 +83,12 @@ extension Sequence where Element == InternalLinkedItem { print("Failed to save LinkedItems: \(error.localizedDescription)") } } - return linkedItems + + if let linkedItems = linkedItems { + return linkedItems.map(\.objectID) + } else { + return nil + } } } From ffcd43060ca594be3e75955b6dfad358ca55efdb Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 16 Jun 2022 14:58:05 -0700 Subject: [PATCH 03/13] separate article content fetch gql call from persistence code --- .../WebReader/WebReaderLoadingContainer.swift | 2 +- .../Views/WebReader/WebReaderViewModel.swift | 2 +- .../Models/DataModels/ArticleContent.swift | 10 +- .../FetchLinkedItemsBackgroundTask.swift | 2 +- .../Public/LinkedItemContentLoading.swift | 298 ++++++++++++++++ .../Public/LinkedItemLoading.swift | 2 +- .../Queries/ArticleContentQuery.swift | 326 +----------------- 7 files changed, 320 insertions(+), 322 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemContentLoading.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift index 45065ca63..90471c821 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift @@ -38,7 +38,7 @@ import Utils let item = await fetchLinkedItem(dataService: dataService, requestID: existing.itemID, username: username) if let item = item, let itemID = item.id { do { - let articleContent = try await dataService.fetchArticleContent(itemID: itemID, username: username, requestCount: 0) + let articleContent = try await dataService.loadArticleContent(itemID: itemID, username: username, requestCount: 0) // We've fetched the article content, now reload the item from core data if let linkedItem = dataService.viewContext.object(with: item.objectID) as? LinkedItem { self.item = linkedItem diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift index 8255a1289..3f9419956 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift @@ -16,7 +16,7 @@ struct SafariWebLink: Identifiable { errorMessage = nil do { - articleContent = try await dataService.fetchArticleContent(itemID: itemID) + articleContent = try await dataService.loadArticleContent(itemID: itemID) } catch { if retryCount == 0 { return await loadContent(dataService: dataService, itemID: itemID, retryCount: 1) diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/ArticleContent.swift b/apple/OmnivoreKit/Sources/Models/DataModels/ArticleContent.swift index 0242712e4..0d1e9fc3f 100644 --- a/apple/OmnivoreKit/Sources/Models/DataModels/ArticleContent.swift +++ b/apple/OmnivoreKit/Sources/Models/DataModels/ArticleContent.swift @@ -1,10 +1,10 @@ import Foundation -public enum ArticleContentStatus { - case failed - case processing - case succeeded - case unknown +public enum ArticleContentStatus: String { + case failed = "FAILED" + case processing = "PROCESSING" + case succeeded = "SUCCEEDED" + case unknown = "UNKNOWN" } public struct ArticleContent { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/FetchLinkedItemsBackgroundTask.swift b/apple/OmnivoreKit/Sources/Services/DataService/FetchLinkedItemsBackgroundTask.swift index c3b407682..af354f3e4 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/FetchLinkedItemsBackgroundTask.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/FetchLinkedItemsBackgroundTask.swift @@ -25,7 +25,7 @@ extension DataService { // Fetch the items for itemID in missingItemIds { // TOOD: run these in parallel logger.debug("fetching item with ID: \(itemID)") - _ = try await articleContent(username: username, itemID: itemID, useCache: false) + _ = try await loadArticleContent(username: username, itemID: itemID, useCache: false) fetchedItemCount += 1 logger.debug("done fetching item with ID: \(itemID)") } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemContentLoading.swift b/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemContentLoading.swift new file mode 100644 index 000000000..891f06a25 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemContentLoading.swift @@ -0,0 +1,298 @@ +import CoreData +import Foundation +import Models +import SwiftGraphQL +import Utils + +extension DataService { + struct PendingLink { + let itemID: String + let retryCount: Int + } + + public func prefetchPages(itemIDs: [String], username: String) async { + // TODO: make this concurrent + for itemID in itemIDs { + await prefetchPage(pendingLink: PendingLink(itemID: itemID, retryCount: 1), username: username) + } + } + + func prefetchPage(pendingLink: PendingLink, username: String) async { + let content = try? await loadArticleContent(username: username, itemID: pendingLink.itemID, useCache: false) + + if content?.contentStatus == .processing, pendingLink.retryCount < 7 { + let retryDelayInNanoSeconds = UInt64(pendingLink.retryCount * 2 * 1_000_000_000) + + do { + try await Task.sleep(nanoseconds: retryDelayInNanoSeconds) + logger.debug("fetching content for \(pendingLink.itemID). retry count: \(pendingLink.retryCount)") + + await prefetchPage( + pendingLink: PendingLink( + itemID: pendingLink.itemID, + retryCount: pendingLink.retryCount + 1 + ), + username: username + ) + } catch { + logger.debug("prefetching task was cancelled") + } + } + } + + public func loadArticleContent( + itemID: String, + username: String? = nil, + requestCount: Int = 1 + ) async throws -> ArticleContent { + guard requestCount < 7 else { + throw ContentFetchError.badData + } + + guard let username = username ?? currentViewer?.username else { + throw ContentFetchError.unauthorized + } + + let fetchedContent = try await loadArticleContent(username: username, itemID: itemID, useCache: true) + + switch fetchedContent.contentStatus { + case .failed: + throw ContentFetchError.badData + case .processing: + let retryDelayInNanoSeconds = UInt64(requestCount * 2 * 1_000_000_000) + try await Task.sleep(nanoseconds: retryDelayInNanoSeconds) + logger.debug("fetching content for \(itemID). request count: \(requestCount)") + return try await loadArticleContent(itemID: itemID, username: username, requestCount: requestCount + 1) + case .succeeded, .unknown: + return fetchedContent + } + } + + func loadArticleContent(username: String, itemID: String, useCache: Bool) async throws -> ArticleContent { + if useCache, let cachedContent = await cachedArticleContent(itemID: itemID) { + return cachedContent + } + + // If the page was locally created, make sure they are synced before we pull content + await syncUnsyncedArticleContent(itemID: itemID) + + let fetchResult = try await articleContentFetch(username: username, itemID: itemID) + + let articleContent = ArticleContent( + title: fetchResult.item.title, + htmlContent: fetchResult.htmlContent, + highlightsJSONString: fetchResult.highlights.asJSONString, + contentStatus: fetchResult.item.isPDF ? .succeeded : .make(from: fetchResult.item.state) + ) + + if articleContent.contentStatus == .succeeded { + do { + try await persistArticleContent(articleProps: fetchResult) + } catch { + var message = "unknown error" + let basicError = (error as? BasicError) ?? BasicError.message(messageText: "unknown error") + if case let BasicError.message(messageText) = basicError { + message = messageText + } + throw ContentFetchError.unknown(description: message) + } + } + + return articleContent + } + + // swiftlint:disable:next function_body_length + func persistArticleContent(articleProps: ArticleProps) async throws { + var needsPDFDownload = false + + await backgroundContext.perform { [weak self] in + guard let self = self else { return } + let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "id == %@", articleProps.item.id) + + let existingItem = try? self.backgroundContext.fetch(fetchRequest).first + let linkedItem = existingItem ?? LinkedItem(entity: LinkedItem.entity(), insertInto: self.backgroundContext) + + let highlightObjects = articleProps.highlights.map { + $0.asManagedObject(context: self.backgroundContext) + } + linkedItem.addToHighlights(NSSet(array: highlightObjects)) + linkedItem.htmlContent = articleProps.htmlContent + linkedItem.id = articleProps.item.id + linkedItem.state = articleProps.item.state + linkedItem.title = articleProps.item.title + linkedItem.createdAt = articleProps.item.createdAt + linkedItem.savedAt = articleProps.item.savedAt + linkedItem.readingProgress = articleProps.item.readingProgress + linkedItem.readingProgressAnchor = Int64(articleProps.item.readingProgressAnchor) + linkedItem.imageURLString = articleProps.item.imageURLString + linkedItem.onDeviceImageURLString = articleProps.item.onDeviceImageURLString + linkedItem.pageURLString = articleProps.item.pageURLString + linkedItem.descriptionText = articleProps.item.descriptionText + linkedItem.publisherURLString = articleProps.item.publisherURLString + linkedItem.author = articleProps.item.author + linkedItem.publishDate = articleProps.item.publishDate + linkedItem.slug = articleProps.item.slug + linkedItem.readAt = articleProps.item.readAt + linkedItem.isArchived = articleProps.item.isArchived + linkedItem.contentReader = articleProps.item.contentReader + linkedItem.serverSyncStatus = Int64(ServerSyncStatus.isNSync.rawValue) + + if articleProps.item.isPDF { + needsPDFDownload = true + + // Check if we already have the PDF item locally. Either in temporary + // space, or in the documents directory + if let localPDF = existingItem?.localPDF { + if PDFUtils.exists(filename: localPDF) { + linkedItem.localPDF = localPDF + needsPDFDownload = false + } + } + + if let tempPDFURL = existingItem?.tempPDFURL { + linkedItem.localPDF = try? PDFUtils.moveToLocal(url: tempPDFURL) + _ = PDFUtils.exists(filename: linkedItem.localPDF) + if linkedItem.localPDF != nil { + needsPDFDownload = false + } + } + } + } + + if articleProps.item.isPDF, needsPDFDownload { + _ = try await fetchPDFData(slug: articleProps.item.slug, pageURLString: articleProps.item.pageURLString) + } + + try await backgroundContext.perform { [weak self] in + do { + try self?.backgroundContext.save() + logger.debug("ArticleContent saved succesfully") + } catch { + self?.backgroundContext.rollback() + logger.debug("Failed to save ArticleContent") + throw error + } + } + } + + public func fetchPDFData(slug: String, pageURLString: String) async throws -> URL? { + guard let url = URL(string: pageURLString) else { + throw BasicError.message(messageText: "No PDF URL found") + } + let result: (Data, URLResponse)? = try? await URLSession.shared.data(from: url) + guard let httpResponse = result?.1 as? HTTPURLResponse, 200 ..< 300 ~= httpResponse.statusCode else { + throw BasicError.message(messageText: "pdfFetch failed. no response or bad status code.") + } + guard let data = result?.0 else { + throw BasicError.message(messageText: "pdfFetch failed. no data received.") + } + + var localPdfURL: URL? + let tempPath = FileManager.default + .urls(for: .cachesDirectory, in: .userDomainMask)[0] + .appendingPathComponent(UUID().uuidString + ".pdf") + + try await backgroundContext.perform { [weak self] in + let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(LinkedItem.slug), slug) + + let linkedItem = try? self?.backgroundContext.fetch(fetchRequest).first + guard let linkedItem = linkedItem else { + let errorMessage = "pdfFetch failed. could not find LinkedItem from fetch request" + throw BasicError.message(messageText: errorMessage) + } + + do { + try data.write(to: tempPath) + let localPDF = try PDFUtils.moveToLocal(url: tempPath) + localPdfURL = PDFUtils.localPdfURL(filename: localPDF) + linkedItem.tempPDFURL = nil + linkedItem.localPDF = localPDF + try self?.backgroundContext.save() + } catch { + self?.backgroundContext.rollback() + let errorMessage = "pdfFetch failed. core data save failed." + throw BasicError.message(messageText: errorMessage) + } + } + + return localPdfURL + } + + func cachedArticleContent(itemID: String) async -> ArticleContent? { + let linkedItemFetchRequest: NSFetchRequest = LinkedItem.fetchRequest() + linkedItemFetchRequest.predicate = NSPredicate( + format: "id == %@", itemID + ) + + let context = backgroundContext + + return await context.perform(schedule: .immediate) { + guard let linkedItem = try? context.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( + title: linkedItem.unwrappedTitle, + htmlContent: htmlContent, + highlightsJSONString: highlights.map { InternalHighlight.make(from: $0) }.asJSONString, + contentStatus: .succeeded + ) + } + } + + public func syncUnsyncedArticleContent(itemID: String) async { + let linkedItemFetchRequest: NSFetchRequest = LinkedItem.fetchRequest() + linkedItemFetchRequest.predicate = NSPredicate( + format: "id == %@", itemID + ) + + let context = backgroundContext + + var id: String? + var url: String? + var title: String? + var originalHtml: String? + var serverSyncStatus: Int64? + + backgroundContext.performAndWait { + guard let linkedItem = try? context.fetch(linkedItemFetchRequest).first else { return } + id = linkedItem.unwrappedID + url = linkedItem.unwrappedPageURLString + title = linkedItem.unwrappedTitle + originalHtml = linkedItem.originalHtml + serverSyncStatus = linkedItem.serverSyncStatus + } + + guard let id = id, let url = url, let title = title, + let serverSyncStatus = serverSyncStatus, + serverSyncStatus == ServerSyncStatus.needsCreation.rawValue + else { + return + } + + do { + if let originalHtml = originalHtml { + _ = try await savePage(id: id, url: url, title: title, originalHtml: originalHtml) + } else { + _ = try await saveURL(id: id, url: url) + } + } catch { + // We don't propogate these errors, we just let it pass through so + // the user can attempt to fetch content again. + print("Error syncUnsyncedArticleContent") + } + } +} + +private extension ArticleContentStatus { + static func make(from status: String?) -> ArticleContentStatus { + guard let status = status else { return .unknown } + return ArticleContentStatus(rawValue: status) ?? .unknown + } +} diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift b/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift index b3db826aa..62b8826c9 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift @@ -25,7 +25,7 @@ public extension DataService { return LinkedItemQueryResult(itemIDs: itemIDs, cursor: fetchResult.cursor) } - + /// Requests a single `LinkedItem` from the server and stores it in CoreData /// - Parameters: /// - username: the Viewer's username diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift index be661d0e3..6c3bd16ec 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift @@ -4,90 +4,15 @@ import Models import SwiftGraphQL import Utils +struct ArticleProps { + let item: InternalLinkedItem + let htmlContent: String + let highlights: [InternalHighlight] + let contentStatus: ArticleContentStatus? // TODO: remove this? +} + extension DataService { - struct PendingLink { - let itemID: String - let retryCount: Int - } - - public func prefetchPages(itemIDs: [String], username: String) async { - // TODO: make this concurrent - for itemID in itemIDs { - await prefetchPage(pendingLink: PendingLink(itemID: itemID, retryCount: 1), username: username) - } - } - - func prefetchPage(pendingLink: PendingLink, username: String) async { - let content = try? await articleContent(username: username, itemID: pendingLink.itemID, useCache: false) - - if content?.contentStatus == .processing, pendingLink.retryCount < 7 { - let retryDelayInNanoSeconds = UInt64(pendingLink.retryCount * 2 * 1_000_000_000) - - do { - try await Task.sleep(nanoseconds: retryDelayInNanoSeconds) - logger.debug("fetching content for \(pendingLink.itemID). retry count: \(pendingLink.retryCount)") - - await prefetchPage( - pendingLink: PendingLink( - itemID: pendingLink.itemID, - retryCount: pendingLink.retryCount + 1 - ), - username: username - ) - } catch { - logger.debug("prefetching task was cancelled") - } - } - } - - public func fetchArticleContent( - itemID: String, - username: String? = nil, - requestCount: Int = 1 - ) async throws -> ArticleContent { - guard requestCount < 7 else { - throw ContentFetchError.badData - } - - guard let username = username ?? currentViewer?.username else { - throw ContentFetchError.unauthorized - } - - let fetchedContent = try await articleContent(username: username, itemID: itemID, useCache: true) - - switch fetchedContent.contentStatus { - case .failed: - throw ContentFetchError.badData - case .processing: - let retryDelayInNanoSeconds = UInt64(requestCount * 2 * 1_000_000_000) - try await Task.sleep(nanoseconds: retryDelayInNanoSeconds) - logger.debug("fetching content for \(itemID). request count: \(requestCount)") - return try await fetchArticleContent(itemID: itemID, username: username, requestCount: requestCount + 1) - case .succeeded, .unknown: - return fetchedContent - } - } - - // swiftlint:disable:next function_body_length - func articleContent( - username: String, - itemID: String, - useCache: Bool - ) async throws -> ArticleContent { - struct ArticleProps { - let item: InternalLinkedItem - let htmlContent: String - let highlights: [InternalHighlight] - let contentStatus: Enums.ArticleSavingRequestStatus? - } - - if useCache, let cachedContent = await cachedArticleContent(itemID: itemID) { - return cachedContent - } - - // If the page was locally created, make sure they are synced before we pull content - await syncUnsyncedArticleContent(itemID: itemID) - + func articleContentFetch(username: String, itemID: String) async throws -> ArticleProps { enum QueryResult { case success(result: ArticleProps) case error(error: String) @@ -122,7 +47,7 @@ extension DataService { ), htmlContent: try $0.content(), highlights: try $0.highlights(selection: highlightSelection.list), - contentStatus: try $0.state() + contentStatus: try $0.state()?.articleContentStatus ) } @@ -144,7 +69,7 @@ extension DataService { let path = appEnvironment.graphqlPath let headers = networker.defaultHeaders - let result: ArticleProps = try await withCheckedThrowingContinuation { continuation in + return try await withCheckedThrowingContinuation { continuation in send(query, to: path, headers: headers) { queryResult in guard let payload = try? queryResult.get() else { continuation.resume(throwing: ContentFetchError.network) @@ -153,243 +78,18 @@ extension DataService { switch payload.data { case let .success(result: result): - let status = result.contentStatus ?? .succeeded - if status == .failed { - continuation.resume(throwing: ContentFetchError.badData) - return - } continuation.resume(returning: result) case .error: continuation.resume(throwing: ContentFetchError.badData) } } } - - let articleContent = ArticleContent( - title: result.item.title, - htmlContent: result.htmlContent, - highlightsJSONString: result.highlights.asJSONString, - contentStatus: result.item.isPDF ? .succeeded : .make(from: result.contentStatus) - ) - - if result.contentStatus == .succeeded || result.item.isPDF { - do { - try await persistArticleContent( - item: result.item, - htmlContent: result.htmlContent, - highlights: result.highlights - ) - } catch { - var message = "unknown error" - let basicError = (error as? BasicError) ?? BasicError.message(messageText: "unknown error") - if case let BasicError.message(messageText) = basicError { - message = messageText - } - throw ContentFetchError.unknown(description: message) - } - } - - return articleContent - } - - // swiftlint:disable:next function_body_length - func persistArticleContent( - item: InternalLinkedItem, - htmlContent: String, - highlights: [InternalHighlight] - ) async throws { - var needsPDFDownload = false - - await backgroundContext.perform { [weak self] in - guard let self = self else { return } - let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() - fetchRequest.predicate = NSPredicate(format: "id == %@", item.id) - - let existingItem = try? self.backgroundContext.fetch(fetchRequest).first - let linkedItem = existingItem ?? LinkedItem(entity: LinkedItem.entity(), insertInto: self.backgroundContext) - - let highlightObjects = highlights.map { - $0.asManagedObject(context: self.backgroundContext) - } - linkedItem.addToHighlights(NSSet(array: highlightObjects)) - linkedItem.htmlContent = htmlContent - linkedItem.id = item.id - linkedItem.state = item.state - linkedItem.title = item.title - linkedItem.createdAt = item.createdAt - linkedItem.savedAt = item.savedAt - linkedItem.readingProgress = item.readingProgress - linkedItem.readingProgressAnchor = Int64(item.readingProgressAnchor) - linkedItem.imageURLString = item.imageURLString - linkedItem.onDeviceImageURLString = item.onDeviceImageURLString - linkedItem.pageURLString = item.pageURLString - linkedItem.descriptionText = item.descriptionText - linkedItem.publisherURLString = item.publisherURLString - linkedItem.author = item.author - linkedItem.publishDate = item.publishDate - linkedItem.slug = item.slug - linkedItem.readAt = item.readAt - linkedItem.isArchived = item.isArchived - linkedItem.contentReader = item.contentReader - linkedItem.serverSyncStatus = Int64(ServerSyncStatus.isNSync.rawValue) - - if item.isPDF { - needsPDFDownload = true - - // Check if we already have the PDF item locally. Either in temporary - // space, or in the documents directory - if let localPDF = existingItem?.localPDF { - if PDFUtils.exists(filename: localPDF) { - linkedItem.localPDF = localPDF - needsPDFDownload = false - } - } - - if let tempPDFURL = existingItem?.tempPDFURL { - linkedItem.localPDF = try? PDFUtils.moveToLocal(url: tempPDFURL) - _ = PDFUtils.exists(filename: linkedItem.localPDF) - if linkedItem.localPDF != nil { - needsPDFDownload = false - } - } - } - } - - if item.isPDF, needsPDFDownload { - _ = try await fetchPDFData(slug: item.slug, pageURLString: item.pageURLString) - } - - try await backgroundContext.perform { [weak self] in - do { - try self?.backgroundContext.save() - logger.debug("ArticleContent saved succesfully") - } catch { - self?.backgroundContext.rollback() - logger.debug("Failed to save ArticleContent") - throw error - } - } - } - - public func fetchPDFData(slug: String, pageURLString: String) async throws -> URL? { - guard let url = URL(string: pageURLString) else { - throw BasicError.message(messageText: "No PDF URL found") - } - let result: (Data, URLResponse)? = try? await URLSession.shared.data(from: url) - guard let httpResponse = result?.1 as? HTTPURLResponse, 200 ..< 300 ~= httpResponse.statusCode else { - throw BasicError.message(messageText: "pdfFetch failed. no response or bad status code.") - } - guard let data = result?.0 else { - throw BasicError.message(messageText: "pdfFetch failed. no data received.") - } - - var localPdfURL: URL? - let tempPath = FileManager.default - .urls(for: .cachesDirectory, in: .userDomainMask)[0] - .appendingPathComponent(UUID().uuidString + ".pdf") - - try await backgroundContext.perform { [weak self] in - let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() - fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(LinkedItem.slug), slug) - - let linkedItem = try? self?.backgroundContext.fetch(fetchRequest).first - guard let linkedItem = linkedItem else { - let errorMessage = "pdfFetch failed. could not find LinkedItem from fetch request" - throw BasicError.message(messageText: errorMessage) - } - - do { - try data.write(to: tempPath) - let localPDF = try PDFUtils.moveToLocal(url: tempPath) - localPdfURL = PDFUtils.localPdfURL(filename: localPDF) - linkedItem.tempPDFURL = nil - linkedItem.localPDF = localPDF - try self?.backgroundContext.save() - } catch { - self?.backgroundContext.rollback() - let errorMessage = "pdfFetch failed. core data save failed." - throw BasicError.message(messageText: errorMessage) - } - } - - return localPdfURL - } - - func cachedArticleContent(itemID: String) async -> ArticleContent? { - let linkedItemFetchRequest: NSFetchRequest = LinkedItem.fetchRequest() - linkedItemFetchRequest.predicate = NSPredicate( - format: "id == %@", itemID - ) - - let context = backgroundContext - - return await context.perform(schedule: .immediate) { - guard let linkedItem = try? context.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( - title: linkedItem.unwrappedTitle, - htmlContent: htmlContent, - highlightsJSONString: highlights.map { InternalHighlight.make(from: $0) }.asJSONString, - contentStatus: .succeeded - ) - } - } - - public func syncUnsyncedArticleContent(itemID: String) async { - let linkedItemFetchRequest: NSFetchRequest = LinkedItem.fetchRequest() - linkedItemFetchRequest.predicate = NSPredicate( - format: "id == %@", itemID - ) - - let context = backgroundContext - - var id: String? - var url: String? - var title: String? - var originalHtml: String? - var serverSyncStatus: Int64? - - backgroundContext.performAndWait { - guard let linkedItem = try? context.fetch(linkedItemFetchRequest).first else { return } - id = linkedItem.unwrappedID - url = linkedItem.unwrappedPageURLString - title = linkedItem.unwrappedTitle - originalHtml = linkedItem.originalHtml - serverSyncStatus = linkedItem.serverSyncStatus - } - - guard let id = id, let url = url, let title = title, - let serverSyncStatus = serverSyncStatus, - serverSyncStatus == ServerSyncStatus.needsCreation.rawValue - else { - return - } - - do { - if let originalHtml = originalHtml { - _ = try await savePage(id: id, url: url, title: title, originalHtml: originalHtml) - } else { - _ = try await saveURL(id: id, url: url) - } - } catch { - // We don't propogate these errors, we just let it pass through so - // the user can attempt to fetch content again. - print("Error syncUnsyncedArticleContent") - } } } -private extension ArticleContentStatus { - static func make(from savingRequestStatus: Enums.ArticleSavingRequestStatus?) -> ArticleContentStatus { - guard let savingRequestStatus = savingRequestStatus else { return .unknown } - - switch savingRequestStatus { +extension Enums.ArticleSavingRequestStatus { + var articleContentStatus: ArticleContentStatus { + switch self { case .failed: return .failed case .processing: From 87092d741829b6a426c0ec4b698ae0f5ee28a7f5 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 16 Jun 2022 15:17:40 -0700 Subject: [PATCH 04/13] use ArticleContentStatus to represent state for InternalLinkedItem --- .../Models/DataModels/ArticleContent.swift | 6 ++++++ .../Public/LinkedItemContentLoading.swift | 11 ++--------- .../Queries/ArticleContentQuery.swift | 19 ++----------------- .../Queries/LinkedItemNetworkQuery.swift | 2 +- .../InternalModels/InternalLinkedItem.swift | 6 +++--- 5 files changed, 14 insertions(+), 30 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/ArticleContent.swift b/apple/OmnivoreKit/Sources/Models/DataModels/ArticleContent.swift index 0d1e9fc3f..e1ff986ef 100644 --- a/apple/OmnivoreKit/Sources/Models/DataModels/ArticleContent.swift +++ b/apple/OmnivoreKit/Sources/Models/DataModels/ArticleContent.swift @@ -25,3 +25,9 @@ public struct ArticleContent { self.contentStatus = contentStatus } } + +public extension String { + var asArticleContentStatus: ArticleContentStatus? { + ArticleContentStatus(rawValue: self) + } +} diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemContentLoading.swift b/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemContentLoading.swift index 891f06a25..684a0b6e6 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemContentLoading.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemContentLoading.swift @@ -82,7 +82,7 @@ extension DataService { title: fetchResult.item.title, htmlContent: fetchResult.htmlContent, highlightsJSONString: fetchResult.highlights.asJSONString, - contentStatus: fetchResult.item.isPDF ? .succeeded : .make(from: fetchResult.item.state) + contentStatus: fetchResult.item.isPDF ? .succeeded : fetchResult.item.state ) if articleContent.contentStatus == .succeeded { @@ -119,7 +119,7 @@ extension DataService { linkedItem.addToHighlights(NSSet(array: highlightObjects)) linkedItem.htmlContent = articleProps.htmlContent linkedItem.id = articleProps.item.id - linkedItem.state = articleProps.item.state + linkedItem.state = articleProps.item.state.rawValue linkedItem.title = articleProps.item.title linkedItem.createdAt = articleProps.item.createdAt linkedItem.savedAt = articleProps.item.savedAt @@ -289,10 +289,3 @@ extension DataService { } } } - -private extension ArticleContentStatus { - static func make(from status: String?) -> ArticleContentStatus { - guard let status = status else { return .unknown } - return ArticleContentStatus(rawValue: status) ?? .unknown - } -} diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift index 6c3bd16ec..3e5da57cd 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift @@ -8,7 +8,6 @@ struct ArticleProps { let item: InternalLinkedItem let htmlContent: String let highlights: [InternalHighlight] - let contentStatus: ArticleContentStatus? // TODO: remove this? } extension DataService { @@ -27,7 +26,7 @@ extension DataService { savedAt: try $0.savedAt().value ?? Date(), readAt: try $0.readAt()?.value, updatedAt: try $0.updatedAt().value ?? Date(), - state: try $0.state()?.rawValue ?? "SUCCEEDED", + state: try $0.state()?.rawValue.asArticleContentStatus ?? .succeeded, readingProgress: try $0.readingProgressPercent(), readingProgressAnchor: try $0.readingProgressAnchorIndex(), imageURLString: try $0.image(), @@ -46,8 +45,7 @@ extension DataService { labels: try $0.labels(selection: feedItemLabelSelection.list.nullable) ?? [] ), htmlContent: try $0.content(), - highlights: try $0.highlights(selection: highlightSelection.list), - contentStatus: try $0.state()?.articleContentStatus + highlights: try $0.highlights(selection: highlightSelection.list) ) } @@ -86,16 +84,3 @@ extension DataService { } } } - -extension Enums.ArticleSavingRequestStatus { - var articleContentStatus: ArticleContentStatus { - switch self { - case .failed: - return .failed - case .processing: - return .processing - case .succeeded: - return .succeeded - } - } -} diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift index e5f614589..bc40476a4 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift @@ -140,7 +140,7 @@ private let libraryArticleSelection = Selection.Article { savedAt: try $0.savedAt().value ?? Date(), readAt: try $0.readAt()?.value, updatedAt: try $0.updatedAt().value ?? Date(), - state: try $0.state()?.rawValue ?? "SUCCEEDED", + state: try $0.state()?.rawValue.asArticleContentStatus ?? .succeeded, readingProgress: try $0.readingProgressPercent(), readingProgressAnchor: try $0.readingProgressAnchorIndex(), imageURLString: try $0.image(), diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift index 115f6aa03..f4e267546 100644 --- a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift +++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift @@ -9,7 +9,7 @@ struct InternalLinkedItem { let savedAt: Date let readAt: Date? let updatedAt: Date - let state: String + let state: ArticleContentStatus var readingProgress: Double var readingProgressAnchor: Int let imageURLString: String? @@ -44,7 +44,7 @@ struct InternalLinkedItem { linkedItem.savedAt = savedAt linkedItem.updatedAt = updatedAt linkedItem.readAt = readAt - linkedItem.state = state + linkedItem.state = state.rawValue linkedItem.readingProgress = readingProgress linkedItem.readingProgressAnchor = Int64(readingProgressAnchor) linkedItem.imageURLString = imageURLString @@ -109,7 +109,7 @@ extension JSONArticle { savedAt: savedAt, readAt: readAt, updatedAt: updatedAt, - state: "SUCCEEDED", + state: .succeeded, readingProgress: readingProgressPercent, readingProgressAnchor: readingProgressAnchorIndex, imageURLString: image, From e399a5e18570e137f51764b854134c543e2af414 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 16 Jun 2022 15:58:10 -0700 Subject: [PATCH 05/13] rename loadContentWithRtries function. move pdfFetch to own file --- .../App/PDFSupport/PDFViewerViewModel.swift | 8 +- .../WebReader/WebReaderLoadingContainer.swift | 2 +- .../Views/WebReader/WebReaderViewModel.swift | 2 +- .../Public/LinkedItemContentLoading.swift | 218 +----------------- .../DataService/Public/PDFLoading.swift | 50 ++++ .../Queries/ArticleContentQuery.swift | 1 + 6 files changed, 64 insertions(+), 217 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/Services/DataService/Public/PDFLoading.swift diff --git a/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift b/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift index 14bcb6ed2..ce6f0a220 100644 --- a/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift @@ -94,12 +94,16 @@ public final class PDFViewerViewModel: ObservableObject { if itemDownloaded { return pdfItem.localPdfURL } + if let tempURL = pdfItem.tempPDFURL { - if let localURL = try? PDFUtils.copyToLocal(url: tempURL) { + if (try? PDFUtils.copyToLocal(url: tempURL)) != nil { return tempURL } } - if let localURL = try await dataService.fetchPDFData(slug: pdfItem.slug, pageURLString: pdfItem.originalArticleURL) { + + let localURL = try await dataService.fetchPDFData(slug: pdfItem.slug, pageURLString: pdfItem.originalArticleURL) + + if let localURL = localURL { return localURL } } catch { diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift index 90471c821..ac508aff7 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift @@ -38,7 +38,7 @@ import Utils let item = await fetchLinkedItem(dataService: dataService, requestID: existing.itemID, username: username) if let item = item, let itemID = item.id { do { - let articleContent = try await dataService.loadArticleContent(itemID: itemID, username: username, requestCount: 0) + let articleContent = try await dataService.loadArticleContentWithRetries(itemID: itemID, username: username, requestCount: 0) // We've fetched the article content, now reload the item from core data if let linkedItem = dataService.viewContext.object(with: item.objectID) as? LinkedItem { self.item = linkedItem diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift index 3f9419956..0af8e158c 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift @@ -16,7 +16,7 @@ struct SafariWebLink: Identifiable { errorMessage = nil do { - articleContent = try await dataService.loadArticleContent(itemID: itemID) + articleContent = try await dataService.loadArticleContentWithRetries(itemID: itemID) } catch { if retryCount == 0 { return await loadContent(dataService: dataService, itemID: itemID, retryCount: 1) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemContentLoading.swift b/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemContentLoading.swift index 684a0b6e6..9a5153d5a 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemContentLoading.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemContentLoading.swift @@ -1,46 +1,16 @@ import CoreData import Foundation import Models -import SwiftGraphQL -import Utils -extension DataService { - struct PendingLink { - let itemID: String - let retryCount: Int - } - - public func prefetchPages(itemIDs: [String], username: String) async { +public extension DataService { + func prefetchPages(itemIDs: [String], username: String) async { // TODO: make this concurrent for itemID in itemIDs { await prefetchPage(pendingLink: PendingLink(itemID: itemID, retryCount: 1), username: username) } } - func prefetchPage(pendingLink: PendingLink, username: String) async { - let content = try? await loadArticleContent(username: username, itemID: pendingLink.itemID, useCache: false) - - if content?.contentStatus == .processing, pendingLink.retryCount < 7 { - let retryDelayInNanoSeconds = UInt64(pendingLink.retryCount * 2 * 1_000_000_000) - - do { - try await Task.sleep(nanoseconds: retryDelayInNanoSeconds) - logger.debug("fetching content for \(pendingLink.itemID). retry count: \(pendingLink.retryCount)") - - await prefetchPage( - pendingLink: PendingLink( - itemID: pendingLink.itemID, - retryCount: pendingLink.retryCount + 1 - ), - username: username - ) - } catch { - logger.debug("prefetching task was cancelled") - } - } - } - - public func loadArticleContent( + func loadArticleContentWithRetries( itemID: String, username: String? = nil, requestCount: Int = 1 @@ -62,191 +32,13 @@ extension DataService { let retryDelayInNanoSeconds = UInt64(requestCount * 2 * 1_000_000_000) try await Task.sleep(nanoseconds: retryDelayInNanoSeconds) logger.debug("fetching content for \(itemID). request count: \(requestCount)") - return try await loadArticleContent(itemID: itemID, username: username, requestCount: requestCount + 1) + return try await loadArticleContentWithRetries(itemID: itemID, username: username, requestCount: requestCount + 1) case .succeeded, .unknown: return fetchedContent } } - func loadArticleContent(username: String, itemID: String, useCache: Bool) async throws -> ArticleContent { - if useCache, let cachedContent = await cachedArticleContent(itemID: itemID) { - return cachedContent - } - - // If the page was locally created, make sure they are synced before we pull content - await syncUnsyncedArticleContent(itemID: itemID) - - let fetchResult = try await articleContentFetch(username: username, itemID: itemID) - - let articleContent = ArticleContent( - title: fetchResult.item.title, - htmlContent: fetchResult.htmlContent, - highlightsJSONString: fetchResult.highlights.asJSONString, - contentStatus: fetchResult.item.isPDF ? .succeeded : fetchResult.item.state - ) - - if articleContent.contentStatus == .succeeded { - do { - try await persistArticleContent(articleProps: fetchResult) - } catch { - var message = "unknown error" - let basicError = (error as? BasicError) ?? BasicError.message(messageText: "unknown error") - if case let BasicError.message(messageText) = basicError { - message = messageText - } - throw ContentFetchError.unknown(description: message) - } - } - - return articleContent - } - - // swiftlint:disable:next function_body_length - func persistArticleContent(articleProps: ArticleProps) async throws { - var needsPDFDownload = false - - await backgroundContext.perform { [weak self] in - guard let self = self else { return } - let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() - fetchRequest.predicate = NSPredicate(format: "id == %@", articleProps.item.id) - - let existingItem = try? self.backgroundContext.fetch(fetchRequest).first - let linkedItem = existingItem ?? LinkedItem(entity: LinkedItem.entity(), insertInto: self.backgroundContext) - - let highlightObjects = articleProps.highlights.map { - $0.asManagedObject(context: self.backgroundContext) - } - linkedItem.addToHighlights(NSSet(array: highlightObjects)) - linkedItem.htmlContent = articleProps.htmlContent - linkedItem.id = articleProps.item.id - linkedItem.state = articleProps.item.state.rawValue - linkedItem.title = articleProps.item.title - linkedItem.createdAt = articleProps.item.createdAt - linkedItem.savedAt = articleProps.item.savedAt - linkedItem.readingProgress = articleProps.item.readingProgress - linkedItem.readingProgressAnchor = Int64(articleProps.item.readingProgressAnchor) - linkedItem.imageURLString = articleProps.item.imageURLString - linkedItem.onDeviceImageURLString = articleProps.item.onDeviceImageURLString - linkedItem.pageURLString = articleProps.item.pageURLString - linkedItem.descriptionText = articleProps.item.descriptionText - linkedItem.publisherURLString = articleProps.item.publisherURLString - linkedItem.author = articleProps.item.author - linkedItem.publishDate = articleProps.item.publishDate - linkedItem.slug = articleProps.item.slug - linkedItem.readAt = articleProps.item.readAt - linkedItem.isArchived = articleProps.item.isArchived - linkedItem.contentReader = articleProps.item.contentReader - linkedItem.serverSyncStatus = Int64(ServerSyncStatus.isNSync.rawValue) - - if articleProps.item.isPDF { - needsPDFDownload = true - - // Check if we already have the PDF item locally. Either in temporary - // space, or in the documents directory - if let localPDF = existingItem?.localPDF { - if PDFUtils.exists(filename: localPDF) { - linkedItem.localPDF = localPDF - needsPDFDownload = false - } - } - - if let tempPDFURL = existingItem?.tempPDFURL { - linkedItem.localPDF = try? PDFUtils.moveToLocal(url: tempPDFURL) - _ = PDFUtils.exists(filename: linkedItem.localPDF) - if linkedItem.localPDF != nil { - needsPDFDownload = false - } - } - } - } - - if articleProps.item.isPDF, needsPDFDownload { - _ = try await fetchPDFData(slug: articleProps.item.slug, pageURLString: articleProps.item.pageURLString) - } - - try await backgroundContext.perform { [weak self] in - do { - try self?.backgroundContext.save() - logger.debug("ArticleContent saved succesfully") - } catch { - self?.backgroundContext.rollback() - logger.debug("Failed to save ArticleContent") - throw error - } - } - } - - public func fetchPDFData(slug: String, pageURLString: String) async throws -> URL? { - guard let url = URL(string: pageURLString) else { - throw BasicError.message(messageText: "No PDF URL found") - } - let result: (Data, URLResponse)? = try? await URLSession.shared.data(from: url) - guard let httpResponse = result?.1 as? HTTPURLResponse, 200 ..< 300 ~= httpResponse.statusCode else { - throw BasicError.message(messageText: "pdfFetch failed. no response or bad status code.") - } - guard let data = result?.0 else { - throw BasicError.message(messageText: "pdfFetch failed. no data received.") - } - - var localPdfURL: URL? - let tempPath = FileManager.default - .urls(for: .cachesDirectory, in: .userDomainMask)[0] - .appendingPathComponent(UUID().uuidString + ".pdf") - - try await backgroundContext.perform { [weak self] in - let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() - fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(LinkedItem.slug), slug) - - let linkedItem = try? self?.backgroundContext.fetch(fetchRequest).first - guard let linkedItem = linkedItem else { - let errorMessage = "pdfFetch failed. could not find LinkedItem from fetch request" - throw BasicError.message(messageText: errorMessage) - } - - do { - try data.write(to: tempPath) - let localPDF = try PDFUtils.moveToLocal(url: tempPath) - localPdfURL = PDFUtils.localPdfURL(filename: localPDF) - linkedItem.tempPDFURL = nil - linkedItem.localPDF = localPDF - try self?.backgroundContext.save() - } catch { - self?.backgroundContext.rollback() - let errorMessage = "pdfFetch failed. core data save failed." - throw BasicError.message(messageText: errorMessage) - } - } - - return localPdfURL - } - - func cachedArticleContent(itemID: String) async -> ArticleContent? { - let linkedItemFetchRequest: NSFetchRequest = LinkedItem.fetchRequest() - linkedItemFetchRequest.predicate = NSPredicate( - format: "id == %@", itemID - ) - - let context = backgroundContext - - return await context.perform(schedule: .immediate) { - guard let linkedItem = try? context.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( - title: linkedItem.unwrappedTitle, - htmlContent: htmlContent, - highlightsJSONString: highlights.map { InternalHighlight.make(from: $0) }.asJSONString, - contentStatus: .succeeded - ) - } - } - - public func syncUnsyncedArticleContent(itemID: String) async { + func syncUnsyncedArticleContent(itemID: String) async { let linkedItemFetchRequest: NSFetchRequest = LinkedItem.fetchRequest() linkedItemFetchRequest.predicate = NSPredicate( format: "id == %@", itemID diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Public/PDFLoading.swift b/apple/OmnivoreKit/Sources/Services/DataService/Public/PDFLoading.swift new file mode 100644 index 000000000..0ef086ea5 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Services/DataService/Public/PDFLoading.swift @@ -0,0 +1,50 @@ +import CoreData +import Foundation +import Models +import Utils + +public extension DataService { + func fetchPDFData(slug: String, pageURLString: String) async throws -> URL? { + guard let url = URL(string: pageURLString) else { + throw BasicError.message(messageText: "No PDF URL found") + } + let result: (Data, URLResponse)? = try? await URLSession.shared.data(from: url) + guard let httpResponse = result?.1 as? HTTPURLResponse, 200 ..< 300 ~= httpResponse.statusCode else { + throw BasicError.message(messageText: "pdfFetch failed. no response or bad status code.") + } + guard let data = result?.0 else { + throw BasicError.message(messageText: "pdfFetch failed. no data received.") + } + + var localPdfURL: URL? + let tempPath = FileManager.default + .urls(for: .cachesDirectory, in: .userDomainMask)[0] + .appendingPathComponent(UUID().uuidString + ".pdf") + + try await backgroundContext.perform { [weak self] in + let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(LinkedItem.slug), slug) + + let linkedItem = try? self?.backgroundContext.fetch(fetchRequest).first + guard let linkedItem = linkedItem else { + let errorMessage = "pdfFetch failed. could not find LinkedItem from fetch request" + throw BasicError.message(messageText: errorMessage) + } + + do { + try data.write(to: tempPath) + let localPDF = try PDFUtils.moveToLocal(url: tempPath) + localPdfURL = PDFUtils.localPdfURL(filename: localPDF) + linkedItem.tempPDFURL = nil + linkedItem.localPDF = localPDF + try self?.backgroundContext.save() + } catch { + self?.backgroundContext.rollback() + let errorMessage = "pdfFetch failed. core data save failed." + throw BasicError.message(messageText: errorMessage) + } + } + + return localPdfURL + } +} diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift index 3e5da57cd..4e1a09eae 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift @@ -11,6 +11,7 @@ struct ArticleProps { } extension DataService { + // swiftlint:disable:next function_body_length func articleContentFetch(username: String, itemID: String) async throws -> ArticleProps { enum QueryResult { case success(result: ArticleProps) From 1cdfe4a4392b944fcea1e73f58720f1d764a7763 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 16 Jun 2022 17:56:40 -0700 Subject: [PATCH 06/13] move function to load content by requestID into data service --- .../WebReader/WebReaderLoadingContainer.swift | 108 +-------- .../Models/DataModels/ArticleContent.swift | 6 +- .../Sources/Models/DataModels/FeedItem.swift | 2 +- .../Services/DataService/ContentLoading.swift | 227 ++++++++++++++++++ .../Services/DataService/DataService.swift | 13 + .../Public/LinkedItemContentLoading.swift | 48 +--- .../Public/LinkedItemLoading.swift | 11 + 7 files changed, 263 insertions(+), 152 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/Services/DataService/ContentLoading.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift index ac508aff7..c4ce0da34 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift @@ -10,112 +10,8 @@ import Utils @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, existingItem.isReadyToRead { - item = existingItem - return - } - - // If the page was locally created, make sure they are synced before we pull content - await dataService.syncUnsyncedArticleContent(itemID: existing.itemID) - - // Fetch the item and it's content - let item = await fetchLinkedItem(dataService: dataService, requestID: existing.itemID, username: username) - if let item = item, let itemID = item.id { - do { - let articleContent = try await dataService.loadArticleContentWithRetries(itemID: itemID, username: username, requestCount: 0) - // We've fetched the article content, now reload the item from core data - if let linkedItem = dataService.viewContext.object(with: item.objectID) as? LinkedItem { - self.item = linkedItem - } else { - self.item = nil - } - } catch { - self.item = nil - } - } else { - self.item = nil - } - } - - private func fetchLinkedItem( - dataService: DataService, - requestID: String, - username: String, - requestCount: Int = 1 - ) async -> LinkedItem? { - guard requestCount < 7 else { - errorMessage = "Unable to fetch item." - return nil - } - - if let objectID = try? await dataService.loadLinkedItem(username: username, itemID: requestID) { - if let linkedItem = dataService.viewContext.object(with: objectID) as? LinkedItem { - return linkedItem - } else { - errorMessage = "Unable to fetch item." - } - return nil - } - - // 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, existingItem.isReadyToRead { - print(" - FROM CORE DATA SERVICE", existingItem) - return existingItem - } - - let result = await fetchLinkedItem( - dataService: dataService, - requestID: existing.itemID, - username: username, - requestCount: requestCount + 1 - ) - if let result = result { - return result - } - } catch { - errorMessage = "Unable to fetch item." - } - return nil - } - - private func existingItemOrItemId(dataService: DataService, requestID: String) -> (existingItem: LinkedItem?, itemID: String) { - let fetchRequest: NSFetchRequest = 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) + guard let objectID = try? await dataService.loadItemContentUsingRequestID(requestID: requestID) else { return } + item = dataService.viewContext.object(with: objectID) as? LinkedItem } func trackReadEvent() { diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/ArticleContent.swift b/apple/OmnivoreKit/Sources/Models/DataModels/ArticleContent.swift index e1ff986ef..028dcbacc 100644 --- a/apple/OmnivoreKit/Sources/Models/DataModels/ArticleContent.swift +++ b/apple/OmnivoreKit/Sources/Models/DataModels/ArticleContent.swift @@ -1,3 +1,4 @@ +import CoreData import Foundation public enum ArticleContentStatus: String { @@ -12,17 +13,20 @@ public struct ArticleContent { public let htmlContent: String public let highlightsJSONString: String public let contentStatus: ArticleContentStatus + public let objectID: NSManagedObjectID? public init( title: String, htmlContent: String, highlightsJSONString: String, - contentStatus: ArticleContentStatus + contentStatus: ArticleContentStatus, + objectID: NSManagedObjectID? ) { self.title = title self.htmlContent = htmlContent self.highlightsJSONString = highlightsJSONString self.contentStatus = contentStatus + self.objectID = objectID } } diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift b/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift index 765c9f329..63f790c3f 100644 --- a/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift +++ b/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift @@ -51,7 +51,7 @@ public extension LinkedItem { return PDFUtils.exists(filename: localPDF) || PDFUtils.tempExists(tempPDFURL: tempPDFURL) } // Check the state and whether we have HTML - return state == "SUCCEEDED" + return state == ArticleContentStatus.succeeded.rawValue } var isPDF: Bool { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/ContentLoading.swift b/apple/OmnivoreKit/Sources/Services/DataService/ContentLoading.swift new file mode 100644 index 000000000..c9fe9302a --- /dev/null +++ b/apple/OmnivoreKit/Sources/Services/DataService/ContentLoading.swift @@ -0,0 +1,227 @@ +import CoreData +import Foundation +import Models +import SwiftGraphQL +import Utils + +struct PendingLink { + let itemID: String + let retryCount: Int +} + +extension DataService { + func prefetchPage(pendingLink: PendingLink, username: String) async { + let content = try? await loadArticleContent(username: username, itemID: pendingLink.itemID, useCache: false) + + if content?.contentStatus == .processing, pendingLink.retryCount < 7 { + let retryDelayInNanoSeconds = UInt64(pendingLink.retryCount * 2 * 1_000_000_000) + + do { + try await Task.sleep(nanoseconds: retryDelayInNanoSeconds) + logger.debug("fetching content for \(pendingLink.itemID). retry count: \(pendingLink.retryCount)") + + await prefetchPage( + pendingLink: PendingLink( + itemID: pendingLink.itemID, + retryCount: pendingLink.retryCount + 1 + ), + username: username + ) + } catch { + logger.debug("prefetching task was cancelled") + } + } + } + + func loadArticleContent(username: String, itemID: String, useCache: Bool) async throws -> ArticleContent { + var objectID: NSManagedObjectID? + if useCache, let cachedContent = await cachedArticleContent(itemID: itemID) { + return cachedContent + } + + // If the page was locally created, make sure they are synced before we pull content + await syncUnsyncedArticleContent(itemID: itemID) + + let fetchResult = try await articleContentFetch(username: username, itemID: itemID) + let contentStatus = fetchResult.item.isPDF ? .succeeded : fetchResult.item.state + + if contentStatus == .succeeded { + do { + objectID = try await persistArticleContent(articleProps: fetchResult) + } catch { + var message = "unknown error" + let basicError = (error as? BasicError) ?? BasicError.message(messageText: "unknown error") + if case let BasicError.message(messageText) = basicError { + message = messageText + } + throw ContentFetchError.unknown(description: message) + } + } + + return ArticleContent( + title: fetchResult.item.title, + htmlContent: fetchResult.htmlContent, + highlightsJSONString: fetchResult.highlights.asJSONString, + contentStatus: fetchResult.item.isPDF ? .succeeded : fetchResult.item.state, + objectID: objectID + ) + } + + func cachedArticleContent(itemID: String) async -> ArticleContent? { + let linkedItemFetchRequest: NSFetchRequest = LinkedItem.fetchRequest() + linkedItemFetchRequest.predicate = NSPredicate( + format: "id == %@", itemID + ) + + let context = backgroundContext + + return await context.perform(schedule: .immediate) { + guard let linkedItem = try? context.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( + title: linkedItem.unwrappedTitle, + htmlContent: htmlContent, + highlightsJSONString: highlights.map { InternalHighlight.make(from: $0) }.asJSONString, + contentStatus: .succeeded, + objectID: linkedItem.objectID + ) + } + } + + // swiftlint:disable:next function_body_length + func persistArticleContent(articleProps: ArticleProps) async throws -> NSManagedObjectID? { + var needsPDFDownload = false + var objectID: NSManagedObjectID? + + await backgroundContext.perform { [weak self] in + guard let self = self else { return } + let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "id == %@", articleProps.item.id) + + let existingItem = try? self.backgroundContext.fetch(fetchRequest).first + let linkedItem = existingItem ?? LinkedItem(entity: LinkedItem.entity(), insertInto: self.backgroundContext) + objectID = linkedItem.objectID + + let highlightObjects = articleProps.highlights.map { + $0.asManagedObject(context: self.backgroundContext) + } + linkedItem.addToHighlights(NSSet(array: highlightObjects)) + linkedItem.htmlContent = articleProps.htmlContent + linkedItem.id = articleProps.item.id + linkedItem.state = articleProps.item.state.rawValue + linkedItem.title = articleProps.item.title + linkedItem.createdAt = articleProps.item.createdAt + linkedItem.savedAt = articleProps.item.savedAt + linkedItem.readingProgress = articleProps.item.readingProgress + linkedItem.readingProgressAnchor = Int64(articleProps.item.readingProgressAnchor) + linkedItem.imageURLString = articleProps.item.imageURLString + linkedItem.onDeviceImageURLString = articleProps.item.onDeviceImageURLString + linkedItem.pageURLString = articleProps.item.pageURLString + linkedItem.descriptionText = articleProps.item.descriptionText + linkedItem.publisherURLString = articleProps.item.publisherURLString + linkedItem.author = articleProps.item.author + linkedItem.publishDate = articleProps.item.publishDate + linkedItem.slug = articleProps.item.slug + linkedItem.readAt = articleProps.item.readAt + linkedItem.isArchived = articleProps.item.isArchived + linkedItem.contentReader = articleProps.item.contentReader + linkedItem.serverSyncStatus = Int64(ServerSyncStatus.isNSync.rawValue) + + if articleProps.item.isPDF { + needsPDFDownload = true + + // Check if we already have the PDF item locally. Either in temporary + // space, or in the documents directory + if let localPDF = existingItem?.localPDF { + if PDFUtils.exists(filename: localPDF) { + linkedItem.localPDF = localPDF + needsPDFDownload = false + } + } + + if let tempPDFURL = existingItem?.tempPDFURL { + linkedItem.localPDF = try? PDFUtils.moveToLocal(url: tempPDFURL) + _ = PDFUtils.exists(filename: linkedItem.localPDF) + if linkedItem.localPDF != nil { + needsPDFDownload = false + } + } + } + } + + if articleProps.item.isPDF, needsPDFDownload { + _ = try await fetchPDFData(slug: articleProps.item.slug, pageURLString: articleProps.item.pageURLString) + } + + try await backgroundContext.perform { [weak self] in + do { + try self?.backgroundContext.save() + logger.debug("ArticleContent saved succesfully") + } catch { + self?.backgroundContext.rollback() + logger.debug("Failed to save ArticleContent") + throw error + } + } + + return objectID + } + + /// Queries CoreData for a LinkedItem using a requestID. + /// - Parameter requestID: A requestID used to check on a newly created item. + /// - Returns: The id of the CoreData object if found. + func linkedItemID(from requestID: String) -> String? { + let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "createdId == %@ OR id == %@", requestID, requestID) + return try? backgroundContext.fetch(fetchRequest).first?.unwrappedID + } + + func syncUnsyncedArticleContent(itemID: String) async { + let linkedItemFetchRequest: NSFetchRequest = LinkedItem.fetchRequest() + linkedItemFetchRequest.predicate = NSPredicate( + format: "id == %@", itemID + ) + + let context = backgroundContext + + var id: String? + var url: String? + var title: String? + var originalHtml: String? + var serverSyncStatus: Int64? + + backgroundContext.performAndWait { + guard let linkedItem = try? context.fetch(linkedItemFetchRequest).first else { return } + id = linkedItem.unwrappedID + url = linkedItem.unwrappedPageURLString + title = linkedItem.unwrappedTitle + originalHtml = linkedItem.originalHtml + serverSyncStatus = linkedItem.serverSyncStatus + } + + guard let id = id, let url = url, let title = title, + let serverSyncStatus = serverSyncStatus, + serverSyncStatus == ServerSyncStatus.needsCreation.rawValue + else { + return + } + + do { + if let originalHtml = originalHtml { + _ = try await savePage(id: id, url: url, title: title, originalHtml: originalHtml) + } else { + _ = try await saveURL(id: id, url: url) + } + } catch { + // We don't propogate these errors, we just let it pass through so + // the user can attempt to fetch content again. + print("Error syncUnsyncedArticleContent") + } + } +} diff --git a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift index f14ac5b6a..a4426aaec 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift @@ -49,6 +49,19 @@ public final class DataService: ObservableObject { return try? persistentContainer.viewContext.fetch(fetchRequest).first } + public func username() async -> String? { + if let cachedUsername = currentViewer?.username { + return cachedUsername + } + + if let viewerObjectID = try? await fetchViewer() { + let viewer = backgroundContext.object(with: viewerObjectID) as? Viewer + return viewer?.unwrappedUsername + } + + return nil + } + public func switchAppEnvironment(appEnvironment: AppEnvironment) { do { try ValetKey.appEnvironmentString.setValue(appEnvironment.rawValue) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemContentLoading.swift b/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemContentLoading.swift index 9a5153d5a..e51fc1c21 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemContentLoading.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemContentLoading.swift @@ -1,6 +1,7 @@ import CoreData import Foundation import Models +import Utils public extension DataService { func prefetchPages(itemIDs: [String], username: String) async { @@ -32,52 +33,11 @@ public extension DataService { let retryDelayInNanoSeconds = UInt64(requestCount * 2 * 1_000_000_000) try await Task.sleep(nanoseconds: retryDelayInNanoSeconds) logger.debug("fetching content for \(itemID). request count: \(requestCount)") - return try await loadArticleContentWithRetries(itemID: itemID, username: username, requestCount: requestCount + 1) + // Check for an updated requestID + let updatedItemID = linkedItemID(from: itemID) ?? itemID + return try await loadArticleContentWithRetries(itemID: updatedItemID, username: username, requestCount: requestCount + 1) case .succeeded, .unknown: return fetchedContent } } - - func syncUnsyncedArticleContent(itemID: String) async { - let linkedItemFetchRequest: NSFetchRequest = LinkedItem.fetchRequest() - linkedItemFetchRequest.predicate = NSPredicate( - format: "id == %@", itemID - ) - - let context = backgroundContext - - var id: String? - var url: String? - var title: String? - var originalHtml: String? - var serverSyncStatus: Int64? - - backgroundContext.performAndWait { - guard let linkedItem = try? context.fetch(linkedItemFetchRequest).first else { return } - id = linkedItem.unwrappedID - url = linkedItem.unwrappedPageURLString - title = linkedItem.unwrappedTitle - originalHtml = linkedItem.originalHtml - serverSyncStatus = linkedItem.serverSyncStatus - } - - guard let id = id, let url = url, let title = title, - let serverSyncStatus = serverSyncStatus, - serverSyncStatus == ServerSyncStatus.needsCreation.rawValue - else { - return - } - - do { - if let originalHtml = originalHtml { - _ = try await savePage(id: id, url: url, title: title, originalHtml: originalHtml) - } else { - _ = try await saveURL(id: id, url: url) - } - } catch { - // We don't propogate these errors, we just let it pass through so - // the user can attempt to fetch content again. - print("Error syncUnsyncedArticleContent") - } - } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift b/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift index 62b8826c9..24c6a5738 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift @@ -40,4 +40,15 @@ public extension DataService { return persistedItemID } + + func loadItemContentUsingRequestID(requestID: String) async throws -> NSManagedObjectID? { + let username: String? = await username() + guard let username = username else { throw BasicError.message(messageText: "unauthorized user") } + + // If the page was locally created, make sure they are synced before we pull content + await syncUnsyncedArticleContent(itemID: requestID) + + let articleContent = try await loadArticleContentWithRetries(itemID: requestID, username: username, requestCount: 0) + return articleContent.objectID + } } From b09268bad2412cb9227a649ed1117cf0195fc2b2 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 16 Jun 2022 19:22:49 -0700 Subject: [PATCH 07/13] fix threading violation --- .../Sources/Services/DataService/ContentLoading.swift | 11 ++++++----- .../DataService/Public/LinkedItemContentLoading.swift | 6 +++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/ContentLoading.swift b/apple/OmnivoreKit/Sources/Services/DataService/ContentLoading.swift index c9fe9302a..775103e3b 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/ContentLoading.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/ContentLoading.swift @@ -1,7 +1,6 @@ import CoreData import Foundation import Models -import SwiftGraphQL import Utils struct PendingLink { @@ -176,10 +175,12 @@ extension DataService { /// Queries CoreData for a LinkedItem using a requestID. /// - Parameter requestID: A requestID used to check on a newly created item. /// - Returns: The id of the CoreData object if found. - func linkedItemID(from requestID: String) -> String? { - let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() - fetchRequest.predicate = NSPredicate(format: "createdId == %@ OR id == %@", requestID, requestID) - return try? backgroundContext.fetch(fetchRequest).first?.unwrappedID + func linkedItemID(from requestID: String) async -> String? { + await backgroundContext.perform(schedule: .immediate) { + let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "createdId == %@ OR id == %@", requestID, requestID) + return try? self.backgroundContext.fetch(fetchRequest).first?.unwrappedID + } } func syncUnsyncedArticleContent(itemID: String) async { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemContentLoading.swift b/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemContentLoading.swift index e51fc1c21..86e020fcf 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemContentLoading.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemContentLoading.swift @@ -33,9 +33,9 @@ public extension DataService { let retryDelayInNanoSeconds = UInt64(requestCount * 2 * 1_000_000_000) try await Task.sleep(nanoseconds: retryDelayInNanoSeconds) logger.debug("fetching content for \(itemID). request count: \(requestCount)") - // Check for an updated requestID - let updatedItemID = linkedItemID(from: itemID) ?? itemID - return try await loadArticleContentWithRetries(itemID: updatedItemID, username: username, requestCount: requestCount + 1) + // Check for an updated itemID + let updatedItemID = await linkedItemID(from: itemID) + return try await loadArticleContentWithRetries(itemID: updatedItemID ?? itemID, username: username, requestCount: requestCount + 1) case .succeeded, .unknown: return fetchedContent } From 726c11afd2699711d82485ac7edae10352c77a13 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 16 Jun 2022 21:53:29 -0700 Subject: [PATCH 08/13] delete a few unused functions and lint fixes --- .../Share/ExtensionSaveService.swift | 6 ++- .../App/PDFSupport/PDFViewerViewModel.swift | 48 +++++++------------ .../WebReader/WebReaderLoadingContainer.swift | 4 +- .../Sources/Models/DataModels/FeedItem.swift | 9 ---- .../Services/DataService/ContentLoading.swift | 2 +- .../Services/DataService/OfflineSync.swift | 5 +- .../Public/LinkedItemContentLoading.swift | 9 +++- .../DataService/Public/PDFLoading.swift | 6 ++- .../OmnivoreKit/Sources/Utils/PDFUtils.swift | 14 ------ 9 files changed, 40 insertions(+), 63 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift index f40b8a61c..c58d4f97d 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift @@ -170,7 +170,11 @@ class ExtensionSaveService { case .none: requestId = try await services.dataService.createPageFromUrl(id: requestId, url: pageScrapePayload.url) case let .pdf(localUrl): - try await services.dataService.createPageFromPdf(id: requestId, localPdfURL: localUrl, url: pageScrapePayload.url) + try await services.dataService.createPageFromPdf( + id: requestId, + localPdfURL: localUrl, + url: pageScrapePayload.url + ) case let .html(html, title, _): requestId = try await services.dataService.createPage( id: requestId, diff --git a/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift b/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift index ce6f0a220..addb54af8 100644 --- a/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift @@ -1,27 +1,23 @@ -import Combine -import CoreData import Foundation import Models import Services import Utils -public final class PDFViewerViewModel: ObservableObject { - @Published public var errorMessage: String? - @Published public var readerView: Bool = false +final class PDFViewerViewModel: ObservableObject { + @Published var errorMessage: String? + @Published var readerView: Bool = false - public let pdfItem: PDFItem + let pdfItem: PDFItem - var subscriptions = Set() - - public init(pdfItem: PDFItem) { + init(pdfItem: PDFItem) { self.pdfItem = pdfItem } - public func loadHighlightPatches(completion onComplete: @escaping ([String]) -> Void) { + func loadHighlightPatches(completion onComplete: @escaping ([String]) -> Void) { onComplete(pdfItem.highlights.map { $0.patch ?? "" }) } - public func createHighlight( + func createHighlight( dataService: DataService, shortId: String, highlightID: String, @@ -37,7 +33,8 @@ public final class PDFViewerViewModel: ObservableObject { ) } - public func mergeHighlight( + // swiftlint:disable:next function_parameter_count + func mergeHighlight( dataService: DataService, shortId: String, highlightID: String, @@ -55,13 +52,13 @@ public final class PDFViewerViewModel: ObservableObject { ) } - public func removeHighlights(dataService: DataService, highlightIds: [String]) { + func removeHighlights(dataService: DataService, highlightIds: [String]) { highlightIds.forEach { highlightID in dataService.deleteHighlight(highlightID: highlightID) } } - public func updateItemReadProgress(dataService: DataService, percent: Double, anchorIndex: Int) { + func updateItemReadProgress(dataService: DataService, percent: Double, anchorIndex: Int) { dataService.updateLinkReadingProgress( itemID: pdfItem.itemID, readingProgress: percent, @@ -69,7 +66,7 @@ public final class PDFViewerViewModel: ObservableObject { ) } - public func highlightShareURL(dataService: DataService, shortId: String) -> URL? { + func highlightShareURL(dataService: DataService, shortId: String) -> URL? { let baseURL = dataService.appEnvironment.serverBaseURL var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false) @@ -82,17 +79,10 @@ public final class PDFViewerViewModel: ObservableObject { return components?.url } - public var itemDownloaded: Bool { - if let localPdfURL = pdfItem.localPdfURL, FileManager.default.fileExists(atPath: localPdfURL.path) { - return true - } - return false - } - - public func downloadPDF(dataService: DataService) async -> URL? { + func downloadPDF(dataService: DataService) async -> URL? { do { - if itemDownloaded { - return pdfItem.localPdfURL + if let localPdfURL = pdfItem.localPdfURL, FileManager.default.fileExists(atPath: localPdfURL.path) { + return localPdfURL } if let tempURL = pdfItem.tempPDFURL { @@ -101,14 +91,10 @@ public final class PDFViewerViewModel: ObservableObject { } } - let localURL = try await dataService.fetchPDFData(slug: pdfItem.slug, pageURLString: pdfItem.originalArticleURL) - - if let localURL = localURL { - return localURL - } + return try await dataService.loadPDFData(slug: pdfItem.slug, pageURLString: pdfItem.originalArticleURL) } catch { print("error downloading PDF", error) + return nil } - return nil } } diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift index c4ce0da34..94265cf37 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift @@ -34,8 +34,8 @@ import Utils @StateObject var viewModel = WebReaderLoadingContainerViewModel() public var body: some View { - if let item = viewModel.item, item.isReadyToRead { - if let pdfItem = PDFItem.make(item: item), let urlStr = item.pageURLString, let remoteUrl = URL(string: urlStr) { + if let item = viewModel.item { + if let pdfItem = PDFItem.make(item: item) { PDFViewer(viewModel: PDFViewerViewModel(pdfItem: pdfItem)) .navigationBarHidden(true) .navigationViewStyle(.stack) diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift b/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift index 63f790c3f..bcdff4116 100644 --- a/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift +++ b/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift @@ -45,15 +45,6 @@ public extension LinkedItem { readingProgress >= 0.98 } - var isReadyToRead: Bool { - if isPDF { - // If its a PDF we verify the local file is available - return PDFUtils.exists(filename: localPDF) || PDFUtils.tempExists(tempPDFURL: tempPDFURL) - } - // Check the state and whether we have HTML - return state == ArticleContentStatus.succeeded.rawValue - } - var isPDF: Bool { if let contentReader = contentReader { return contentReader == "PDF" diff --git a/apple/OmnivoreKit/Sources/Services/DataService/ContentLoading.swift b/apple/OmnivoreKit/Sources/Services/DataService/ContentLoading.swift index 775103e3b..db17d6e9c 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/ContentLoading.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/ContentLoading.swift @@ -155,7 +155,7 @@ extension DataService { } if articleProps.item.isPDF, needsPDFDownload { - _ = try await fetchPDFData(slug: articleProps.item.slug, pageURLString: articleProps.item.pageURLString) + _ = try await loadPDFData(slug: articleProps.item.slug, pageURLString: articleProps.item.pageURLString) } try await backgroundContext.perform { [weak self] in diff --git a/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift b/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift index 73dce5a04..b8660289a 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift @@ -5,7 +5,6 @@ import Utils public extension DataService { internal func syncOfflineItemsWithServerIfNeeded() async throws { - // TODO: send a simple request to see if we're online? var unsyncedLinkedItems = [LinkedItem]() var unsyncedHighlights = [Highlight]() @@ -130,9 +129,9 @@ public extension DataService { Task { if let originalHtml = originalHtml { - try await createPage(id: id, originalHtml: originalHtml, title: title, url: url) + _ = try await createPage(id: id, originalHtml: originalHtml, title: title, url: url) } else { - try await createPageFromUrl(id: id, url: url) + _ = try await createPageFromUrl(id: id, url: url) } } default: diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemContentLoading.swift b/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemContentLoading.swift index 86e020fcf..312c5d5ee 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemContentLoading.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemContentLoading.swift @@ -33,9 +33,16 @@ public extension DataService { let retryDelayInNanoSeconds = UInt64(requestCount * 2 * 1_000_000_000) try await Task.sleep(nanoseconds: retryDelayInNanoSeconds) logger.debug("fetching content for \(itemID). request count: \(requestCount)") + // Check for an updated itemID + // May have changed in the loadArticleContent call let updatedItemID = await linkedItemID(from: itemID) - return try await loadArticleContentWithRetries(itemID: updatedItemID ?? itemID, username: username, requestCount: requestCount + 1) + + return try await loadArticleContentWithRetries( + itemID: updatedItemID ?? itemID, + username: username, + requestCount: requestCount + 1 + ) case .succeeded, .unknown: return fetchedContent } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Public/PDFLoading.swift b/apple/OmnivoreKit/Sources/Services/DataService/Public/PDFLoading.swift index 0ef086ea5..c22e01542 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Public/PDFLoading.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Public/PDFLoading.swift @@ -4,19 +4,23 @@ import Models import Utils public extension DataService { - func fetchPDFData(slug: String, pageURLString: String) async throws -> URL? { + func loadPDFData(slug: String, pageURLString: String) async throws -> URL? { guard let url = URL(string: pageURLString) else { throw BasicError.message(messageText: "No PDF URL found") } + let result: (Data, URLResponse)? = try? await URLSession.shared.data(from: url) + guard let httpResponse = result?.1 as? HTTPURLResponse, 200 ..< 300 ~= httpResponse.statusCode else { throw BasicError.message(messageText: "pdfFetch failed. no response or bad status code.") } + guard let data = result?.0 else { throw BasicError.message(messageText: "pdfFetch failed. no data received.") } var localPdfURL: URL? + let tempPath = FileManager.default .urls(for: .cachesDirectory, in: .userDomainMask)[0] .appendingPathComponent(UUID().uuidString + ".pdf") diff --git a/apple/OmnivoreKit/Sources/Utils/PDFUtils.swift b/apple/OmnivoreKit/Sources/Utils/PDFUtils.swift index 1919377fc..074ec2c03 100644 --- a/apple/OmnivoreKit/Sources/Utils/PDFUtils.swift +++ b/apple/OmnivoreKit/Sources/Utils/PDFUtils.swift @@ -1,10 +1,3 @@ -// -// PDFUtils.swift -// -// -// Created by Jackson Harper on 6/3/22. -// - import CoreImage import Foundation import QuickLookThumbnailing @@ -47,13 +40,6 @@ public enum PDFUtils { return false } - public static func tempExists(tempPDFURL: URL?) -> Bool { - if let tempPDFURL = tempPDFURL { - return FileManager.default.fileExists(atPath: tempPDFURL.path) - } - return false - } - public static func titleFromPdfFile(_ urlStr: String) -> String { let url = URL(string: urlStr) if let url = url { From 6f86495cf485f4aa5b4d07faa7e6f4689df19e58 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 16 Jun 2022 22:14:45 -0700 Subject: [PATCH 09/13] lint fixes --- .../AppExtensions/Share/ExtensionSaveService.swift | 6 +++++- .../AppExtensions/Share/ShareExtensionScene.swift | 7 ++++++- .../Sources/App/PDFSupport/PDFViewer.swift | 4 +++- apple/OmnivoreKit/Sources/App/Services.swift | 1 + .../Sources/App/Views/Home/HomeFeedViewIOS.swift | 6 +++++- .../Sources/App/Views/WebReader/WebReader.swift | 1 + .../Sources/Models/LinkedItemFilter.swift | 12 +++++++++--- .../Sources/Models/PageScrapePayload.swift | 13 ------------- .../Sources/Services/DataService/DataService.swift | 1 + .../Mutations/CreateLabelPublisher.swift | 1 + .../DataService/Mutations/DeleteSubscription.swift | 4 +++- .../DataService/Mutations/MergeHighlight.swift | 1 + .../DataService/Mutations/SaveArticle.swift | 7 +++++-- .../Services/DataService/Mutations/SavePDF.swift | 13 ++----------- .../Sources/Services/DataService/SaveService.swift | 1 - .../InternalModels/InternalLinkedItem.swift | 2 +- .../OmnivoreKit/Sources/Views/Article/WebView.swift | 4 +++- .../Sources/Views/ShareExtensionView.swift | 6 +++++- apple/OmnivoreKit/Tests/UtilsTests/UtilsTests.swift | 1 + 19 files changed, 53 insertions(+), 38 deletions(-) delete mode 100644 apple/OmnivoreKit/Sources/Services/DataService/SaveService.swift diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift index c58d4f97d..4875e0ba6 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift @@ -18,7 +18,10 @@ class ExtensionSaveService { self.queue = OperationQueue() } - private func queueSaveOperation(_ pageScrape: PageScrapePayload, shareExtensionViewModel: ShareExtensionChildViewModel) { + private func queueSaveOperation( + _ pageScrape: PageScrapePayload, + shareExtensionViewModel: ShareExtensionChildViewModel + ) { ProcessInfo().performExpiringActivity(withReason: "app.omnivore.SaveActivity") { [self] expiring in guard !expiring else { self.queue.cancelAllOperations() @@ -85,6 +88,7 @@ class ExtensionSaveService { var queue: OperationQueue? var uploadTask: URLSessionTask? + // swiftlint:disable:next nesting enum State: Int { case created case started diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift index 467d158f7..1b3225cfe 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift @@ -55,7 +55,12 @@ struct ShareExtensionView: View { var body: some View { ShareExtensionChildView( viewModel: childViewModel, - onAppearAction: { viewModel.savePage(extensionContext: extensionContext, shareExtensionViewModel: childViewModel) }, + onAppearAction: { + viewModel.savePage( + extensionContext: extensionContext, + shareExtensionViewModel: childViewModel + ) + }, readNowButtonAction: { viewModel.handleReadNowAction(requestId: $0, extensionContext: extensionContext) }, dismissButtonTappedAction: { _, _ in extensionContext?.completeRequest(returningItems: [], completionHandler: nil) diff --git a/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewer.swift b/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewer.swift index d6217081c..28a8811ff 100644 --- a/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewer.swift +++ b/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewer.swift @@ -2,6 +2,7 @@ import Combine import SwiftUI import Utils +// swiftlint:disable file_length #if os(iOS) import PSPDFKit import PSPDFKitUI @@ -147,7 +148,7 @@ import Utils pdfStateObject.document = document pdfStateObject.coordinator = PDFViewCoordinator(document: document, viewModel: viewModel) } else { - errorMessage = "Unable to download PDF: \(pdfURL)" + errorMessage = "Unable to download PDF: \(pdfURL?.description ?? "")" } } } @@ -220,6 +221,7 @@ import Utils return result } + // swiftlint:disable:next function_body_length func highlightSelection(pageView: PDFPageView, selectedText: String, dataService: DataService) -> String { let highlightID = UUID().uuidString.lowercased() let quote = quoteFromSelectedText(selectedText) diff --git a/apple/OmnivoreKit/Sources/App/Services.swift b/apple/OmnivoreKit/Sources/App/Services.swift index cc394380f..b7b602c17 100644 --- a/apple/OmnivoreKit/Sources/App/Services.swift +++ b/apple/OmnivoreKit/Sources/App/Services.swift @@ -102,4 +102,5 @@ public final class Services { #endif // Command to simulate BG Task +// swiftlint:disable:next line_length // e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.omnivore.fetchLinkedItems"] diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 480e84012..bb53c910c 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -5,6 +5,7 @@ import UserNotifications import Utils import Views +// swiftlint:disable file_length #if os(iOS) private let enableGrid = UIDevice.isIPad || FeatureFlag.enableGridCardsOnPhone @@ -196,7 +197,10 @@ import Views } .padding(.horizontal) .sheet(isPresented: $showLabelsSheet) { - FilterByLabelsView(initiallySelected: viewModel.selectedLabels, initiallyNegated: viewModel.negatedLabels) { + FilterByLabelsView( + initiallySelected: viewModel.selectedLabels, + initiallyNegated: viewModel.negatedLabels + ) { self.viewModel.selectedLabels = $0 self.viewModel.negatedLabels = $1 } diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift index d2b47ab95..a16e2a307 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift @@ -73,6 +73,7 @@ import WebKit return webView } + // swiftlint:disable:next cyclomatic_complexity func updateUIView(_ webView: WKWebView, context: Context) { if annotationSaveTransactionID != context.coordinator.lastSavedAnnotationID { context.coordinator.lastSavedAnnotationID = annotationSaveTransactionID diff --git a/apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift b/apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift index c257b92ef..62669a63c 100644 --- a/apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift +++ b/apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift @@ -58,11 +58,17 @@ public extension LinkedItemFilter { return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, notInArchivePredicate]) case .readlater: // non-archived or deleted items without the Newsletter label - let nonNewsletterLabelPredicate = NSPredicate(format: "NOT SUBQUERY(labels, $label, $label.name == \"Newsletter\") .@count > 0") - return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, notInArchivePredicate, nonNewsletterLabelPredicate]) + let nonNewsletterLabelPredicate = NSPredicate( + format: "NOT SUBQUERY(labels, $label, $label.name == \"Newsletter\") .@count > 0" + ) + return NSCompoundPredicate(andPredicateWithSubpredicates: [ + undeletedPredicate, notInArchivePredicate, nonNewsletterLabelPredicate + ]) case .newsletters: // non-archived or deleted items with the Newsletter label - let newsletterLabelPredicate = NSPredicate(format: "SUBQUERY(labels, $label, $label.name == \"Newsletter\").@count > 0") + let newsletterLabelPredicate = NSPredicate( + format: "SUBQUERY(labels, $label, $label.name == \"Newsletter\").@count > 0" + ) return NSCompoundPredicate(andPredicateWithSubpredicates: [notInArchivePredicate, newsletterLabelPredicate]) case .all: // include everything undeleted diff --git a/apple/OmnivoreKit/Sources/Models/PageScrapePayload.swift b/apple/OmnivoreKit/Sources/Models/PageScrapePayload.swift index 106d425b4..1b997e82f 100644 --- a/apple/OmnivoreKit/Sources/Models/PageScrapePayload.swift +++ b/apple/OmnivoreKit/Sources/Models/PageScrapePayload.swift @@ -256,20 +256,7 @@ private extension PageScrapePayload { let localFile = UUID().uuidString.lowercased() + ".pdf" dest.appendPathComponent(localFile) do { - print("EXISTING PDF URL", url) - let attr = try? FileManager.default.attributesOfItem(atPath: url.path) - if let attr = attr { - print("EXISTING FILE SIZE", attr[.size]) - } - try FileManager.default.copyItem(at: url, to: dest) - print("COPIED TO URL", dest) - - let attr2 = try? FileManager.default.attributesOfItem(atPath: dest.path) - if let attr2 = attr2 { - print("COPIED FILE SIZE", attr2[.size]) - } - return PageScrapePayload(url: url.absoluteString, localUrl: dest) } catch { print("error copying file locally", error) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift index a4426aaec..b35582b0d 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift @@ -122,6 +122,7 @@ public final class DataService: ObservableObject { return isFirstRunOfVersion || isFirstRunWithBuildNumber } + // swiftlint:disable:next function_body_length public func persistPageScrapePayload(_ pageScrape: PageScrapePayload, requestId: String) async throws { let normalizedURL = normalizeURL(pageScrape.url) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateLabelPublisher.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateLabelPublisher.swift index 8df8b0d4a..e053a8717 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateLabelPublisher.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateLabelPublisher.swift @@ -26,6 +26,7 @@ extension DataService { } } + // swiftlint:disable:next function_body_length func syncLabelCreation(label: InternalLinkedItemLabel) { enum MutationResult { case saved(label: InternalLinkedItemLabel) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/DeleteSubscription.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/DeleteSubscription.swift index d8bdf3289..e35a8bd69 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/DeleteSubscription.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/DeleteSubscription.swift @@ -13,7 +13,9 @@ public extension DataService { let selection = Selection { try $0.on( unsubscribeError: .init { .error(errorMessage: (try $0.errorCodes().first ?? .unauthorized).rawValue) }, - unsubscribeSuccess: .init { .success(id: try $0.subscription(selection: Selection.Subscription { try $0.id() })) } + unsubscribeSuccess: .init { + .success(id: try $0.subscription(selection: Selection.Subscription { try $0.id() })) + } ) } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/MergeHighlight.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/MergeHighlight.swift index 4f8eefc3f..798fcddd6 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/MergeHighlight.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/MergeHighlight.swift @@ -38,6 +38,7 @@ extension DataService { return internalHighlight.encoded() } + // swiftlint:disable:next function_body_length func syncHighlightMerge(highlight: InternalHighlight, articleId: String, overlapHighlightIdList: [String]) { enum MutationResult { case saved(highlight: InternalHighlight) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SaveArticle.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SaveArticle.swift index 69e7e511e..2c0766899 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SaveArticle.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SaveArticle.swift @@ -84,8 +84,11 @@ public extension Networker { } public extension DataService { - // swiftlint:disable:next line_length - func saveArticlePublisher(pageScrapePayload: PageScrapePayload, uploadFileId: String?) -> AnyPublisher { + // swiftlint:disable:next function_body_length + func saveArticlePublisher( + pageScrapePayload: PageScrapePayload, + uploadFileId: String? + ) -> AnyPublisher { enum MutationResult { case saved(created: Bool) case error(errorCode: Enums.CreateArticleErrorCode) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePDF.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePDF.swift index c5b53b58e..3e9d7787f 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePDF.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePDF.swift @@ -11,6 +11,7 @@ public struct UploadFileRequestPayload { } public extension DataService { + // swiftlint:disable:next function_body_length func uploadFileRequest(id: String, url: String) async throws -> UploadFileRequestPayload { enum MutationResult { case success(payload: UploadFileRequestPayload) @@ -60,11 +61,7 @@ public extension DataService { switch payload.data { case let .success(payload): - if let urlString = payload.urlString, let url = URL(string: urlString) { - continuation.resume(returning: payload) - } else { - continuation.resume(throwing: SaveArticleError.unknown(description: "No upload URL")) - } + continuation.resume(returning: payload) case let .error(errorCode: errorCode): switch errorCode { case .unauthorized: @@ -85,12 +82,6 @@ public extension DataService { request.httpMethod = "PUT" request.addValue("application/pdf", forHTTPHeaderField: "content-type") - print("UPLOADING PDF", localPdfURL) - let attr = try? FileManager.default.attributesOfItem(atPath: localPdfURL.path) - if let attr = attr { - print("UPLOADING ATTR", attr[.size]) - } - return try await withCheckedThrowingContinuation { continuation in let task = networker.urlSession.uploadTask(with: request, fromFile: localPdfURL) { _, response, _ in if let httpResponse = response as? HTTPURLResponse, 200 ... 299 ~= httpResponse.statusCode { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/SaveService.swift b/apple/OmnivoreKit/Sources/Services/DataService/SaveService.swift deleted file mode 100644 index 8b1378917..000000000 --- a/apple/OmnivoreKit/Sources/Services/DataService/SaveService.swift +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift index f4e267546..a7fe46e3b 100644 --- a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift +++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift @@ -31,7 +31,7 @@ struct InternalLinkedItem { if let contentReader = contentReader { return contentReader == "PDF" } - return (pageURLString ?? "").hasSuffix("pdf") + return pageURLString.hasSuffix("pdf") } func asManagedObject(inContext context: NSManagedObjectContext) -> LinkedItem { diff --git a/apple/OmnivoreKit/Sources/Views/Article/WebView.swift b/apple/OmnivoreKit/Sources/Views/Article/WebView.swift index 32ebc172e..62e6ebd76 100644 --- a/apple/OmnivoreKit/Sources/Views/Article/WebView.swift +++ b/apple/OmnivoreKit/Sources/Views/Article/WebView.swift @@ -40,7 +40,9 @@ public final class WebView: WKWebView { } public func updateMaxWidthPercentage() { - if let maxWidthPercentage = UserDefaults.standard.value(forKey: UserDefaultKey.preferredWebMaxWidthPercentage.rawValue) as? Int { + if let maxWidthPercentage = UserDefaults.standard.value( + forKey: UserDefaultKey.preferredWebMaxWidthPercentage.rawValue + ) as? Int { dispatchEvent(.updateMaxWidthPercentage(maxWidthPercentage: maxWidthPercentage)) } } diff --git a/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift b/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift index 8d6f4774d..bf5186771 100644 --- a/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift +++ b/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift @@ -46,7 +46,11 @@ struct CornerRadiusStyle: ViewModifier { var corners = UIRectCorner.allCorners func path(in rect: CGRect) -> Path { - let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius)) + let path = UIBezierPath( + roundedRect: rect, + byRoundingCorners: corners, + cornerRadii: CGSize(width: radius, height: radius) + ) return Path(path.cgPath) } } diff --git a/apple/OmnivoreKit/Tests/UtilsTests/UtilsTests.swift b/apple/OmnivoreKit/Tests/UtilsTests/UtilsTests.swift index 8439564aa..8034aa1b9 100644 --- a/apple/OmnivoreKit/Tests/UtilsTests/UtilsTests.swift +++ b/apple/OmnivoreKit/Tests/UtilsTests/UtilsTests.swift @@ -14,6 +14,7 @@ final class UtilsTests: XCTestCase { XCTAssertEqual(normalizeURL("https://omnivore.app/"), "https://omnivore.app") // utm_ removed + // swiftlint:disable:next line_length XCTAssertEqual(normalizeURL("https://omnivore.app/?aa=a&bb=b&utm_track=track&cc=c"), "https://omnivore.app?aa=a&bb=b&cc=c") // query params sorted From 6d61c0f9e756714d76fe82358dbbffe3ef7ffbb9 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 16 Jun 2022 22:26:39 -0700 Subject: [PATCH 10/13] resolve warnings --- .../Sources/App/Views/Registration/CreateProfileView.swift | 2 +- apple/OmnivoreKit/Sources/Models/PageScrapePayload.swift | 5 ++--- .../Services/DataService/Mutations/MergeHighlight.swift | 2 +- .../Sources/Services/DataService/OfflineSync.swift | 3 --- apple/OmnivoreKit/Sources/Views/SnackBar.swift | 2 +- 5 files changed, 5 insertions(+), 9 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/CreateProfileView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/CreateProfileView.swift index c54160557..e218f96b2 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Registration/CreateProfileView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Registration/CreateProfileView.swift @@ -168,7 +168,7 @@ struct CreateProfileView: View { .foregroundColor(.red) } } - .animation(.default) + .animation(.default, value: 0.35) VStack(alignment: .leading, spacing: 6) { Text("Bio (optional)") diff --git a/apple/OmnivoreKit/Sources/Models/PageScrapePayload.swift b/apple/OmnivoreKit/Sources/Models/PageScrapePayload.swift index 1b997e82f..12af0214f 100644 --- a/apple/OmnivoreKit/Sources/Models/PageScrapePayload.swift +++ b/apple/OmnivoreKit/Sources/Models/PageScrapePayload.swift @@ -262,9 +262,8 @@ private extension PageScrapePayload { print("error copying file locally", error) } } - // TODO: - // Don't try to handle file URLs that are not PDFs. - // In the future we can add image and other file type support here + + // If file is not a pdf then return nil return nil } return PageScrapePayload(url: url.absoluteString) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/MergeHighlight.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/MergeHighlight.swift index 798fcddd6..cb3e2c0d5 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/MergeHighlight.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/MergeHighlight.swift @@ -11,7 +11,7 @@ extension DataService { quote: String, patch: String, articleId: String, - overlapHighlightIdList: [String] // TODO: pass in annotation? + overlapHighlightIdList: [String] ) -> [String: Any]? { let internalHighlight = InternalHighlight( id: highlightID, diff --git a/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift b/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift index b8660289a..be0294a82 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift @@ -117,9 +117,6 @@ public extension DataService { Task { try await createPageFromPdf(id: id, localPdfURL: localPdfURL, url: url) } - } else { - // TODO: This is an invalid object, we should have a way of reflecting that with an error state - // updateLinkedItemStatus(id: id, status: .) } case "WEB": let id = item.unwrappedID diff --git a/apple/OmnivoreKit/Sources/Views/SnackBar.swift b/apple/OmnivoreKit/Sources/Views/SnackBar.swift index 6c8382d98..d2fee56ed 100644 --- a/apple/OmnivoreKit/Sources/Views/SnackBar.swift +++ b/apple/OmnivoreKit/Sources/Views/SnackBar.swift @@ -36,7 +36,7 @@ public struct Snackbar: View { .cornerRadius(5) .offset(x: 0, y: -8) .shadow(color: .gray, radius: 2) - .animation(Animation.spring()) + .animation(.spring(), value: true) } } } From 5a3bb622084ba88ad1d0aa7f83e84279628c5cbf Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 16 Jun 2022 22:28:13 -0700 Subject: [PATCH 11/13] bump ios version to 1.10.0 --- apple/Omnivore.xcodeproj/project.pbxproj | 36 ++++++++++++------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/apple/Omnivore.xcodeproj/project.pbxproj b/apple/Omnivore.xcodeproj/project.pbxproj index bb47dd924..473fe264d 100644 --- a/apple/Omnivore.xcodeproj/project.pbxproj +++ b/apple/Omnivore.xcodeproj/project.pbxproj @@ -1229,7 +1229,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_TEAM = QJF2XZ86HB; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = InfoPlists/ShareExtensionMac.plist; @@ -1239,7 +1239,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.9.0; + MARKETING_VERSION = 1.10.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "app.omnivore.app.ShareExtension-Mac"; @@ -1260,7 +1260,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_TEAM = QJF2XZ86HB; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = InfoPlists/ShareExtensionMac.plist; @@ -1270,7 +1270,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.9.0; + MARKETING_VERSION = 1.10.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "app.omnivore.app.ShareExtension-Mac"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1341,7 +1341,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = QJF2XZ86HB; ENABLE_HARDENED_RUNTIME = YES; @@ -1352,7 +1352,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.9.0; + MARKETING_VERSION = 1.10.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app; @@ -1375,7 +1375,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = QJF2XZ86HB; ENABLE_HARDENED_RUNTIME = YES; @@ -1386,7 +1386,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.9.0; + MARKETING_VERSION = 1.10.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1441,7 +1441,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.9.0; + MARKETING_VERSION = 1.10.0; PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app; PRODUCT_NAME = Omnivore; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1473,7 +1473,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.9.0; + MARKETING_VERSION = 1.10.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_LDFLAGS = ( @@ -1512,7 +1512,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.9.0; + MARKETING_VERSION = 1.10.0; MTL_FAST_MATH = YES; OTHER_LDFLAGS = ( "-framework", @@ -1538,7 +1538,7 @@ CODE_SIGN_ENTITLEMENTS = "Entitlements/SafariExtension-Mac.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_TEAM = QJF2XZ86HB; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; @@ -1551,7 +1551,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.9.0; + MARKETING_VERSION = 1.10.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_LDFLAGS = ( @@ -1576,7 +1576,7 @@ CODE_SIGN_ENTITLEMENTS = "Entitlements/SafariExtension-Mac.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_TEAM = QJF2XZ86HB; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; @@ -1589,7 +1589,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.9.0; + MARKETING_VERSION = 1.10.0; MTL_FAST_MATH = YES; OTHER_LDFLAGS = ( "-framework", @@ -1674,7 +1674,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.9.0; + MARKETING_VERSION = 1.10.0; PRODUCT_BUNDLE_IDENTIFIER = "app.omnivore.app.share-extension"; PRODUCT_NAME = ShareExtension; SDKROOT = iphoneos; @@ -1728,7 +1728,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.9.0; + MARKETING_VERSION = 1.10.0; PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app; PRODUCT_NAME = Omnivore; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1756,7 +1756,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.9.0; + MARKETING_VERSION = 1.10.0; PRODUCT_BUNDLE_IDENTIFIER = "app.omnivore.app.share-extension"; PRODUCT_NAME = ShareExtension; SDKROOT = iphoneos; From 7223c38c69e6d1147ec2a3db54ff7028ef517b2e Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 17 Jun 2022 13:59:39 -0700 Subject: [PATCH 12/13] update graphql swift schema --- .../Services/DataService/GQLSchema.swift | 51 ++++++++++++++----- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift index b14558c77..d0242d354 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift @@ -11333,6 +11333,7 @@ extension Objects { let readAt: [String: DateTime] let readingProgressAnchorIndex: [String: Int] let readingProgressPercent: [String: Double] + let savedAt: [String: DateTime] let shortId: [String: String] let siteName: [String: String] let slug: [String: String] @@ -11439,6 +11440,10 @@ extension Objects.SearchItem: Decodable { if let value = try container.decode(Double?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } + case "savedAt": + if let value = try container.decode(DateTime?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } case "shortId": if let value = try container.decode(String?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -11512,6 +11517,7 @@ extension Objects.SearchItem: Decodable { readAt = map["readAt"] readingProgressAnchorIndex = map["readingProgressAnchorIndex"] readingProgressPercent = map["readingProgressPercent"] + savedAt = map["savedAt"] shortId = map["shortId"] siteName = map["siteName"] slug = map["slug"] @@ -11798,7 +11804,7 @@ extension Fields where TypeLock == Objects.SearchItem { } } - func readingProgressAnchorIndex() throws -> Int? { + func readingProgressAnchorIndex() throws -> Int { let field = GraphQLField.leaf( name: "readingProgressAnchorIndex", arguments: [] @@ -11807,13 +11813,16 @@ extension Fields where TypeLock == Objects.SearchItem { switch response { case let .decoding(data): - return data.readingProgressAnchorIndex[field.alias!] + if let data = data.readingProgressAnchorIndex[field.alias!] { + return data + } + throw HttpError.badpayload case .mocking: - return nil + return Int.mockValue } } - func readingProgressPercent() throws -> Double? { + func readingProgressPercent() throws -> Double { let field = GraphQLField.leaf( name: "readingProgressPercent", arguments: [] @@ -11822,9 +11831,30 @@ extension Fields where TypeLock == Objects.SearchItem { switch response { case let .decoding(data): - return data.readingProgressPercent[field.alias!] + if let data = data.readingProgressPercent[field.alias!] { + return data + } + throw HttpError.badpayload case .mocking: - return nil + return Double.mockValue + } + } + + func savedAt() throws -> DateTime { + let field = GraphQLField.leaf( + name: "savedAt", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.savedAt[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return DateTime.mockValue } } @@ -11954,7 +11984,7 @@ extension Fields where TypeLock == Objects.SearchItem { } } - func updatedAt() throws -> DateTime { + func updatedAt() throws -> DateTime? { let field = GraphQLField.leaf( name: "updatedAt", arguments: [] @@ -11963,12 +11993,9 @@ extension Fields where TypeLock == Objects.SearchItem { switch response { case let .decoding(data): - if let data = data.updatedAt[field.alias!] { - return data - } - throw HttpError.badpayload + return data.updatedAt[field.alias!] case .mocking: - return DateTime.mockValue + return nil } } From 3031baa94dfa360295ca16c528a84813031514f9 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 17 Jun 2022 14:04:26 -0700 Subject: [PATCH 13/13] use search graphql query rather than articles --- .../Queries/LinkedItemNetworkQuery.swift | 50 +++++++++++++------ 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift index bc40476a4..19fa42483 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift @@ -25,15 +25,15 @@ extension DataService { case error(error: String) } - let selection = Selection { + let selection = Selection { try $0.on( - articlesError: .init { + searchError: .init { QueryResult.error(error: try $0.errorCodes().description) }, - articlesSuccess: .init { + searchSuccess: .init { QueryResult.success( result: InternalLinkedItemQueryResult( - items: try $0.edges(selection: articleEdgeSelection.list), + items: try $0.edges(selection: searchItemEdgeSelection.list), cursor: try $0.pageInfo(selection: Selection.PageInfo { try $0.endCursor() }) @@ -44,18 +44,10 @@ extension DataService { } let query = Selection.Query { - try $0.articles( + try $0.search( after: OptionalArgument(cursor), first: OptionalArgument(limit), - includePending: OptionalArgument(true), query: OptionalArgument(searchQuery), - sharedOnly: .present(false), - sort: OptionalArgument( - InputObjects.SortParams( - by: .updatedTime, - order: .present(.descending) - ) - ), selection: selection ) } @@ -160,6 +152,34 @@ private let libraryArticleSelection = Selection.Article { ) } -private let articleEdgeSelection = Selection.ArticleEdge { - try $0.node(selection: libraryArticleSelection) +private let searchItemSelection = Selection.SearchItem { + InternalLinkedItem( + id: try $0.id(), + title: try $0.title(), + createdAt: try $0.createdAt().value ?? Date(), + savedAt: try $0.savedAt().value ?? Date(), + readAt: try $0.readAt()?.value, + updatedAt: try $0.updatedAt()?.value ?? Date(), + state: try $0.state()?.rawValue.asArticleContentStatus ?? .succeeded, + readingProgress: try $0.readingProgressPercent(), + readingProgressAnchor: try $0.readingProgressAnchorIndex(), + imageURLString: try $0.image(), + onDeviceImageURLString: nil, + documentDirectoryPath: nil, + pageURLString: try $0.url(), + descriptionText: try $0.description(), + publisherURLString: try $0.originalArticleUrl(), + siteName: try $0.siteName(), + author: try $0.author(), + publishDate: try $0.publishedAt()?.value, + slug: try $0.slug(), + isArchived: try $0.isArchived(), + contentReader: try $0.contentReader().rawValue, + originalHtml: nil, + labels: try $0.labels(selection: feedItemLabelSelection.list.nullable) ?? [] + ) +} + +private let searchItemEdgeSelection = Selection.SearchItemEdge { + try $0.node(selection: searchItemSelection) }