From 974757c7da725c91219efabee8ffa17ff7723947 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 27 May 2022 16:38:19 -0700 Subject: [PATCH 01/32] Improve background uploading of PDFs --- .../Share/ShareExtensionScene.swift | 15 ++++- .../DataService/Mutations/SavePDF.swift | 59 ++++++++++++------- .../DataService/Networking/Networker.swift | 25 +++++++- 3 files changed, 71 insertions(+), 28 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift index d3fa98d5f..1566bcf91 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift @@ -26,6 +26,7 @@ final class ShareExtensionViewModel: ObservableObject { @Published var debugText: String? var subscriptions = Set() + var backgroundTask: UIBackgroundTaskIdentifier? let requestID = UUID().uuidString.lowercased() init() {} @@ -41,11 +42,16 @@ final class ShareExtensionViewModel: ObservableObject { } func savePage(extensionContext: NSExtensionContext?) { + backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "BACKGROUND") + PageScraper.scrape(extensionContext: extensionContext) { [weak self] result in switch result { case let .success(payload): self?.persist(pageScrapePayload: payload, requestId: self?.requestID ?? "") case let .failure(error): + if let backgroundTask = self?.backgroundTask { + UIApplication.shared.endBackgroundTask(backgroundTask) + } self?.debugText = error.message } } @@ -59,7 +65,6 @@ final class ShareExtensionViewModel: ObservableObject { return } - let backgroundTask = UIApplication.shared.beginBackgroundTask(withName: requestId) let saveLinkPublisher: AnyPublisher = { if case let .pdf(data) = pageScrapePayload.contentType { return services.dataService.uploadPDFPublisher(pageScrapePayload: pageScrapePayload, @@ -80,10 +85,14 @@ final class ShareExtensionViewModel: ObservableObject { guard case let .failure(error) = completion else { return } self?.debugText = "saveArticleError: \(error)" self?.status = .failed(error: error) - UIApplication.shared.endBackgroundTask(backgroundTask) + if let backgroundTask = self?.backgroundTask { + UIApplication.shared.endBackgroundTask(backgroundTask) + } } receiveValue: { [weak self] _ in self?.status = .success - UIApplication.shared.endBackgroundTask(backgroundTask) + if let backgroundTask = self?.backgroundTask { + UIApplication.shared.endBackgroundTask(backgroundTask) + } } .store(in: &subscriptions) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePDF.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePDF.swift index 4267b5a6f..df0511ed6 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePDF.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePDF.swift @@ -64,6 +64,7 @@ private extension DataService { return Deferred { Future { promise in send(mutation, to: path, headers: headers) { result in + print("result of upload file request", result) switch result { case let .success(payload): if let graphqlError = payload.errors { @@ -100,29 +101,43 @@ private extension DataService { request.addValue("application/pdf", forHTTPHeaderField: "content-type") request.httpBody = data - return networker.urlSession.dataTaskPublisher(for: request) - .tryMap { data, response -> String in - let serverResponse = ServerResponse(data: data, response: response) - if serverResponse.httpUrlResponse?.statusCode == 200, let fileUploadID = fileUploadConfig.uploadID { - return fileUploadID - } + // TODO: Maybe better to copy into this directory immediately + // instead of loading and writing the data + let tempDir = FileManager.default.temporaryDirectory + let localURL = tempDir.appendingPathComponent(fileUploadConfig.uploadFileID ?? "temporary") + try? data.write(to: localURL) - throw ServerError(serverResponse: serverResponse) - } - .mapError { error -> SaveArticleError in - let serverResponse = ServerResponse(error: error) - NetworkRequestLogger.log(request: request, serverResponse: serverResponse) - let serverError = ServerError(serverResponse: serverResponse) - switch serverError { - case .noConnection, .timeout: - return .network - case .unauthenticated: - return .unauthorized - case .unknown: - return .unknown(description: "upload to file server failed") - } - } - .eraseToAnyPublisher() + print("STARTING UPLOAD TASK WITH LOCAL URL", localURL) + + let task = networker.backgroundSession.uploadTask(with: request, fromFile: localURL) + task.resume() + + // Just return immediately at this point. + return Empty(completeImmediately: true).eraseToAnyPublisher() +// return "".publisher.eraseToAnyPublisher() +// return networker.urlSession.dataTaskPublisher(for: request) +// .tryMap { data, response -> String in +// let serverResponse = ServerResponse(data: data, response: response) +// if serverResponse.httpUrlResponse?.statusCode == 200, let fileUploadID = fileUploadConfig.uploadID { +// return fileUploadID +// } +// +// throw ServerError(serverResponse: serverResponse) +// } +// .mapError { error -> SaveArticleError in +// let serverResponse = ServerResponse(error: error) +// NetworkRequestLogger.log(request: request, serverResponse: serverResponse) +// let serverError = ServerError(serverResponse: serverResponse) +// switch serverError { +// case .noConnection, .timeout: +// return .network +// case .unauthenticated: +// return .unauthorized +// case .unknown: +// return .unknown(description: "upload to file server failed") +// } +// } +// .eraseToAnyPublisher() } // swiftlint:disable:next line_length diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Networking/Networker.swift b/apple/OmnivoreKit/Sources/Services/DataService/Networking/Networker.swift index 4f2869e23..cdf683d3e 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Networking/Networker.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Networking/Networker.swift @@ -1,7 +1,7 @@ import Foundation import Models -public final class Networker { +public final class Networker: NSObject, URLSessionTaskDelegate { let urlSession: URLSession let appEnvironment: AppEnvironment @@ -15,9 +15,28 @@ public final class Networker { return headers } - public init(appEnvironment: AppEnvironment, urlSession: URLSession = .shared) { + public init(appEnvironment: AppEnvironment) { self.appEnvironment = appEnvironment - self.urlSession = urlSession + self.urlSession = .shared + } + + lazy var backgroundSession: URLSession = { + let sessionConfig = URLSessionConfiguration.background(withIdentifier: "app.omnivoreapp.BackgroundSessionConfig") + sessionConfig.sharedContainerIdentifier = "group.app.omnivoreapp" + return URLSession(configuration: sessionConfig, delegate: self, delegateQueue: nil) + }() + + public func urlSession(_: URLSession, task: URLSessionTask, didCompleteWithError _: Error?) { + print("finished upload of file:", task.taskIdentifier) + } + + public func urlSession(_: URLSession, + task: URLSessionTask, + didSendBodyData _: Int64, + totalBytesSent: Int64, + totalBytesExpectedToSend _: Int64) + { + print("sent background data:", task.taskIdentifier, totalBytesSent) } } From a17cb72527833e2f7128825deaede5456cd3a8d5 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 30 May 2022 09:16:33 -0700 Subject: [PATCH 02/32] Use the request ID as the background task ID --- .../Sources/App/AppExtensions/Share/ShareExtensionScene.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift index 1566bcf91..0a3c78217 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift @@ -42,7 +42,7 @@ final class ShareExtensionViewModel: ObservableObject { } func savePage(extensionContext: NSExtensionContext?) { - backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "BACKGROUND") + backgroundTask = UIApplication.shared.beginBackgroundTask(withName: requestID) PageScraper.scrape(extensionContext: extensionContext) { [weak self] result in switch result { From bb86447451b0df66cf0faacc3565b1d96df30b68 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 30 May 2022 09:34:40 -0700 Subject: [PATCH 03/32] Fix params list on SavePage call --- .../Services/DataService/Mutations/SavePage.swift | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePage.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePage.swift index b3ae4fc12..9f34c2268 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePage.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePage.swift @@ -12,16 +12,19 @@ public extension DataService { } let input = InputObjects.SavePageInput( - url: requestId, - source: html, - clientRequestId: "ios-page", + url: pageScrapePayload.url, + source: "ios-page", + clientRequestId: requestId, title: OptionalArgument(title), - originalContent: pageScrapePayload.url + originalContent: html ) let selection = Selection { try $0.on( - saveSuccess: .init { .saved(requestId: requestId, url: (try? $0.url()) ?? "") }, saveError: .init { .error(errorCode: (try? $0.errorCodes().first) ?? .unknown) } + saveSuccess: .init { .saved(requestId: requestId, url: (try? $0.url()) ?? "") }, + saveError: .init { + .error(errorCode: (try? $0.errorCodes().first) ?? .unknown) + } ) } From 53c423b71410af9e849141b2519cd81ad41512dc Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 30 May 2022 22:30:43 -0700 Subject: [PATCH 04/32] WIP: upload PDFs using background task --- .../Share/ShareExtensionScene.swift | 118 +++++++++++------- .../App/PDFSupport/PDFViewerViewModel.swift | 36 +++--- .../CoreDataModel.xcdatamodel/contents | 6 +- .../Sources/Models/DataModels/FeedItem.swift | 1 + .../Sources/Models/DataModels/PDFItem.swift | 4 +- .../Sources/Models/PageScrapePayload.swift | 26 +++- .../Services/DataService/DataService.swift | 117 +++++++++++++++++ .../FetchLinkedItemsBackgroundTask.swift | 1 + .../DataService/Mutations/SavePDF.swift | 63 +++++----- .../DataService/Networking/Networker.swift | 8 +- .../Services/DataService/OfflineSync.swift | 24 +++- .../Queries/ArticleContentQuery.swift | 12 +- .../Queries/LibraryItemsQuery.swift | 2 + .../InternalModels/InternalLinkedItem.swift | 4 + .../Sources/Views/ShareExtensionView.swift | 20 +-- packages/api/src/routers/page_router.ts | 118 ++++++++++++++++++ packages/api/src/server.ts | 2 + packages/api/src/utils/helpers.ts | 21 ++++ packages/api/src/utils/uploads.ts | 32 ++--- 19 files changed, 489 insertions(+), 126 deletions(-) create mode 100644 packages/api/src/routers/page_router.ts diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift index 0a3c78217..e52e92eda 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift @@ -22,7 +22,7 @@ public extension PlatformViewController { final class ShareExtensionViewModel: ObservableObject { @Published var title: String? - @Published var status: ShareExtensionStatus = FeatureFlag.enableReadNow ? .processing : .success + @Published var status: ShareExtensionStatus = .processing @Published var debugText: String? var subscriptions = Set() @@ -60,53 +60,83 @@ final class ShareExtensionViewModel: ObservableObject { private func persist(pageScrapePayload: PageScrapePayload, requestId: String) { let services = Services() - guard services.authenticator.hasValidAuthToken else { - status = .failed(error: .unauthorized) - return - } - - let saveLinkPublisher: AnyPublisher = { - if case let .pdf(data) = pageScrapePayload.contentType { - return services.dataService.uploadPDFPublisher(pageScrapePayload: pageScrapePayload, - data: data, - requestId: requestId) - } else if case let .html(html, title) = pageScrapePayload.contentType { - return services.dataService.savePagePublisher(pageScrapePayload: pageScrapePayload, - html: html, - title: title, - requestId: requestId) - } else { - return services.dataService.saveUrlPublisher(pageScrapePayload: pageScrapePayload, requestId: requestId) - } - }() - - saveLinkPublisher - .sink { [weak self] completion in - guard case let .failure(error) = completion else { return } - self?.debugText = "saveArticleError: \(error)" - self?.status = .failed(error: error) - if let backgroundTask = self?.backgroundTask { - UIApplication.shared.endBackgroundTask(backgroundTask) - } - } receiveValue: { [weak self] _ in - self?.status = .success - if let backgroundTask = self?.backgroundTask { - UIApplication.shared.endBackgroundTask(backgroundTask) - } - } - .store(in: &subscriptions) - - // Check connection to get fast feedback for auth/network errors Task { - let hasConnectionAndValidToken = await services.dataService.hasConnectionAndValidToken() - - if !hasConnectionAndValidToken { - DispatchQueue.main.async { - self.debugText = "saveArticleError: No connection or invalid token." - self.status = .failed(error: .unknown(description: "")) + do { + // Save locally, then attempt to sync to the server + let item = try await services.dataService.persistPageScrapePayload(pageScrapePayload, requestId: requestId) + // TODO: need to update this on the main thread and handle the result == false case here + if item != nil { + self.status = .saved + } else { + self.status = .failed(error: SaveArticleError.unknown(description: "Unable to save page")) + return } + + // force a server sync + if let item = item { + let syncResult = services.dataService.syncLocalCreatedLinkedItem(item: item) + print("RESULT", syncResult) + } +// self.status = .synced +// } else { +// self.status = .syncFailed(error: SaveArticleError.unknown(description: "Unable to sync page")) +// } + + } catch { + print("ERROR SAVING PAGE", error) } } + // First persist to Core Data + // services.dataService.persist(jsonArticle: article) + +// +// guard services.authenticator.hasValidAuthToken else { +// status = .failed(error: .unauthorized) +// return +// } +// +// let saveLinkPublisher: AnyPublisher = { +// if case let .pdf(data) = pageScrapePayload.contentType { +// return services.dataService.uploadPDFPublisher(pageScrapePayload: pageScrapePayload, +// data: data, +// requestId: requestId) +// } else if case let .html(html, title) = pageScrapePayload.contentType { +// return services.dataService.savePagePublisher(pageScrapePayload: pageScrapePayload, +// html: html, +// title: title, +// requestId: requestId) +// } else { +// return services.dataService.saveUrlPublisher(pageScrapePayload: pageScrapePayload, requestId: requestId) +// } +// }() +// +// saveLinkPublisher +// .sink { [weak self] completion in +// guard case let .failure(error) = completion else { return } +// self?.debugText = "saveArticleError: \(error)" +// self?.status = .failed(error: error) +// if let backgroundTask = self?.backgroundTask { +// UIApplication.shared.endBackgroundTask(backgroundTask) +// } +// } receiveValue: { [weak self] _ in +// self?.status = .success +// if let backgroundTask = self?.backgroundTask { +// UIApplication.shared.endBackgroundTask(backgroundTask) +// } +// } +// .store(in: &subscriptions) +// +// // Check connection to get fast feedback for auth/network errors +// Task { +// let hasConnectionAndValidToken = await services.dataService.hasConnectionAndValidToken() +// +// if !hasConnectionAndValidToken { +// DispatchQueue.main.async { +// self.debugText = "saveArticleError: No connection or invalid token." +// self.status = .failed(error: .unknown(description: "")) +// } +// } +// } } } diff --git a/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift b/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift index 2d0b70cbf..e8777e0b9 100644 --- a/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift @@ -18,25 +18,29 @@ public final class PDFViewerViewModel: ObservableObject { } public func dataURL(remoteURL: URL) -> URL { - if let storedURL = storedURL { + // TODO: we should probably not reach this point of localPdfURL is not set + // this means the PDF has not been downloaded yet, so we likely don't have + // a valid URL to work with. + + if let storedURL = pdfItem.localPdfURL { return storedURL } - guard let data = pdfItem.documentData else { return remoteURL } - - let subPath = pdfItem.title.isEmpty ? UUID().uuidString : pdfItem.title - - let path = FileManager.default - .urls(for: .cachesDirectory, in: .userDomainMask)[0] - .appendingPathComponent(subPath) - - do { - try data.write(to: path) - storedURL = path - return path - } catch { - return remoteURL - } +// guard let data = pdfItem.documentData else { return remoteURL } +// +// let subPath = pdfItem.title.isEmpty ? UUID().uuidString : pdfItem.title +// +// let path = FileManager.default +// .urls(for: .cachesDirectory, in: .userDomainMask)[0] +// .appendingPathComponent(subPath) +// +// do { +// try data.write(to: path) +// storedURL = path +// return path +// } catch { + return remoteURL + // } } public func loadHighlightPatches(completion onComplete: @escaping ([String]) -> Void) { diff --git a/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents b/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents index 3556a931c..3b0ea0639 100644 --- a/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents +++ b/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -29,7 +29,9 @@ + + @@ -86,7 +88,7 @@ - + diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift b/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift index 934e545c5..fb877bfb8 100644 --- a/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift +++ b/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift @@ -31,6 +31,7 @@ public extension LinkedItem { var unwrappedSlug: String { slug ?? "" } var unwrappedTitle: String { title ?? "" } var unwrappedPageURLString: String { pageURLString ?? "" } + var unwrappedLocalPdfURL: String { localPdfURL ?? "" } var unwrappedSavedAt: Date { savedAt ?? Date() } var unwrappedCreatedAt: Date { createdAt ?? Date() } diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/PDFItem.swift b/apple/OmnivoreKit/Sources/Models/DataModels/PDFItem.swift index 2953a2f73..5dee64dd0 100644 --- a/apple/OmnivoreKit/Sources/Models/DataModels/PDFItem.swift +++ b/apple/OmnivoreKit/Sources/Models/DataModels/PDFItem.swift @@ -5,7 +5,7 @@ public struct PDFItem { public let objectID: NSManagedObjectID public let itemID: String public let pdfURL: URL? - public let documentData: Data? + public let localPdfURL: URL? public let title: String public let slug: String public let readingProgress: Double @@ -22,7 +22,7 @@ public struct PDFItem { objectID: item.objectID, itemID: item.unwrappedID, pdfURL: URL(string: item.unwrappedPageURLString), - documentData: item.pdfData, + localPdfURL: URL(string: item.unwrappedLocalPdfURL), title: item.unwrappedID, slug: item.unwrappedSlug, readingProgress: item.readingProgress, diff --git a/apple/OmnivoreKit/Sources/Models/PageScrapePayload.swift b/apple/OmnivoreKit/Sources/Models/PageScrapePayload.swift index 27b051346..f5bd16e6e 100644 --- a/apple/OmnivoreKit/Sources/Models/PageScrapePayload.swift +++ b/apple/OmnivoreKit/Sources/Models/PageScrapePayload.swift @@ -18,7 +18,7 @@ public struct PageScrapePayload { public enum ContentType { case none case html(html: String, title: String?) - case pdf(data: Data) + case pdf(localUrl: URL) } public let url: String @@ -29,9 +29,9 @@ public struct PageScrapePayload { self.contentType = .none } - init(url: String, pdfData: Data) { + init(url: String, localUrl: URL) { self.url = url - self.contentType = .pdf(data: pdfData) + self.contentType = .pdf(localUrl: localUrl) } init(url: String, title: String?, html: String) { @@ -248,12 +248,28 @@ private extension PageScrapePayload { return nil } + static func sharedContainerURL() -> URL { + FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: "group.app.omnivoreapp" + )! + } + static func makeFromURL(_ url: URL) -> PageScrapePayload? { if url.isFileURL { let type = try? url.resourceValues(forKeys: [.typeIdentifierKey]).typeIdentifier - if type == UTType.pdf.identifier, let data = try? Data(contentsOf: url) { - return PageScrapePayload(url: url.absoluteString, pdfData: data) + if type == UTType.pdf.identifier { + // Copy PDFs into a temporary file where they are staged for processing. + var dest = sharedContainerURL() + let localFile = UUID().uuidString.lowercased() + ".pdf" + dest.appendPathComponent(localFile) + do { + try FileManager.default.copyItem(at: url, to: dest) + return PageScrapePayload(url: url.absoluteString, localUrl: dest) + } catch { + 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 return nil diff --git a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift index 8c8f02ed1..4514916d9 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift @@ -1,8 +1,11 @@ import Combine import CoreData +import CoreImage import Foundation import Models import OSLog +import QuickLookThumbnailing +import UIKit import Utils let logger = Logger(subsystem: "app.omnivore", category: "data-service") @@ -34,6 +37,7 @@ public final class DataService: ObservableObject { } else { persistentContainer.loadPersistentStores { _, error in if let error = error { + print("error", error) fatalError("Core Data store failed to load with error: \(error)") } } @@ -97,4 +101,117 @@ public final class DataService: ObservableObject { UserDefaults.standard.set(appVersion, forKey: UserDefaultKey.lastUsedAppVersion.rawValue) return isFirstRun } + + public func persistPageScrapePayload(_ pageScrape: PageScrapePayload, requestId: String) async throws -> LinkedItem? { + try await backgroundContext.perform { [weak self] in + guard let self = self else { return nil } + let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "id == %@", requestId) + + let currentTime = Date() + let existingItem = try? self.backgroundContext.fetch(fetchRequest).first + let linkedItem = existingItem ?? LinkedItem(entity: LinkedItem.entity(), insertInto: self.backgroundContext) + + linkedItem.id = requestId + linkedItem.title = pageScrape.url + linkedItem.pageURLString = pageScrape.url + linkedItem.serverSyncStatus = Int64(ServerSyncStatus.needsCreation.rawValue) + linkedItem.savedAt = currentTime + linkedItem.createdAt = currentTime + linkedItem.isArchived = false + + linkedItem.imageURLString = nil + linkedItem.onDeviceImageURLString = nil + linkedItem.descriptionText = nil + linkedItem.publisherURLString = nil + linkedItem.author = nil + linkedItem.publishDate = nil + + if let currentViewer = self.currentViewer { + linkedItem.slug = "\(currentViewer)/\(requestId)" + } else { + // Technically this is invalid, but I don't think slug is used at all locally anymore + linkedItem.slug = requestId + } + + switch pageScrape.contentType { + case let .pdf(localUrl): + print("SAVING PDF", localUrl) + + linkedItem.contentReader = "PDF" + linkedItem.localPdfURL = localUrl.absoluteString + linkedItem.title = self.titleFromPdfFile(pageScrape.url) + +// TODO: Attempt to set thumbnail from PDF data +// let thumbnailUrl = DataService.thumbnailUrl(localUrl: localUrl) +// self.createThumbnailFor(inputUrl: localUrl, at: thumbnailUrl) +// linkedItem.imageURLString = thumbnailUrl.absoluteString + + case let .html(html: html, title: title): + print("SAVING HTML", html, title ?? "no title") + linkedItem.contentReader = "WEB" + linkedItem.originalHtml = html + linkedItem.title = title ?? self.titleFromPdfFile(pageScrape.url) + case .none: + print("SAVING NONE TYPE") + throw BasicError.message(messageText: "Attempting to save none type") + } + + do { + try self.backgroundContext.save() + logger.debug("ArticleContent saved succesfully") + } catch { + self.backgroundContext.rollback() + + print("Failed to save ArticleContent", error.localizedDescription, error) + throw error + } + return linkedItem + } + } + + func titleFromPdfFile(_ urlStr: String) -> String { + let url = URL(string: urlStr) + if let url = url { + return url.lastPathComponent + } + return urlStr + } + + func titleFromUrl(_ urlStr: String) -> String { + let url = URL(string: urlStr) + if let url = url { + return url.lastPathComponent + } + return urlStr + } + + func thumbnailUrl(localUrl: URL) -> URL { + var thumbnailUrl = localUrl + thumbnailUrl.appendPathExtension(".pdf") + return thumbnailUrl + } + + // TODO: we can try to use this to create PDF thumbnails locally + func createThumbnailFor(inputUrl: URL, at outputUrl: URL) { + let size = CGSize(width: 80, height: 80) + let scale = UIScreen.main.scale + + // Create the thumbnail request. + let request = + QLThumbnailGenerator.Request( + fileAt: inputUrl, + size: size, + scale: scale, + representationTypes: .all + ) + + // Retrieve the singleton instance of the thumbnail generator and generate the thumbnails. + let generator = QLThumbnailGenerator.shared + generator.saveBestRepresentation(for: request, to: outputUrl, contentType: UTType.jpeg.identifier) { error in + if let error = error { + print(error.localizedDescription) + } + } + } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/FetchLinkedItemsBackgroundTask.swift b/apple/OmnivoreKit/Sources/Services/DataService/FetchLinkedItemsBackgroundTask.swift index 939f3330e..e96d03a6c 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/FetchLinkedItemsBackgroundTask.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/FetchLinkedItemsBackgroundTask.swift @@ -33,6 +33,7 @@ extension DataService { let maxItemCount = 30 let fetchResult = try await fetchLinkedItemIDs(limit: 10, cursor: cursor) let newItemsToFetch = await itemsNotInStore(from: fetchResult.itemIDs) + print("newItemsToFetch", newItemsToFetch) let itemsToFetch = previouslyFetchedIDs + newItemsToFetch if newItemsToFetch.isEmpty || itemsToFetch.count > maxItemCount || fetchResult.cursor == nil { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePDF.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePDF.swift index df0511ed6..a6cbfb5d8 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePDF.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePDF.swift @@ -4,18 +4,19 @@ import Models import SwiftGraphQL public extension DataService { - func uploadPDFPublisher( - pageScrapePayload: PageScrapePayload, - data: Data, - requestId: String - ) -> AnyPublisher { - uploadFileRequestPublisher(pageScrapePayload: pageScrapePayload, requestId: requestId) - .flatMap { self.uploadFilePublisher(fileUploadConfig: $0, data: data) } - .flatMap { self.saveFilePublisher(pageScrapePayload: pageScrapePayload, uploadFileId: $0, requestId: requestId) } - .catch { _ in self.saveUrlPublisher(pageScrapePayload: pageScrapePayload, requestId: requestId) } - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() - } +// func uploadPDFPublisher( +// pageScrapePayload: PageScrapePayload, +// data: Data, +// requestId: String +// ) -> AnyPublisher { +// // uploadFileRequestPublisher(pageScrapePayload: pageScrapePayload, requestId: requestId) +// // .flatMap { self.uploadFilePublisher(fileUploadConfig: $0, data: data) } +// uploadFilePublisher(pageScrapePayload: pageScrapePayload, requestId: requestId, data: data) +// .flatMap { self.saveFilePublisher(pageScrapePayload: pageScrapePayload, uploadFileId: $0, requestId: requestId) } +// .catch { _ in self.saveUrlPublisher(pageScrapePayload: pageScrapePayload, requestId: requestId) } +// .receive(on: DispatchQueue.main) +// .eraseToAnyPublisher() +// } } private struct UploadFileRequestPayload { @@ -26,17 +27,18 @@ private struct UploadFileRequestPayload { private extension DataService { // swiftlint:disable:next line_length - func uploadFileRequestPublisher(pageScrapePayload: PageScrapePayload, requestId: String?) -> AnyPublisher { + // func uploadFileRequestPublisher(pageScrapePayload: PageScrapePayload, requestId: String?) -> AnyPublisher { + func uploadFileRequestPublisher(item: LinkedItem) -> AnyPublisher { enum MutationResult { case success(payload: UploadFileRequestPayload) case error(errorCode: Enums.UploadFileRequestErrorCode?) } let input = InputObjects.UploadFileRequestInput( - url: pageScrapePayload.url, + url: item.unwrappedPageURLString, contentType: "application/pdf", createPageEntry: OptionalArgument(true), - clientRequestId: OptionalArgument(requestId) + clientRequestId: OptionalArgument(item.unwrappedID) ) let selection = Selection { @@ -92,25 +94,26 @@ private extension DataService { } // swiftlint:disable:next line_length - func uploadFilePublisher(fileUploadConfig: UploadFileRequestPayload, data: Data) -> AnyPublisher { - let url = fileUploadConfig.urlString.flatMap { URL(string: $0) } - guard let url = url else { return Future { $0(.failure(.badData)) }.eraseToAnyPublisher() } + public func uploadFilePublisher(item: LinkedItem) -> AnyPublisher { + var urlComponents = URLComponents(url: appEnvironment.serverBaseURL, resolvingAgainstBaseURL: true)! + // let headers = networker.defaultHeaders - var request = URLRequest(url: url) - request.httpMethod = "PUT" - request.addValue("application/pdf", forHTTPHeaderField: "content-type") - request.httpBody = data + urlComponents.path = "/api/page/pdf" + urlComponents.queryItems = [URLQueryItem(name: "url", value: item.pageURLString), URLQueryItem(name: "clientRequestId", value: item.unwrappedID)] - // TODO: Maybe better to copy into this directory immediately - // instead of loading and writing the data - let tempDir = FileManager.default.temporaryDirectory - let localURL = tempDir.appendingPathComponent(fileUploadConfig.uploadFileID ?? "temporary") - try? data.write(to: localURL) + if let localPdfURL = item.localPdfURL, let localUrl = URL(string: localPdfURL) { + print("UPLOADING TO URL", urlComponents.url) + var request = URLRequest(url: urlComponents.url!) + request.httpMethod = "PUT" - print("STARTING UPLOAD TASK WITH LOCAL URL", localURL) + networker.defaultHeaders.forEach { (key: String, value: String) in + request.addValue(value, forHTTPHeaderField: key) + } + request.setValue("application/pdf", forHTTPHeaderField: "content-type") - let task = networker.backgroundSession.uploadTask(with: request, fromFile: localURL) - task.resume() + let task = networker.backgroundSession.uploadTask(with: request, fromFile: localUrl) + task.resume() + } // Just return immediately at this point. return Empty(completeImmediately: true).eraseToAnyPublisher() diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Networking/Networker.swift b/apple/OmnivoreKit/Sources/Services/DataService/Networking/Networker.swift index cdf683d3e..a94e514e6 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Networking/Networker.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Networking/Networker.swift @@ -26,8 +26,12 @@ public final class Networker: NSObject, URLSessionTaskDelegate { return URLSession(configuration: sessionConfig, delegate: self, delegateQueue: nil) }() - public func urlSession(_: URLSession, task: URLSessionTask, didCompleteWithError _: Error?) { - print("finished upload of file:", task.taskIdentifier) + public func urlSession(_: URLSession, task: URLSessionTask, didCompleteWithError: Error?) { + if let httpResponse = task.response as? HTTPURLResponse { + print("httpRespinse status code", httpResponse.statusCode) + print("current Request", task.currentRequest?.url) + } + print("finished upload of file:", task.taskIdentifier, task.currentRequest, task.response, "with error", didCompleteWithError) } public func urlSession(_: URLSession, diff --git a/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift b/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift index e64cee327..d55eb6c9f 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift @@ -35,12 +35,34 @@ extension DataService { } } + public func syncLocalCreatedLinkedItem(item: LinkedItem) { + switch item.contentReader { + case "PDF": + // SaveFile + uploadFilePublisher(item: item) + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + case "WEB": + if item.originalHtml != nil { + // SavePage + } else { + // SaveURL + } + case .none: + print("NONE HANDLER") + case .some: + print("SOME HANDLER") + } + } + private func syncLinkedItems(unsyncedLinkedItems: [LinkedItem]) { for item in unsyncedLinkedItems { guard let syncStatus = ServerSyncStatus(rawValue: Int(item.serverSyncStatus)) else { continue } switch syncStatus { - case .isNSync, .isSyncing, .needsCreation: + case .needsCreation: + print("SYNCING LINKED ITEM", unsyncedLinkedItems) + case .isNSync, .isSyncing: break case .needsDeletion: item.serverSyncStatus = Int64(ServerSyncStatus.isSyncing.rawValue) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift index b954c3a82..a8ac006c1 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift @@ -111,6 +111,7 @@ extension DataService { slug: try $0.slug(), isArchived: try $0.isArchived(), contentReader: try $0.contentReader().rawValue, + originalHtml: nil, labels: try $0.labels(selection: feedItemLabelSelection.list.nullable) ?? [] ), htmlContent: try $0.content(), @@ -217,7 +218,7 @@ extension DataService { linkedItem.isArchived = item.isArchived linkedItem.contentReader = item.contentReader - if linkedItem.isPDF, linkedItem.pdfData == nil { + if linkedItem.isPDF, linkedItem.localPdfURL == nil { do { try self.fetchPDFData(slug: linkedItem.unwrappedSlug, pageURLString: linkedItem.unwrappedPageURLString) } catch { @@ -257,9 +258,16 @@ extension DataService { let errorMessage = "pdfFetch failed. could not find LinkedItem from fetch request" throw BasicError.message(messageText: errorMessage) } - linkedItem.pdfData = data + + let subPath = UUID().uuidString + ".pdf" // linkedItem.title.isEmpty ? UUID().uuidString : linkedItem.title + + let path = FileManager.default + .urls(for: .cachesDirectory, in: .userDomainMask)[0] + .appendingPathComponent(subPath) do { + try data.write(to: path) + linkedItem.localPdfURL = path.absoluteString try self?.backgroundContext.save() logger.debug("PDF data saved succesfully") } catch { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LibraryItemsQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LibraryItemsQuery.swift index edc905154..b36e266bb 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LibraryItemsQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LibraryItemsQuery.swift @@ -110,6 +110,7 @@ public extension DataService { slug: try $0.slug(), isArchived: try $0.isArchived(), contentReader: try $0.contentReader().rawValue, + originalHtml: nil, labels: try $0.labels(selection: feedItemLabelSelection.list.nullable) ?? [] ) } @@ -174,6 +175,7 @@ private let libraryArticleSelection = Selection.Article { slug: try $0.slug(), isArchived: try $0.isArchived(), contentReader: try $0.contentReader().rawValue, + originalHtml: nil, labels: try $0.labels(selection: feedItemLabelSelection.list.nullable) ?? [] ) } diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift index d050c5e4f..5f3ba86fe 100644 --- a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift +++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift @@ -21,6 +21,7 @@ struct InternalLinkedItem { let slug: String let isArchived: Bool let contentReader: String? + let originalHtml: String? var labels: [InternalLinkedItemLabel] var isPDF: Bool { @@ -51,6 +52,7 @@ struct InternalLinkedItem { linkedItem.slug = slug linkedItem.isArchived = isArchived linkedItem.contentReader = contentReader + linkedItem.originalHtml = originalHtml for label in labels { linkedItem.addToLabels(label.asManagedObject(inContext: context)) @@ -66,6 +68,7 @@ extension Sequence where Element == InternalLinkedItem { context.performAndWait { linkedItems = map { $0.asManagedObject(inContext: context) } + print("LINKED ITEMS", linkedItems) do { try context.save() print("LinkedItems saved succesfully") @@ -107,6 +110,7 @@ extension JSONArticle { slug: slug, isArchived: isArchived, contentReader: contentReader, + originalHtml: nil, labels: [] ) diff --git a/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift b/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift index 8cfa95fbb..9ab0d82fa 100644 --- a/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift +++ b/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift @@ -4,17 +4,23 @@ import Utils public enum ShareExtensionStatus { case processing - case success + case saved + case synced case failed(error: SaveArticleError) + case syncFailed(error: SaveArticleError) var displayMessage: String { switch self { - case .success: - return LocalText.saveArticleSavedState - case let .failed(error: error): - return error.displayMessage case .processing: return LocalText.saveArticleProcessingState + case .saved: + return LocalText.saveArticleSavedState + case .synced: + return "Synced" + case let .failed(error: error): + return "Save failed \(error.displayMessage)" + case let .syncFailed(error: error): + return "Sync failed \(error.displayMessage)" } } } @@ -137,7 +143,7 @@ public struct ShareExtensionChildView: View { Spacer() - if case ShareExtensionStatus.success = status { + if case ShareExtensionStatus.saved = status { HStack(spacing: 4) { Text("Saved to Omnivore") .font(.appTitleThree) @@ -203,7 +209,7 @@ public struct ShareExtensionChildView: View { .padding(.horizontal) HStack { - if case ShareExtensionStatus.success = status, FeatureFlag.enableReadNow { + if case ShareExtensionStatus.saved = status, FeatureFlag.enableReadNow { Button( action: { readNowButtonAction() }, label: { Text("Read Now").frame(maxWidth: .infinity) } diff --git a/packages/api/src/routers/page_router.ts b/packages/api/src/routers/page_router.ts new file mode 100644 index 000000000..d1522a3dc --- /dev/null +++ b/packages/api/src/routers/page_router.ts @@ -0,0 +1,118 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import express from 'express' +import { ArticleSavingRequestStatus, CreateArticleErrorCode, PageType, UploadFileStatus } from '../generated/graphql' +import { isSiteBlockedForParse } from '../utils/blocked' +import cors from 'cors' +import { env } from '../env' +import { buildLogger } from '../utils/logger' +import * as jwt from 'jsonwebtoken' +import { corsConfig } from '../utils/corsConfig' +import { createPageSaveRequest } from '../services/create_page_save_request' +import { initModels } from '../server' +import { kx } from '../datalayer/knex_config' +import { fileNameForFilePath, generateSlug, isString, titleForFilePath, validateUuid } from '../utils/helpers' +import { generateUploadFilePathName, generateUploadSignedUrl } from '../utils/uploads' +import { Claims } from '../resolvers/types' +import { createPage, getPageByParam, updatePage } from '../elastic/pages' +import { createPubSubClient } from '../datalayer/pubsub' + +const logger = buildLogger('app.dispatch') + +export function pageRouter() { + const router = express.Router() + + // Create a page from an uploaded PDF document + router.options('/pdf', cors({ ...corsConfig, maxAge: 600 })) + router.put('/pdf', cors(corsConfig), async (req, res) => { + const token = req?.cookies?.auth || req?.headers?.authorization + if (!token || !jwt.verify(token, env.server.jwtSecret)) { + return res.status(401).send({ errorCode: 'UNAUTHORIZED' }) + } + const claims = jwt.decode(token) as Claims + + // Get the content type from the query params + const { url, clientRequestId } = req.query + const contentType = req.headers['content-type'] + console.log('contentType', contentType, 'url', url, 'clientRequestId', clientRequestId) + + if (!isString(url) || !isString(contentType) || !isString(clientRequestId)) { + console.log('creating page from pdf failed', url, contentType, clientRequestId) + return res.status(400).send({ errorCode: 'BAD_DATA' }) + } + + if (!validateUuid(clientRequestId)) { + console.log('creating page from pdf failed invalid uuid') + return res.status(400).send({ errorCode: 'BAD_DATA' }) + } + + const models = initModels(kx, false) + const ctx = { + uid: claims.uid, + pubsub: createPubSubClient(), + } + + const title = titleForFilePath(url) + const fileName = fileNameForFilePath(url) + const uploadFileData = await models.uploadFile.create({ + url: url, + userId: claims.uid, + fileName: fileName, + status: UploadFileStatus.Initialized, + contentType: "application/pdf", + }) + + const uploadFilePathName = generateUploadFilePathName( + uploadFileData.id, + fileName, + ) + + const signedUrl = await generateUploadSignedUrl(uploadFilePathName, "application/pdf") + + + const page = await getPageByParam({ + userId: claims.uid, + url: url, + }) + + if (page) { + console.log('updating page') + await updatePage( + page.id, + { + savedAt: new Date(), + archivedAt: null, + }, + ctx + ) + } else { + console.log('creating page') + const pageId = await createPage({ + url: signedUrl, + id: clientRequestId, + userId: claims.uid, + title: title, + hash: uploadFilePathName, + content: '', + pageType: PageType.File, + uploadFileId: uploadFileData.id, + slug: generateSlug(uploadFilePathName), + createdAt: new Date(), + savedAt: new Date(), + readingProgressPercent: 0, + readingProgressAnchorIndex: 0, + state: ArticleSavingRequestStatus.Processing, + }, ctx) + if (!pageId) { + return res.sendStatus(500) + } + } + + console.log('redirecting to signed URL', signedUrl) + return res.redirect(signedUrl) + }) + + return router +} diff --git a/packages/api/src/server.ts b/packages/api/src/server.ts index d65bbcdd8..64f0326c0 100755 --- a/packages/api/src/server.ts +++ b/packages/api/src/server.ts @@ -15,6 +15,7 @@ import { config, loggers } from 'winston' import { sentryConfig } from './sentry' import { makeApolloServer } from './apollo' import { authRouter } from './routers/auth/auth_router' +import { pageRouter } from './routers/page_router' import { articleRouter } from './routers/article_router' import { mobileAuthRouter } from './routers/auth/mobile/mobile_auth_router' import { contentServiceRouter } from './routers/svc/content' @@ -105,6 +106,7 @@ export const createApp = (): { app.get('/_ah/health', (req, res) => res.sendStatus(200)) app.use('/api/auth', authRouter()) + app.use('/api/page', pageRouter()) app.use('/api/article', articleRouter()) app.use('/api/mobile-auth', mobileAuthRouter()) app.use('/svc/pubsub/content', contentServiceRouter()) diff --git a/packages/api/src/utils/helpers.ts b/packages/api/src/utils/helpers.ts index 0a5305e13..baea054b2 100644 --- a/packages/api/src/utils/helpers.ts +++ b/packages/api/src/utils/helpers.ts @@ -233,6 +233,18 @@ export const validatedDate = ( } } +export const fileNameForFilePath = (urlStr: string): string => { + const url = normalizeUrl(new URL(urlStr).href, { + stripHash: true, + stripWWW: false, + }) + const fileName = decodeURI(path.basename(new URL(url).pathname)).replace( + /[^a-zA-Z0-9-_.]/g, + '' + ) + return fileName +} + export const titleForFilePath = (url: string): string => { try { const title = decodeURI(path.basename(new URL(url).pathname, '.pdf')) @@ -242,3 +254,12 @@ export const titleForFilePath = (url: string): string => { } return url } + +export const validateUuid = (str: string): boolean => { + const regexExp = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/gi; + return regexExp.test(str) +} + +export const isString = (check: any): check is string => { + return (typeof check === 'string' || check instanceof String) +} \ No newline at end of file diff --git a/packages/api/src/utils/uploads.ts b/packages/api/src/utils/uploads.ts index 76422657e..630349938 100644 --- a/packages/api/src/utils/uploads.ts +++ b/packages/api/src/utils/uploads.ts @@ -20,9 +20,11 @@ export const generateUploadSignedUrl = async ( contentType: string, selectedBucket?: string ): Promise => { - if (env.dev.isLocal) { - return 'http://localhost:3000/uploads/' + filePathName - } + // if (env.dev.isLocal) { + // return 'http://localhost:3000/uploads/' + filePathName + // } + + console.log("signed URL", filePathName, contentType, selectedBucket || bucketName) // These options will allow temporary uploading of file with requested content type const options: GetSignedUrlConfig = { @@ -59,9 +61,9 @@ export const makeStorageFilePublic = async ( id: string, fileName: string ): Promise => { - if (env.dev.isLocal) { - return 'http://localhost:3000/public/' + id + '/' + fileName - } + // if (env.dev.isLocal) { + // return 'http://localhost:3000/public/' + id + '/' + fileName + // } // Makes the file public const filePathName = generateUploadFilePathName(id, fileName) @@ -75,12 +77,12 @@ export const getStorageFileDetails = async ( id: string, fileName: string ): Promise<{ md5Hash: string; fileUrl: string }> => { - if (env.dev.isLocal) { - return { - md5Hash: 'some_md5_hash', - fileUrl: 'http://localhost:3000/public/' + id + '/' + fileName, - } - } + // if (env.dev.isLocal) { + // return { + // md5Hash: 'some_md5_hash', + // fileUrl: 'http://localhost:3000/public/' + id + '/' + fileName, + // } + // } const filePathName = generateUploadFilePathName(id, fileName) const file = storage.bucket(bucketName).file(filePathName) @@ -103,9 +105,9 @@ export const uploadToSignedUrl = async ( data: Buffer, contentType: string ): Promise => { - if (env.dev.isLocal) { - return - } + // if (env.dev.isLocal) { + // return + // } await axios.put(uploadUrl, data, { headers: { From 6f35740272cb36f9ac7712c595b2f0a067abc18f Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 30 May 2022 22:38:24 -0700 Subject: [PATCH 05/32] Linting fixes --- .../InternalModels/InternalLinkedItem.swift | 1 - packages/api/src/routers/page_router.ts | 87 +++++++++++++------ packages/api/src/utils/helpers.ts | 7 +- packages/api/src/utils/uploads.ts | 7 +- 4 files changed, 71 insertions(+), 31 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift index 5f3ba86fe..8927f7b63 100644 --- a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift +++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift @@ -68,7 +68,6 @@ extension Sequence where Element == InternalLinkedItem { context.performAndWait { linkedItems = map { $0.asManagedObject(inContext: context) } - print("LINKED ITEMS", linkedItems) do { try context.save() print("LinkedItems saved succesfully") diff --git a/packages/api/src/routers/page_router.ts b/packages/api/src/routers/page_router.ts index d1522a3dc..39ae6295a 100644 --- a/packages/api/src/routers/page_router.ts +++ b/packages/api/src/routers/page_router.ts @@ -3,7 +3,12 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import express from 'express' -import { ArticleSavingRequestStatus, CreateArticleErrorCode, PageType, UploadFileStatus } from '../generated/graphql' +import { + ArticleSavingRequestStatus, + CreateArticleErrorCode, + PageType, + UploadFileStatus, +} from '../generated/graphql' import { isSiteBlockedForParse } from '../utils/blocked' import cors from 'cors' import { env } from '../env' @@ -13,8 +18,17 @@ import { corsConfig } from '../utils/corsConfig' import { createPageSaveRequest } from '../services/create_page_save_request' import { initModels } from '../server' import { kx } from '../datalayer/knex_config' -import { fileNameForFilePath, generateSlug, isString, titleForFilePath, validateUuid } from '../utils/helpers' -import { generateUploadFilePathName, generateUploadSignedUrl } from '../utils/uploads' +import { + fileNameForFilePath, + generateSlug, + isString, + titleForFilePath, + validateUuid, +} from '../utils/helpers' +import { + generateUploadFilePathName, + generateUploadSignedUrl, +} from '../utils/uploads' import { Claims } from '../resolvers/types' import { createPage, getPageByParam, updatePage } from '../elastic/pages' import { createPubSubClient } from '../datalayer/pubsub' @@ -36,10 +50,26 @@ export function pageRouter() { // Get the content type from the query params const { url, clientRequestId } = req.query const contentType = req.headers['content-type'] - console.log('contentType', contentType, 'url', url, 'clientRequestId', clientRequestId) + console.log( + 'contentType', + contentType, + 'url', + url, + 'clientRequestId', + clientRequestId + ) - if (!isString(url) || !isString(contentType) || !isString(clientRequestId)) { - console.log('creating page from pdf failed', url, contentType, clientRequestId) + if ( + !isString(url) || + !isString(contentType) || + !isString(clientRequestId) + ) { + console.log( + 'creating page from pdf failed', + url, + contentType, + clientRequestId + ) return res.status(400).send({ errorCode: 'BAD_DATA' }) } @@ -61,16 +91,18 @@ export function pageRouter() { userId: claims.uid, fileName: fileName, status: UploadFileStatus.Initialized, - contentType: "application/pdf", + contentType: 'application/pdf', }) const uploadFilePathName = generateUploadFilePathName( uploadFileData.id, - fileName, + fileName ) - const signedUrl = await generateUploadSignedUrl(uploadFilePathName, "application/pdf") - + const signedUrl = await generateUploadSignedUrl( + uploadFilePathName, + 'application/pdf' + ) const page = await getPageByParam({ userId: claims.uid, @@ -89,22 +121,25 @@ export function pageRouter() { ) } else { console.log('creating page') - const pageId = await createPage({ - url: signedUrl, - id: clientRequestId, - userId: claims.uid, - title: title, - hash: uploadFilePathName, - content: '', - pageType: PageType.File, - uploadFileId: uploadFileData.id, - slug: generateSlug(uploadFilePathName), - createdAt: new Date(), - savedAt: new Date(), - readingProgressPercent: 0, - readingProgressAnchorIndex: 0, - state: ArticleSavingRequestStatus.Processing, - }, ctx) + const pageId = await createPage( + { + url: signedUrl, + id: clientRequestId, + userId: claims.uid, + title: title, + hash: uploadFilePathName, + content: '', + pageType: PageType.File, + uploadFileId: uploadFileData.id, + slug: generateSlug(uploadFilePathName), + createdAt: new Date(), + savedAt: new Date(), + readingProgressPercent: 0, + readingProgressAnchorIndex: 0, + state: ArticleSavingRequestStatus.Processing, + }, + ctx + ) if (!pageId) { return res.sendStatus(500) } diff --git a/packages/api/src/utils/helpers.ts b/packages/api/src/utils/helpers.ts index baea054b2..c4db77d44 100644 --- a/packages/api/src/utils/helpers.ts +++ b/packages/api/src/utils/helpers.ts @@ -256,10 +256,11 @@ export const titleForFilePath = (url: string): string => { } export const validateUuid = (str: string): boolean => { - const regexExp = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/gi; + const regexExp = + /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/gi return regexExp.test(str) } export const isString = (check: any): check is string => { - return (typeof check === 'string' || check instanceof String) -} \ No newline at end of file + return typeof check === 'string' || check instanceof String +} diff --git a/packages/api/src/utils/uploads.ts b/packages/api/src/utils/uploads.ts index 630349938..f69189f56 100644 --- a/packages/api/src/utils/uploads.ts +++ b/packages/api/src/utils/uploads.ts @@ -24,7 +24,12 @@ export const generateUploadSignedUrl = async ( // return 'http://localhost:3000/uploads/' + filePathName // } - console.log("signed URL", filePathName, contentType, selectedBucket || bucketName) + console.log( + 'signed URL', + filePathName, + contentType, + selectedBucket || bucketName + ) // These options will allow temporary uploading of file with requested content type const options: GetSignedUrlConfig = { From 822a38a8631561b73438ea8ffee24429c07455dc Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 31 May 2022 12:34:11 -0700 Subject: [PATCH 06/32] convert save page publisher to async --- .../Share/ShareExtensionScene.swift | 86 ++++++++++++------- .../DataService/Mutations/SavePage.swift | 53 ++++++------ 2 files changed, 79 insertions(+), 60 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift index e52e92eda..74aaba17d 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift @@ -45,49 +45,71 @@ final class ShareExtensionViewModel: ObservableObject { backgroundTask = UIApplication.shared.beginBackgroundTask(withName: requestID) PageScraper.scrape(extensionContext: extensionContext) { [weak self] result in + guard let self = self else { return } switch result { case let .success(payload): - self?.persist(pageScrapePayload: payload, requestId: self?.requestID ?? "") + Task { + await self.persist(pageScrapePayload: payload, requestId: self.requestID) + } case let .failure(error): - if let backgroundTask = self?.backgroundTask { + if let backgroundTask = self.backgroundTask { UIApplication.shared.endBackgroundTask(backgroundTask) } - self?.debugText = error.message + self.debugText = error.message } } } - private func persist(pageScrapePayload: PageScrapePayload, requestId: String) { + private func persist(pageScrapePayload: PageScrapePayload, requestId: String) async { let services = Services() - Task { - do { - // Save locally, then attempt to sync to the server - let item = try await services.dataService.persistPageScrapePayload(pageScrapePayload, requestId: requestId) - // TODO: need to update this on the main thread and handle the result == false case here - if item != nil { - self.status = .saved - } else { - self.status = .failed(error: SaveArticleError.unknown(description: "Unable to save page")) - return - } + // Save locally first + let linkedItem = try? await services.dataService.persistPageScrapePayload(pageScrapePayload, requestId: requestId) - // force a server sync - if let item = item { - let syncResult = services.dataService.syncLocalCreatedLinkedItem(item: item) - print("RESULT", syncResult) - } -// self.status = .synced -// } else { -// self.status = .syncFailed(error: SaveArticleError.unknown(description: "Unable to sync page")) -// } - - } catch { - print("ERROR SAVING PAGE", error) - } + if let linkedItem = linkedItem { + // Sync with server now that we saved the item locally + services.dataService.syncLocalCreatedLinkedItem(item: linkedItem) + updateStatus(newStatus: .saved) + } else { + updateStatus(newStatus: .failed(error: SaveArticleError.unknown(description: "Unable to save page"))) } - // First persist to Core Data - // services.dataService.persist(jsonArticle: article) + } + + private func updateStatus(newStatus: ShareExtensionStatus) { + DispatchQueue.main.async { + self.status = newStatus + } + } +} + +// Task { +// do { +// // Save locally, then attempt to sync to the server +// let item = try await services.dataService.persistPageScrapePayload(pageScrapePayload, requestId: requestId) +// // TODO: need to update this on the main thread and handle the result == false case here +// if item != nil { +// self.status = .saved +// } else { +// self.status = .failed(error: SaveArticleError.unknown(description: "Unable to save page")) +// return +// } +// +// // force a server sync +// if let item = item { +// let syncResult = services.dataService.syncLocalCreatedLinkedItem(item: item) +// print("RESULT", syncResult) +// } +//// self.status = .synced +//// } else { +//// self.status = .syncFailed(error: SaveArticleError.unknown(description: "Unable to sync page")) +//// } +// +// } catch { +// print("ERROR SAVING PAGE", error) +// } +// } +// // First persist to Core Data +// // services.dataService.persist(jsonArticle: article) // // guard services.authenticator.hasValidAuthToken else { @@ -137,8 +159,8 @@ final class ShareExtensionViewModel: ObservableObject { // } // } // } - } -} +// } +// } struct ShareExtensionView: View { let extensionContext: NSExtensionContext? diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePage.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePage.swift index 9f34c2268..a6241f260 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePage.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePage.swift @@ -1,11 +1,9 @@ -import Combine import Foundation import Models import SwiftGraphQL public extension DataService { - // swiftlint:disable:next line_length - func savePagePublisher(pageScrapePayload: PageScrapePayload, html: String, title: String?, requestId: String) -> AnyPublisher { + func savePage(pageScrapePayload: PageScrapePayload, html: String, title: String?, requestId: String) async throws { enum MutationResult { case saved(requestId: String, url: String) case error(errorCode: Enums.SaveErrorCode) @@ -35,38 +33,37 @@ public extension DataService { let path = appEnvironment.graphqlPath let headers = networker.defaultHeaders - return Deferred { - Future { promise in - send(mutation, to: path, headers: headers) { result in - switch result { - case let .success(payload): - if let graphqlError = payload.errors { - promise(.failure(.unknown(description: graphqlError.first.debugDescription))) - } - - switch payload.data { - case .saved: - promise(.success(())) - case let .error(errorCode: errorCode): - switch errorCode { - case .unauthorized: - promise(.failure(.unauthorized)) - default: - promise(.failure(.unknown(description: errorCode.rawValue))) - } - } - case let .failure(error): - promise(.failure(SaveError.make(from: error))) + return try await withCheckedThrowingContinuation { continuation in + send(mutation, to: path, headers: headers) { result in + switch result { + case let .success(payload): + if let graphqlError = payload.errors { + continuation.resume( + throwing: SaveArticleError.unknown(description: graphqlError.first.debugDescription) + ) + return } + + switch payload.data { + case .saved: + continuation.resume() + case let .error(errorCode: errorCode): + switch errorCode { + case .unauthorized: + continuation.resume(throwing: SaveArticleError.unauthorized) + default: + continuation.resume(throwing: SaveArticleError.unknown(description: errorCode.rawValue)) + } + } + case let .failure(error): + continuation.resume(throwing: SaveArticleError.make(from: error)) } } } - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() } } -private extension SaveError { +private extension SaveArticleError { static func make(from httpError: HttpError) -> SaveArticleError { switch httpError { case .network, .timeout: From def0fc76781e3fd5044c3b5c4918f6f62d468074 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 31 May 2022 12:46:16 -0700 Subject: [PATCH 07/32] convert saveURL to async --- .../DataService/Mutations/SaveArticle.swift | 11 ---- .../DataService/Mutations/SavePage.swift | 2 +- .../DataService/Mutations/SaveUrl.swift | 62 +++++++------------ 3 files changed, 25 insertions(+), 50 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SaveArticle.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SaveArticle.swift index cc7352b41..bd1b0fdfa 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SaveArticle.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SaveArticle.swift @@ -219,14 +219,3 @@ public extension DataService { .eraseToAnyPublisher() } } - -private extension SaveArticleError { - static func make(from httpError: HttpError) -> SaveArticleError { - switch httpError { - case .network, .timeout: - return .network - case .badpayload, .badURL, .badstatus, .cancelled: - return .unknown(description: httpError.localizedDescription) - } - } -} diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePage.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePage.swift index a6241f260..ee2c54733 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePage.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePage.swift @@ -63,7 +63,7 @@ public extension DataService { } } -private extension SaveArticleError { +extension SaveArticleError { static func make(from httpError: HttpError) -> SaveArticleError { switch httpError { case .network, .timeout: diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SaveUrl.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SaveUrl.swift index 3a2ba77e7..6f29d8e04 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SaveUrl.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SaveUrl.swift @@ -1,11 +1,9 @@ -import Combine import Foundation import Models import SwiftGraphQL public extension DataService { - // swiftlint:disable:next line_length - func saveUrlPublisher(pageScrapePayload: PageScrapePayload, requestId: String) -> AnyPublisher { + func saveURL(pageScrapePayload: PageScrapePayload, requestId: String) async throws { enum MutationResult { case saved(requestId: String, url: String) case error(errorCode: Enums.SaveErrorCode) @@ -31,44 +29,32 @@ public extension DataService { let path = appEnvironment.graphqlPath let headers = networker.defaultHeaders - return Deferred { - Future { promise in - send(mutation, to: path, headers: headers) { result in - switch result { - case let .success(payload): - if let graphqlError = payload.errors { - promise(.failure(.unknown(description: graphqlError.first.debugDescription))) - } - - switch payload.data { - case .saved: - promise(.success(())) - case let .error(errorCode: errorCode): - switch errorCode { - case .unauthorized: - promise(.failure(.unauthorized)) - default: - promise(.failure(.unknown(description: errorCode.rawValue))) - } - } - case let .failure(error): - promise(.failure(SaveError.make(from: error))) + return try await withCheckedThrowingContinuation { continuation in + send(mutation, to: path, headers: headers) { result in + switch result { + case let .success(payload): + if let graphqlError = payload.errors { + continuation.resume( + throwing: SaveArticleError.unknown(description: graphqlError.first.debugDescription) + ) + return } + + switch payload.data { + case .saved: + continuation.resume() + case let .error(errorCode: errorCode): + switch errorCode { + case .unauthorized: + continuation.resume(throwing: SaveArticleError.unauthorized) + default: + continuation.resume(throwing: SaveArticleError.unknown(description: errorCode.rawValue)) + } + } + case let .failure(error): + continuation.resume(throwing: SaveArticleError.make(from: error)) } } } - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() - } -} - -private extension SaveError { - static func make(from httpError: HttpError) -> SaveArticleError { - switch httpError { - case .network, .timeout: - return .network - case .badpayload, .badURL, .badstatus, .cancelled: - return .unknown(description: httpError.localizedDescription) - } } } From 0e10d5630dd6f48db4b81bdf3d6d0347e223dec5 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 31 May 2022 15:11:10 -0700 Subject: [PATCH 08/32] Rebase and merge --- .../Services/DataService/DataService.swift | 4 +-- .../DataService/Mutations/SavePage.swift | 13 +++++----- .../DataService/Mutations/SaveUrl.swift | 8 +++--- .../DataService/Networking/Networker.swift | 1 - .../Services/DataService/OfflineSync.swift | 26 +++++++++++++++---- .../api/src/resolvers/upload_files/index.ts | 2 +- 6 files changed, 35 insertions(+), 19 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift index 4514916d9..505fbd1a6 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift @@ -153,8 +153,8 @@ public final class DataService: ObservableObject { linkedItem.originalHtml = html linkedItem.title = title ?? self.titleFromPdfFile(pageScrape.url) case .none: - print("SAVING NONE TYPE") - throw BasicError.message(messageText: "Attempting to save none type") + print("SAVING URL", linkedItem.unwrappedPageURLString) + linkedItem.contentReader = "WEB" } do { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePage.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePage.swift index ee2c54733..7229db5c3 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePage.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePage.swift @@ -3,23 +3,23 @@ import Models import SwiftGraphQL public extension DataService { - func savePage(pageScrapePayload: PageScrapePayload, html: String, title: String?, requestId: String) async throws { + func savePage(item: LinkedItem) async throws { enum MutationResult { case saved(requestId: String, url: String) case error(errorCode: Enums.SaveErrorCode) } let input = InputObjects.SavePageInput( - url: pageScrapePayload.url, + url: item.unwrappedPageURLString, source: "ios-page", - clientRequestId: requestId, - title: OptionalArgument(title), - originalContent: html + clientRequestId: item.unwrappedID, + title: OptionalArgument(item.title), + originalContent: item.originalHtml! ) let selection = Selection { try $0.on( - saveSuccess: .init { .saved(requestId: requestId, url: (try? $0.url()) ?? "") }, + saveSuccess: .init { .saved(requestId: item.unwrappedID, url: (try? $0.url()) ?? "") }, saveError: .init { .error(errorCode: (try? $0.errorCodes().first) ?? .unknown) } @@ -43,7 +43,6 @@ public extension DataService { ) return } - switch payload.data { case .saved: continuation.resume() diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SaveUrl.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SaveUrl.swift index 6f29d8e04..2cacf3d5c 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SaveUrl.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SaveUrl.swift @@ -3,18 +3,20 @@ import Models import SwiftGraphQL public extension DataService { - func saveURL(pageScrapePayload: PageScrapePayload, requestId: String) async throws { + func saveURL(item: LinkedItem) async throws { enum MutationResult { case saved(requestId: String, url: String) case error(errorCode: Enums.SaveErrorCode) } let input = InputObjects.SaveUrlInput( - url: pageScrapePayload.url, + url: item.unwrappedPageURLString, source: "ios-url", - clientRequestId: requestId + clientRequestId: item.unwrappedID ) + print("UPLOADING ITEM:", item.unwrappedID, item) + let requestId = item.unwrappedID let selection = Selection { try $0.on( saveSuccess: .init { .saved(requestId: requestId, url: (try? $0.url()) ?? "") }, diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Networking/Networker.swift b/apple/OmnivoreKit/Sources/Services/DataService/Networking/Networker.swift index a94e514e6..d9a5d9c20 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Networking/Networker.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Networking/Networker.swift @@ -29,7 +29,6 @@ public final class Networker: NSObject, URLSessionTaskDelegate { public func urlSession(_: URLSession, task: URLSessionTask, didCompleteWithError: Error?) { if let httpResponse = task.response as? HTTPURLResponse { print("httpRespinse status code", httpResponse.statusCode) - print("current Request", task.currentRequest?.url) } print("finished upload of file:", task.taskIdentifier, task.currentRequest, task.response, "with error", didCompleteWithError) } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift b/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift index d55eb6c9f..798369406 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift @@ -40,13 +40,29 @@ extension DataService { case "PDF": // SaveFile uploadFilePublisher(item: item) - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() + .sink(receiveCompletion: { [weak self] _ in + print("received PDF saved completion") + }, receiveValue: { [weak self] _ in + print("recived save PDF value", item) + }) + .store(in: &subscriptions) case "WEB": if item.originalHtml != nil { - // SavePage + savePagePublisher(item: item) + .sink(receiveCompletion: { [weak self] _ in + print("received page saved completion") + }, receiveValue: { [weak self] _ in + print("recived save page value", item) + }) + .store(in: &subscriptions) } else { - // SaveURL + saveUrlPublisher(item: item) + .sink(receiveCompletion: { [weak self] _ in + print("received url saved completion") + }, receiveValue: { [weak self] _ in +// print("recived save url value", item) + }) + .store(in: &subscriptions) } case .none: print("NONE HANDLER") @@ -61,7 +77,7 @@ extension DataService { switch syncStatus { case .needsCreation: - print("SYNCING LINKED ITEM", unsyncedLinkedItems) + syncLocalCreatedLinkedItem(item: item) case .isNSync, .isSyncing: break case .needsDeletion: diff --git a/packages/api/src/resolvers/upload_files/index.ts b/packages/api/src/resolvers/upload_files/index.ts index 0fb38ea06..5e49aa0ac 100644 --- a/packages/api/src/resolvers/upload_files/index.ts +++ b/packages/api/src/resolvers/upload_files/index.ts @@ -112,7 +112,7 @@ export const uploadFileRequestResolver: ResolverFn< savedAt: new Date(), readingProgressPercent: 0, readingProgressAnchorIndex: 0, - state: ArticleSavingRequestStatus.Processing, + state: ArticleSavingRequestStatus.Succeeded, }, ctx ) From cbf02159367e0cd62fcffcb94378b5a159661704 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 31 May 2022 15:11:51 -0700 Subject: [PATCH 09/32] Call uploadFileRequest before uploading files App Engine cant handle larger request sizes, so using a redirect here doesn't work. --- .../Share/ShareExtensionScene.swift | 5 +- .../DataService/Mutations/SavePDF.swift | 101 ++++++------------ .../Services/DataService/OfflineSync.swift | 59 +++++----- .../Sources/Views/ShareExtensionView.swift | 47 +++++++- 4 files changed, 107 insertions(+), 105 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift index 74aaba17d..0d555bbe2 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift @@ -67,9 +67,10 @@ final class ShareExtensionViewModel: ObservableObject { let linkedItem = try? await services.dataService.persistPageScrapePayload(pageScrapePayload, requestId: requestId) if let linkedItem = linkedItem { - // Sync with server now that we saved the item locally - services.dataService.syncLocalCreatedLinkedItem(item: linkedItem) updateStatus(newStatus: .saved) + + await services.dataService.syncLocalCreatedLinkedItem(item: linkedItem) + updateStatus(newStatus: .synced) } else { updateStatus(newStatus: .failed(error: SaveArticleError.unknown(description: "Unable to save page"))) } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePDF.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePDF.swift index a6cbfb5d8..63577afdd 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePDF.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePDF.swift @@ -26,9 +26,7 @@ private struct UploadFileRequestPayload { } private extension DataService { - // swiftlint:disable:next line_length - // func uploadFileRequestPublisher(pageScrapePayload: PageScrapePayload, requestId: String?) -> AnyPublisher { - func uploadFileRequestPublisher(item: LinkedItem) -> AnyPublisher { + public func uploadFileRequest(item: LinkedItem) async throws -> URL { enum MutationResult { case success(payload: UploadFileRequestPayload) case error(errorCode: Enums.UploadFileRequestErrorCode?) @@ -63,84 +61,51 @@ private extension DataService { let path = appEnvironment.graphqlPath let headers = networker.defaultHeaders - return Deferred { - Future { promise in - send(mutation, to: path, headers: headers) { result in - print("result of upload file request", result) - switch result { - case let .success(payload): - if let graphqlError = payload.errors { - promise(.failure(.unknown(description: graphqlError.first.debugDescription))) - } - - switch payload.data { - case let .success(payload): - promise(.success(payload)) - case let .error(errorCode): - switch errorCode { - case .unauthorized: - promise(.failure(.unauthorized)) - default: - promise(.failure(.unknown(description: errorCode.debugDescription))) - } - } - case .failure: - promise(.failure(.badData)) + return try await withCheckedThrowingContinuation { continuation in + send(mutation, to: path, headers: headers) { result in + switch result { + case let .success(payload): + if let graphqlError = payload.errors { + continuation.resume( + throwing: SaveArticleError.unknown(description: graphqlError.first.debugDescription) + ) + return } + + switch payload.data { + case let .success(payload): + if let urlString = payload.urlString, let url = URL(string: urlString) { + continuation.resume(returning: url) + } else { + continuation.resume(throwing: SaveArticleError.unknown(description: "No upload URL")) + } + case let .error(errorCode: errorCode): + switch errorCode { + case .unauthorized: + continuation.resume(throwing: SaveArticleError.unauthorized) + default: + continuation.resume(throwing: SaveArticleError.unknown(description: errorCode?.rawValue ?? "unknown")) + } + } + case let .failure(error): + continuation.resume(throwing: SaveArticleError.make(from: error)) } } } - .eraseToAnyPublisher() } - // swiftlint:disable:next line_length - public func uploadFilePublisher(item: LinkedItem) -> AnyPublisher { - var urlComponents = URLComponents(url: appEnvironment.serverBaseURL, resolvingAgainstBaseURL: true)! - // let headers = networker.defaultHeaders - - urlComponents.path = "/api/page/pdf" - urlComponents.queryItems = [URLQueryItem(name: "url", value: item.pageURLString), URLQueryItem(name: "clientRequestId", value: item.unwrappedID)] - + public func uploadFile(item: LinkedItem, url: URL) -> URLSessionTask? { if let localPdfURL = item.localPdfURL, let localUrl = URL(string: localPdfURL) { - print("UPLOADING TO URL", urlComponents.url) - var request = URLRequest(url: urlComponents.url!) + var request = URLRequest(url: url) request.httpMethod = "PUT" - - networker.defaultHeaders.forEach { (key: String, value: String) in - request.addValue(value, forHTTPHeaderField: key) - } request.setValue("application/pdf", forHTTPHeaderField: "content-type") let task = networker.backgroundSession.uploadTask(with: request, fromFile: localUrl) task.resume() + return task + } else { + return nil } - - // Just return immediately at this point. - return Empty(completeImmediately: true).eraseToAnyPublisher() -// return "".publisher.eraseToAnyPublisher() -// return networker.urlSession.dataTaskPublisher(for: request) -// .tryMap { data, response -> String in -// let serverResponse = ServerResponse(data: data, response: response) -// if serverResponse.httpUrlResponse?.statusCode == 200, let fileUploadID = fileUploadConfig.uploadID { -// return fileUploadID -// } -// -// throw ServerError(serverResponse: serverResponse) -// } -// .mapError { error -> SaveArticleError in -// let serverResponse = ServerResponse(error: error) -// NetworkRequestLogger.log(request: request, serverResponse: serverResponse) -// let serverError = ServerError(serverResponse: serverResponse) -// switch serverError { -// case .noConnection, .timeout: -// return .network -// case .unauthenticated: -// return .unauthorized -// case .unknown: -// return .unknown(description: "upload to file server failed") -// } -// } -// .eraseToAnyPublisher() } // swiftlint:disable:next line_length diff --git a/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift b/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift index 798369406..6e6058c07 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift @@ -35,40 +35,38 @@ extension DataService { } } - public func syncLocalCreatedLinkedItem(item: LinkedItem) { + public func syncLocalCreatedLinkedItem(item: LinkedItem) async -> Bool { switch item.contentReader { case "PDF": // SaveFile - uploadFilePublisher(item: item) - .sink(receiveCompletion: { [weak self] _ in - print("received PDF saved completion") - }, receiveValue: { [weak self] _ in - print("recived save PDF value", item) - }) - .store(in: &subscriptions) - case "WEB": - if item.originalHtml != nil { - savePagePublisher(item: item) - .sink(receiveCompletion: { [weak self] _ in - print("received page saved completion") - }, receiveValue: { [weak self] _ in - print("recived save page value", item) - }) - .store(in: &subscriptions) - } else { - saveUrlPublisher(item: item) - .sink(receiveCompletion: { [weak self] _ in - print("received url saved completion") - }, receiveValue: { [weak self] _ in -// print("recived save url value", item) - }) - .store(in: &subscriptions) + do { + let uploadRequest = try await uploadFileRequest(item: item) + uploadFile(item: item, url: uploadRequest) + } catch { + logger.debug("Failed to upload PDF LinkedItem: \(error.localizedDescription)") + return false } - case .none: - print("NONE HANDLER") - case .some: - print("SOME HANDLER") + case "WEB": + do { + if item.originalHtml != nil { + try await savePage(item: item) + } else { + try await saveURL(item: item) + } + item.serverSyncStatus = Int64(ServerSyncStatus.isNSync.rawValue) + print("ITEMS SYNCED") + try backgroundContext.save() + return true + } catch { + print("Error saving", error) + backgroundContext.rollback() + logger.debug("Failed to sync LinkedItem: \(error.localizedDescription)") + return false + } + default: + return false } + return false } private func syncLinkedItems(unsyncedLinkedItems: [LinkedItem]) { @@ -77,7 +75,8 @@ extension DataService { switch syncStatus { case .needsCreation: - syncLocalCreatedLinkedItem(item: item) + // syncLocalCreatedLinkedItem(item: item) + break case .isNSync, .isSyncing: break case .needsDeletion: diff --git a/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift b/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift index 9ab0d82fa..11e80f41b 100644 --- a/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift +++ b/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift @@ -143,7 +143,20 @@ public struct ShareExtensionChildView: View { Spacer() - if case ShareExtensionStatus.saved = status { + switch status { + case .processing: + HStack { + Spacer() + Text("Saving...") + Spacer() + } + case .saved: + HStack { + Spacer() + Text("Syncing...") + Spacer() + } + case .synced: HStack(spacing: 4) { Text("Saved to Omnivore") .font(.appTitleThree) @@ -154,19 +167,43 @@ public struct ShareExtensionChildView: View { .lineLimit(nil) } .padding() - } else if case let ShareExtensionStatus.failed(error) = status { + case let .failed(error: error): HStack { Spacer() - Text(error.displayMessage) + Text("Failed to save:" + error.displayMessage) Spacer() } - } else { + case let .syncFailed(error: error): HStack { Spacer() - Text("Saving...") + Text("Failed to sync:" + error.displayMessage) Spacer() } } +// if case ShareExtensionStatus.saved = status { +// HStack(spacing: 4) { +// Text("Saved to Omnivore") +// .font(.appTitleThree) +// .foregroundColor(.appGrayText) +// .padding(.trailing, 16) +// .multilineTextAlignment(.center) +// .fixedSize(horizontal: false, vertical: true) +// .lineLimit(nil) +// } +// .padding() +// } else if case let ShareExtensionStatus.failed(error) = status { +// HStack { +// Spacer() +// Text(error.displayMessage) +// Spacer() +// } +// } else { +// HStack { +// Spacer() +// Text("Saving...") +// Spacer() +// } +// } ScrollView { if FeatureFlag.enableRemindersFromShareExtension { From f8dcab74ec3110edaee1d8fd25d877be15a3e797 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 31 May 2022 15:14:44 -0700 Subject: [PATCH 10/32] Nil out background task after we have ended it --- .../Sources/App/AppExtensions/Share/ShareExtensionScene.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift index 0d555bbe2..84b12edd0 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift @@ -54,6 +54,7 @@ final class ShareExtensionViewModel: ObservableObject { case let .failure(error): if let backgroundTask = self.backgroundTask { UIApplication.shared.endBackgroundTask(backgroundTask) + self.backgroundTask = nil } self.debugText = error.message } From 38fed646f5cca489f74de238a1f38cbb63f32159 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 31 May 2022 15:18:37 -0700 Subject: [PATCH 11/32] Remove debug code --- .../App/PDFSupport/PDFViewerViewModel.swift | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift b/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift index e8777e0b9..e2ab7554b 100644 --- a/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift @@ -26,21 +26,7 @@ public final class PDFViewerViewModel: ObservableObject { return storedURL } -// guard let data = pdfItem.documentData else { return remoteURL } -// -// let subPath = pdfItem.title.isEmpty ? UUID().uuidString : pdfItem.title -// -// let path = FileManager.default -// .urls(for: .cachesDirectory, in: .userDomainMask)[0] -// .appendingPathComponent(subPath) -// -// do { -// try data.write(to: path) -// storedURL = path -// return path -// } catch { return remoteURL - // } } public func loadHighlightPatches(completion onComplete: @escaping ([String]) -> Void) { From 632d794af58b3e3e6572d9a120c69cef9d77b7c5 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 31 May 2022 15:19:21 -0700 Subject: [PATCH 12/32] Remove some debug lines --- apple/OmnivoreKit/Sources/Services/DataService/DataService.swift | 1 - .../Services/DataService/FetchLinkedItemsBackgroundTask.swift | 1 - 2 files changed, 2 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift index 505fbd1a6..486390a11 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift @@ -37,7 +37,6 @@ public final class DataService: ObservableObject { } else { persistentContainer.loadPersistentStores { _, error in if let error = error { - print("error", error) fatalError("Core Data store failed to load with error: \(error)") } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/FetchLinkedItemsBackgroundTask.swift b/apple/OmnivoreKit/Sources/Services/DataService/FetchLinkedItemsBackgroundTask.swift index e96d03a6c..939f3330e 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/FetchLinkedItemsBackgroundTask.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/FetchLinkedItemsBackgroundTask.swift @@ -33,7 +33,6 @@ extension DataService { let maxItemCount = 30 let fetchResult = try await fetchLinkedItemIDs(limit: 10, cursor: cursor) let newItemsToFetch = await itemsNotInStore(from: fetchResult.itemIDs) - print("newItemsToFetch", newItemsToFetch) let itemsToFetch = previouslyFetchedIDs + newItemsToFetch if newItemsToFetch.isEmpty || itemsToFetch.count > maxItemCount || fetchResult.cursor == nil { From 6faa831e15399f80138ec036a3ffef8f83d386b3 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 31 May 2022 15:24:45 -0700 Subject: [PATCH 13/32] Remove debug lines --- .../DataService/Mutations/SavePDF.swift | 16 ------------- .../Services/DataService/OfflineSync.swift | 3 +++ .../Sources/Views/ShareExtensionView.swift | 24 ------------------- 3 files changed, 3 insertions(+), 40 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePDF.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePDF.swift index 63577afdd..86717ff88 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePDF.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePDF.swift @@ -3,22 +3,6 @@ import Foundation import Models import SwiftGraphQL -public extension DataService { -// func uploadPDFPublisher( -// pageScrapePayload: PageScrapePayload, -// data: Data, -// requestId: String -// ) -> AnyPublisher { -// // uploadFileRequestPublisher(pageScrapePayload: pageScrapePayload, requestId: requestId) -// // .flatMap { self.uploadFilePublisher(fileUploadConfig: $0, data: data) } -// uploadFilePublisher(pageScrapePayload: pageScrapePayload, requestId: requestId, data: data) -// .flatMap { self.saveFilePublisher(pageScrapePayload: pageScrapePayload, uploadFileId: $0, requestId: requestId) } -// .catch { _ in self.saveUrlPublisher(pageScrapePayload: pageScrapePayload, requestId: requestId) } -// .receive(on: DispatchQueue.main) -// .eraseToAnyPublisher() -// } -} - private struct UploadFileRequestPayload { let uploadID: String? let uploadFileID: String? diff --git a/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift b/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift index 6e6058c07..225a4e4a5 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift @@ -75,6 +75,9 @@ extension DataService { switch syncStatus { case .needsCreation: + // TODO: We will want to sync items that need creation in the background + // these items are forced to sync when saved, but should be re-tried in + // the background. // syncLocalCreatedLinkedItem(item: item) break case .isNSync, .isSyncing: diff --git a/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift b/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift index 11e80f41b..acc4bd060 100644 --- a/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift +++ b/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift @@ -180,30 +180,6 @@ public struct ShareExtensionChildView: View { Spacer() } } -// if case ShareExtensionStatus.saved = status { -// HStack(spacing: 4) { -// Text("Saved to Omnivore") -// .font(.appTitleThree) -// .foregroundColor(.appGrayText) -// .padding(.trailing, 16) -// .multilineTextAlignment(.center) -// .fixedSize(horizontal: false, vertical: true) -// .lineLimit(nil) -// } -// .padding() -// } else if case let ShareExtensionStatus.failed(error) = status { -// HStack { -// Spacer() -// Text(error.displayMessage) -// Spacer() -// } -// } else { -// HStack { -// Spacer() -// Text("Saving...") -// Spacer() -// } -// } ScrollView { if FeatureFlag.enableRemindersFromShareExtension { From 11f1b78c45f8080410302084e7e29983fb4d5f45 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 31 May 2022 15:26:33 -0700 Subject: [PATCH 14/32] Remove changes to upload.ts --- packages/api/src/utils/uploads.ts | 37 +++++++++++++------------------ 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/packages/api/src/utils/uploads.ts b/packages/api/src/utils/uploads.ts index f69189f56..76422657e 100644 --- a/packages/api/src/utils/uploads.ts +++ b/packages/api/src/utils/uploads.ts @@ -20,16 +20,9 @@ export const generateUploadSignedUrl = async ( contentType: string, selectedBucket?: string ): Promise => { - // if (env.dev.isLocal) { - // return 'http://localhost:3000/uploads/' + filePathName - // } - - console.log( - 'signed URL', - filePathName, - contentType, - selectedBucket || bucketName - ) + if (env.dev.isLocal) { + return 'http://localhost:3000/uploads/' + filePathName + } // These options will allow temporary uploading of file with requested content type const options: GetSignedUrlConfig = { @@ -66,9 +59,9 @@ export const makeStorageFilePublic = async ( id: string, fileName: string ): Promise => { - // if (env.dev.isLocal) { - // return 'http://localhost:3000/public/' + id + '/' + fileName - // } + if (env.dev.isLocal) { + return 'http://localhost:3000/public/' + id + '/' + fileName + } // Makes the file public const filePathName = generateUploadFilePathName(id, fileName) @@ -82,12 +75,12 @@ export const getStorageFileDetails = async ( id: string, fileName: string ): Promise<{ md5Hash: string; fileUrl: string }> => { - // if (env.dev.isLocal) { - // return { - // md5Hash: 'some_md5_hash', - // fileUrl: 'http://localhost:3000/public/' + id + '/' + fileName, - // } - // } + if (env.dev.isLocal) { + return { + md5Hash: 'some_md5_hash', + fileUrl: 'http://localhost:3000/public/' + id + '/' + fileName, + } + } const filePathName = generateUploadFilePathName(id, fileName) const file = storage.bucket(bucketName).file(filePathName) @@ -110,9 +103,9 @@ export const uploadToSignedUrl = async ( data: Buffer, contentType: string ): Promise => { - // if (env.dev.isLocal) { - // return - // } + if (env.dev.isLocal) { + return + } await axios.put(uploadUrl, data, { headers: { From 2eb0d9439995f79f396aa9f5c73d241aff624773 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 31 May 2022 16:15:19 -0700 Subject: [PATCH 15/32] make sure end background task is callled on failed saves --- .../Share/ShareExtensionScene.swift | 12 ++++++--- .../Sources/App/PDFSupport/PDFViewer.swift | 2 +- .../App/PDFSupport/PDFViewerViewModel.swift | 26 ------------------- .../Sources/Models/DataModels/FeedItem.swift | 1 - .../Sources/Models/DataModels/PDFItem.swift | 2 +- 5 files changed, 11 insertions(+), 32 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift index 74aaba17d..b4d7e2db9 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift @@ -46,20 +46,26 @@ final class ShareExtensionViewModel: ObservableObject { PageScraper.scrape(extensionContext: extensionContext) { [weak self] result in guard let self = self else { return } + switch result { case let .success(payload): Task { await self.persist(pageScrapePayload: payload, requestId: self.requestID) + self.endBackgroundTask() } case let .failure(error): - if let backgroundTask = self.backgroundTask { - UIApplication.shared.endBackgroundTask(backgroundTask) - } self.debugText = error.message + self.endBackgroundTask() } } } + private func endBackgroundTask() { + if let backgroundTask = self.backgroundTask { + UIApplication.shared.endBackgroundTask(backgroundTask) + } + } + private func persist(pageScrapePayload: PageScrapePayload, requestId: String) async { let services = Services() diff --git a/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewer.swift b/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewer.swift index ea7b59c0f..e4a80b185 100644 --- a/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewer.swift +++ b/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewer.swift @@ -29,7 +29,7 @@ import Utils @State private var shareLink: ShareLink? init(remoteURL: URL, viewModel: PDFViewerViewModel) { - self.pdfURL = viewModel.dataURL(remoteURL: remoteURL) + self.pdfURL = viewModel.pdfItem.localPdfURL ?? remoteURL self.viewModel = viewModel } diff --git a/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift b/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift index e8777e0b9..978063551 100644 --- a/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift @@ -17,32 +17,6 @@ public final class PDFViewerViewModel: ObservableObject { self.pdfItem = pdfItem } - public func dataURL(remoteURL: URL) -> URL { - // TODO: we should probably not reach this point of localPdfURL is not set - // this means the PDF has not been downloaded yet, so we likely don't have - // a valid URL to work with. - - if let storedURL = pdfItem.localPdfURL { - return storedURL - } - -// guard let data = pdfItem.documentData else { return remoteURL } -// -// let subPath = pdfItem.title.isEmpty ? UUID().uuidString : pdfItem.title -// -// let path = FileManager.default -// .urls(for: .cachesDirectory, in: .userDomainMask)[0] -// .appendingPathComponent(subPath) -// -// do { -// try data.write(to: path) -// storedURL = path -// return path -// } catch { - return remoteURL - // } - } - public func loadHighlightPatches(completion onComplete: @escaping ([String]) -> Void) { onComplete(pdfItem.highlights.map { $0.patch ?? "" }) } diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift b/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift index fb877bfb8..934e545c5 100644 --- a/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift +++ b/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift @@ -31,7 +31,6 @@ public extension LinkedItem { var unwrappedSlug: String { slug ?? "" } var unwrappedTitle: String { title ?? "" } var unwrappedPageURLString: String { pageURLString ?? "" } - var unwrappedLocalPdfURL: String { localPdfURL ?? "" } var unwrappedSavedAt: Date { savedAt ?? Date() } var unwrappedCreatedAt: Date { createdAt ?? Date() } diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/PDFItem.swift b/apple/OmnivoreKit/Sources/Models/DataModels/PDFItem.swift index 5dee64dd0..3380e48f1 100644 --- a/apple/OmnivoreKit/Sources/Models/DataModels/PDFItem.swift +++ b/apple/OmnivoreKit/Sources/Models/DataModels/PDFItem.swift @@ -22,7 +22,7 @@ public struct PDFItem { objectID: item.objectID, itemID: item.unwrappedID, pdfURL: URL(string: item.unwrappedPageURLString), - localPdfURL: URL(string: item.unwrappedLocalPdfURL), + localPdfURL: item.localPdfURL.flatMap { URL(string: $0) }, title: item.unwrappedID, slug: item.unwrappedSlug, readingProgress: item.readingProgress, From ca49ce5106b4da9b0c228f88bc9291edbdf3a304 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 31 May 2022 17:04:43 -0700 Subject: [PATCH 16/32] Simplify the OfflineSync handler for LinkedItems --- .../DataService/Mutations/SavePDF.swift | 10 ++-- .../DataService/Mutations/SavePage.swift | 12 ++--- .../DataService/Mutations/SaveUrl.swift | 10 ++-- .../Services/DataService/OfflineSync.swift | 51 ++++++++++--------- 4 files changed, 41 insertions(+), 42 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePDF.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePDF.swift index 86717ff88..437e23a3b 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePDF.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePDF.swift @@ -10,17 +10,17 @@ private struct UploadFileRequestPayload { } private extension DataService { - public func uploadFileRequest(item: LinkedItem) async throws -> URL { + public func uploadFileRequest(id: String, url: String) async throws -> URL { enum MutationResult { case success(payload: UploadFileRequestPayload) case error(errorCode: Enums.UploadFileRequestErrorCode?) } let input = InputObjects.UploadFileRequestInput( - url: item.unwrappedPageURLString, + url: url, contentType: "application/pdf", createPageEntry: OptionalArgument(true), - clientRequestId: OptionalArgument(item.unwrappedID) + clientRequestId: OptionalArgument(id) ) let selection = Selection { @@ -78,8 +78,8 @@ private extension DataService { } } - public func uploadFile(item: LinkedItem, url: URL) -> URLSessionTask? { - if let localPdfURL = item.localPdfURL, let localUrl = URL(string: localPdfURL) { + public func uploadFile(localPdfURL: String?, url: URL) -> URLSessionTask? { + if let localPdfURL = localPdfURL, let localUrl = URL(string: localPdfURL) { var request = URLRequest(url: url) request.httpMethod = "PUT" request.setValue("application/pdf", forHTTPHeaderField: "content-type") diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePage.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePage.swift index 7229db5c3..8108ae05c 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePage.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePage.swift @@ -3,23 +3,23 @@ import Models import SwiftGraphQL public extension DataService { - func savePage(item: LinkedItem) async throws { + func savePage(id: String, url: String, title: String, originalHtml: String) async throws { enum MutationResult { case saved(requestId: String, url: String) case error(errorCode: Enums.SaveErrorCode) } let input = InputObjects.SavePageInput( - url: item.unwrappedPageURLString, + url: url, source: "ios-page", - clientRequestId: item.unwrappedID, - title: OptionalArgument(item.title), - originalContent: item.originalHtml! + clientRequestId: id, + title: OptionalArgument(title), + originalContent: originalHtml ) let selection = Selection { try $0.on( - saveSuccess: .init { .saved(requestId: item.unwrappedID, url: (try? $0.url()) ?? "") }, + saveSuccess: .init { .saved(requestId: id, url: (try? $0.url()) ?? "") }, saveError: .init { .error(errorCode: (try? $0.errorCodes().first) ?? .unknown) } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SaveUrl.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SaveUrl.swift index 2cacf3d5c..887e412c5 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SaveUrl.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SaveUrl.swift @@ -3,23 +3,21 @@ import Models import SwiftGraphQL public extension DataService { - func saveURL(item: LinkedItem) async throws { + func saveURL(id: String, url: String) async throws { enum MutationResult { case saved(requestId: String, url: String) case error(errorCode: Enums.SaveErrorCode) } let input = InputObjects.SaveUrlInput( - url: item.unwrappedPageURLString, + url: url, source: "ios-url", - clientRequestId: item.unwrappedID + clientRequestId: id ) - print("UPLOADING ITEM:", item.unwrappedID, item) - let requestId = item.unwrappedID let selection = Selection { try $0.on( - saveSuccess: .init { .saved(requestId: requestId, url: (try? $0.url()) ?? "") }, + saveSuccess: .init { .saved(requestId: id, url: (try? $0.url()) ?? "") }, saveError: .init { .error(errorCode: (try? $0.errorCodes().first) ?? .unknown) } ) } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift b/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift index 225a4e4a5..115a84bc4 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift @@ -35,38 +35,40 @@ extension DataService { } } - public func syncLocalCreatedLinkedItem(item: LinkedItem) async -> Bool { + public func syncLocalCreatedLinkedItem(item: LinkedItem) { switch item.contentReader { case "PDF": - // SaveFile - do { - let uploadRequest = try await uploadFileRequest(item: item) - uploadFile(item: item, url: uploadRequest) - } catch { - logger.debug("Failed to upload PDF LinkedItem: \(error.localizedDescription)") - return false + let id = item.unwrappedID + let localPdfURL = item.localPdfURL + let url = item.unwrappedPageURLString + Task { + let uploadRequestUrl = try await uploadFileRequest(id: id, url: url) + await uploadFile(localPdfURL: localPdfURL, url: uploadRequestUrl) + try await backgroundContext.perform { + item.serverSyncStatus = Int64(ServerSyncStatus.isNSync.rawValue) + try self.backgroundContext.save() + } } case "WEB": - do { - if item.originalHtml != nil { - try await savePage(item: item) + let id = item.unwrappedID + let url = item.unwrappedPageURLString + let title = item.unwrappedTitle + let originalHtml = item.originalHtml + + Task { + if let originalHtml = originalHtml { + try await savePage(id: id, url: url, title: title, originalHtml: originalHtml) } else { - try await saveURL(item: item) + try await saveURL(id: id, url: url) + } + try await backgroundContext.perform { + item.serverSyncStatus = Int64(ServerSyncStatus.isNSync.rawValue) + try self.backgroundContext.save() } - item.serverSyncStatus = Int64(ServerSyncStatus.isNSync.rawValue) - print("ITEMS SYNCED") - try backgroundContext.save() - return true - } catch { - print("Error saving", error) - backgroundContext.rollback() - logger.debug("Failed to sync LinkedItem: \(error.localizedDescription)") - return false } default: - return false + break } - return false } private func syncLinkedItems(unsyncedLinkedItems: [LinkedItem]) { @@ -78,8 +80,7 @@ extension DataService { // TODO: We will want to sync items that need creation in the background // these items are forced to sync when saved, but should be re-tried in // the background. - // syncLocalCreatedLinkedItem(item: item) - break + syncLocalCreatedLinkedItem(item: item) case .isNSync, .isSyncing: break case .needsDeletion: From 04195665a7573394a513d3a1a05ea66c986108c8 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 1 Jun 2022 09:45:31 -0700 Subject: [PATCH 17/32] Make services a member var so they arent dealloced during async operations --- .../App/AppExtensions/Share/ShareExtensionScene.swift | 3 +-- .../Sources/Services/DataService/Networking/File.swift | 8 ++++++++ .../Sources/Services/NSNotification+BackgroundSync.swift | 8 ++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/Services/DataService/Networking/File.swift create mode 100644 apple/OmnivoreKit/Sources/Services/NSNotification+BackgroundSync.swift diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift index cf69fe450..534b9c472 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift @@ -25,6 +25,7 @@ final class ShareExtensionViewModel: ObservableObject { @Published var status: ShareExtensionStatus = .processing @Published var debugText: String? + let services = Services() var subscriptions = Set() var backgroundTask: UIBackgroundTaskIdentifier? let requestID = UUID().uuidString.lowercased() @@ -71,8 +72,6 @@ final class ShareExtensionViewModel: ObservableObject { } private func persist(pageScrapePayload: PageScrapePayload, requestId: String) async { - let services = Services() - // Save locally first let linkedItem = try? await services.dataService.persistPageScrapePayload(pageScrapePayload, requestId: requestId) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Networking/File.swift b/apple/OmnivoreKit/Sources/Services/DataService/Networking/File.swift new file mode 100644 index 000000000..fe03fbc66 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Services/DataService/Networking/File.swift @@ -0,0 +1,8 @@ +// +// File.swift +// +// +// Created by Jackson Harper on 5/31/22. +// + +import Foundation diff --git a/apple/OmnivoreKit/Sources/Services/NSNotification+BackgroundSync.swift b/apple/OmnivoreKit/Sources/Services/NSNotification+BackgroundSync.swift new file mode 100644 index 000000000..fe03fbc66 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Services/NSNotification+BackgroundSync.swift @@ -0,0 +1,8 @@ +// +// File.swift +// +// +// Created by Jackson Harper on 5/31/22. +// + +import Foundation From 3ab3fd6f775209a207e456739d7f70a40558c227 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 1 Jun 2022 09:46:12 -0700 Subject: [PATCH 18/32] Remove unused file --- .../Sources/Services/DataService/Networking/File.swift | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 apple/OmnivoreKit/Sources/Services/DataService/Networking/File.swift diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Networking/File.swift b/apple/OmnivoreKit/Sources/Services/DataService/Networking/File.swift deleted file mode 100644 index fe03fbc66..000000000 --- a/apple/OmnivoreKit/Sources/Services/DataService/Networking/File.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// File.swift -// -// -// Created by Jackson Harper on 5/31/22. -// - -import Foundation From b2cbcc23c33abc9e96439d321e6bb49a8114e338 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 1 Jun 2022 17:24:42 -0700 Subject: [PATCH 19/32] Improvements to background syncing --- .../Share/ExtensionSaveService.swift | 160 ++++++++++++++++++ .../Share/ShareExtensionScene.swift | 127 +------------- .../Services/DataService/DataService.swift | 18 +- .../DataService/Mutations/SavePDF.swift | 89 ++++++---- .../DataService/Networking/Networker.swift | 33 ++-- .../Services/DataService/OfflineSync.swift | 92 ++++++++-- .../Services/DataService/SaveService.swift | 1 + .../NSNotification+BackgroundSync.swift | 8 - 8 files changed, 321 insertions(+), 207 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift create mode 100644 apple/OmnivoreKit/Sources/Services/DataService/SaveService.swift delete mode 100644 apple/OmnivoreKit/Sources/Services/NSNotification+BackgroundSync.swift diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift new file mode 100644 index 000000000..649d9757a --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift @@ -0,0 +1,160 @@ +// +// File.swift +// +// +// Created by Jackson Harper on 6/1/22. +// + +import Foundation +import Models +import Services +import Views + +typealias UpdateStatusFunc = (ShareExtensionStatus) -> Void + +class ExtensionSaveService { + let queue: OperationQueue + + init() { + self.queue = OperationQueue() + } + + private func queueSaveOperation(_ pageScrape: PageScrapePayload, updateStatusFunc: UpdateStatusFunc?) { + ProcessInfo().performExpiringActivity(withReason: "app.omnivore.SaveActivity") { [self] expiring in + guard !expiring else { + self.queue.cancelAllOperations() + self.queue.waitUntilAllOperationsAreFinished() + return + } + + let operation = SaveOperation(pageScrapePayload: pageScrape, updateStatusFunc: updateStatusFunc) + + self.queue.addOperation(operation) + self.queue.waitUntilAllOperationsAreFinished() + } + } + + public func save(_ extensionContext: NSExtensionContext, updateStatusFunc: UpdateStatusFunc?) { + PageScraper.scrape(extensionContext: extensionContext) { [weak self] result in + guard let self = self else { return } + + switch result { + case let .success(payload): + self.queueSaveOperation(payload, updateStatusFunc: updateStatusFunc) + case let .failure(error): + print("failed", error) + } + } + } + + class SaveOperation: Operation, URLSessionDelegate { + let requestId: String + let services: Services + let pageScrapePayload: PageScrapePayload + let updateStatusFunc: UpdateStatusFunc? + + var queue: OperationQueue? + var uploadTask: URLSessionTask? + + enum State: Int { + case created + case started + case finished + } + + init(pageScrapePayload: PageScrapePayload, updateStatusFunc: UpdateStatusFunc? = nil) { + self.pageScrapePayload = pageScrapePayload + self.updateStatusFunc = updateStatusFunc + + self.state = .created + self.services = Services() + self.requestId = UUID().uuidString.lowercased() + } + + open var state: State = .created { + willSet { + willChangeValue(forKey: "isReady") + willChangeValue(forKey: "isExecuting") + willChangeValue(forKey: "isFinished") + willChangeValue(forKey: "isCancelled") + } + didSet { + didChangeValue(forKey: "isCancelled") + didChangeValue(forKey: "isFinished") + didChangeValue(forKey: "isExecuting") + didChangeValue(forKey: "isReady") + } + } + + override var isAsynchronous: Bool { + true + } + + override var isReady: Bool { + true + } + + override var isExecuting: Bool { + self.state == .started + } + + override var isFinished: Bool { + self.state == .finished + } + + override func start() { + guard !isCancelled else { return } + state = .started + queue = OperationQueue() + + Task { + await persist(services: self.services, pageScrapePayload: self.pageScrapePayload, requestId: self.requestId) + } + } + + override func cancel() { +// task?.cancel() +// finishOperation() +// +// storeUnresolvedSavedItem() + super.cancel() + } + + private func updateStatus(newStatus: ShareExtensionStatus) { + DispatchQueue.main.async { + if let updateStatusFunc = self.updateStatusFunc { + updateStatusFunc(newStatus) + } + } + } + + private func persist(services: Services, pageScrapePayload: PageScrapePayload, requestId: String) async { + do { + try await services.dataService.persistPageScrapePayload(pageScrapePayload, requestId: requestId) + } catch { + updateStatus(newStatus: .failed(error: SaveArticleError.unknown(description: "Unable to access content"))) + return + } + + do { + updateStatus(newStatus: .saved) + + switch pageScrapePayload.contentType { + case .none: + try await services.dataService.syncUrl(id: requestId, url: pageScrapePayload.url) + case let .pdf(localUrl): + try await services.dataService.syncPdf(id: requestId, localPdfURL: localUrl, url: pageScrapePayload.url) + case let .html(html, title): + try await services.dataService.syncPage(id: requestId, originalHtml: html, title: title, url: pageScrapePayload.url) + } + + } catch { + print("ERROR SYNCING", error) + updateStatus(newStatus: .syncFailed(error: SaveArticleError.unknown(description: "Unknown Error"))) + } + + state = .finished + updateStatus(newStatus: .synced) + } + } +} diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift index 534b9c472..f5b86535c 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift @@ -25,12 +25,10 @@ final class ShareExtensionViewModel: ObservableObject { @Published var status: ShareExtensionStatus = .processing @Published var debugText: String? - let services = Services() var subscriptions = Set() var backgroundTask: UIBackgroundTaskIdentifier? let requestID = UUID().uuidString.lowercased() - - init() {} + let saveService = ExtensionSaveService() func handleReadNowAction(extensionContext: NSExtensionContext?) { #if os(iOS) @@ -43,135 +41,20 @@ final class ShareExtensionViewModel: ObservableObject { } func savePage(extensionContext: NSExtensionContext?) { - backgroundTask = UIApplication.shared.beginBackgroundTask(withName: requestID) - - PageScraper.scrape(extensionContext: extensionContext) { [weak self] result in - guard let self = self else { return } - - switch result { - case let .success(payload): - Task { - await self.persist(pageScrapePayload: payload, requestId: self.requestID) - self.endBackgroundTask() - } - case let .failure(error): - if let backgroundTask = self.backgroundTask { - UIApplication.shared.endBackgroundTask(backgroundTask) - self.backgroundTask = nil - } - self.debugText = error.message - self.endBackgroundTask() - } - } - } - - private func endBackgroundTask() { - if let backgroundTask = self.backgroundTask { - UIApplication.shared.endBackgroundTask(backgroundTask) - } - } - - private func persist(pageScrapePayload: PageScrapePayload, requestId: String) async { - // Save locally first - let linkedItem = try? await services.dataService.persistPageScrapePayload(pageScrapePayload, requestId: requestId) - - if let linkedItem = linkedItem { - updateStatus(newStatus: .saved) - - await services.dataService.syncLocalCreatedLinkedItem(item: linkedItem) - updateStatus(newStatus: .synced) + if let extensionContext = extensionContext { + saveService.save(extensionContext, updateStatusFunc: updateStatus) } else { - updateStatus(newStatus: .failed(error: SaveArticleError.unknown(description: "Unable to save page"))) + updateStatus(.failed(error: .unknown(description: "Internal Error"))) } } - private func updateStatus(newStatus: ShareExtensionStatus) { + private func updateStatus(_ newStatus: ShareExtensionStatus) { DispatchQueue.main.async { self.status = newStatus } } } -// Task { -// do { -// // Save locally, then attempt to sync to the server -// let item = try await services.dataService.persistPageScrapePayload(pageScrapePayload, requestId: requestId) -// // TODO: need to update this on the main thread and handle the result == false case here -// if item != nil { -// self.status = .saved -// } else { -// self.status = .failed(error: SaveArticleError.unknown(description: "Unable to save page")) -// return -// } -// -// // force a server sync -// if let item = item { -// let syncResult = services.dataService.syncLocalCreatedLinkedItem(item: item) -// print("RESULT", syncResult) -// } -//// self.status = .synced -//// } else { -//// self.status = .syncFailed(error: SaveArticleError.unknown(description: "Unable to sync page")) -//// } -// -// } catch { -// print("ERROR SAVING PAGE", error) -// } -// } -// // First persist to Core Data -// // services.dataService.persist(jsonArticle: article) - -// -// guard services.authenticator.hasValidAuthToken else { -// status = .failed(error: .unauthorized) -// return -// } -// -// let saveLinkPublisher: AnyPublisher = { -// if case let .pdf(data) = pageScrapePayload.contentType { -// return services.dataService.uploadPDFPublisher(pageScrapePayload: pageScrapePayload, -// data: data, -// requestId: requestId) -// } else if case let .html(html, title) = pageScrapePayload.contentType { -// return services.dataService.savePagePublisher(pageScrapePayload: pageScrapePayload, -// html: html, -// title: title, -// requestId: requestId) -// } else { -// return services.dataService.saveUrlPublisher(pageScrapePayload: pageScrapePayload, requestId: requestId) -// } -// }() -// -// saveLinkPublisher -// .sink { [weak self] completion in -// guard case let .failure(error) = completion else { return } -// self?.debugText = "saveArticleError: \(error)" -// self?.status = .failed(error: error) -// if let backgroundTask = self?.backgroundTask { -// UIApplication.shared.endBackgroundTask(backgroundTask) -// } -// } receiveValue: { [weak self] _ in -// self?.status = .success -// if let backgroundTask = self?.backgroundTask { -// UIApplication.shared.endBackgroundTask(backgroundTask) -// } -// } -// .store(in: &subscriptions) -// -// // Check connection to get fast feedback for auth/network errors -// Task { -// let hasConnectionAndValidToken = await services.dataService.hasConnectionAndValidToken() -// -// if !hasConnectionAndValidToken { -// DispatchQueue.main.async { -// self.debugText = "saveArticleError: No connection or invalid token." -// self.status = .failed(error: .unknown(description: "")) -// } -// } -// } -// } -// } - struct ShareExtensionView: View { let extensionContext: NSExtensionContext? @StateObject private var viewModel = ShareExtensionViewModel() diff --git a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift index 486390a11..92eea91ad 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift @@ -15,10 +15,10 @@ public final class DataService: ObservableObject { public static var showIntercomMessenger: (() -> Void)? public let appEnvironment: AppEnvironment - let networker: Networker + public let networker: Networker var persistentContainer: PersistentContainer - var backgroundContext: NSManagedObjectContext + public var backgroundContext: NSManagedObjectContext var subscriptions = Set() public var viewContext: NSManagedObjectContext { @@ -41,6 +41,12 @@ public final class DataService: ObservableObject { } } } + + NotificationCenter.default + .addObserver(self, + selector: #selector(locallyCreatedItemSynced), + name: NSNotification.LocallyCreatedItemSynced, + object: nil) } public var currentViewer: Viewer? { @@ -101,9 +107,9 @@ public final class DataService: ObservableObject { return isFirstRun } - public func persistPageScrapePayload(_ pageScrape: PageScrapePayload, requestId: String) async throws -> LinkedItem? { + public func persistPageScrapePayload(_ pageScrape: PageScrapePayload, requestId: String) async throws { try await backgroundContext.perform { [weak self] in - guard let self = self else { return nil } + guard let self = self else { return } let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() fetchRequest.predicate = NSPredicate(format: "id == %@", requestId) @@ -135,8 +141,6 @@ public final class DataService: ObservableObject { switch pageScrape.contentType { case let .pdf(localUrl): - print("SAVING PDF", localUrl) - linkedItem.contentReader = "PDF" linkedItem.localPdfURL = localUrl.absoluteString linkedItem.title = self.titleFromPdfFile(pageScrape.url) @@ -147,7 +151,6 @@ public final class DataService: ObservableObject { // linkedItem.imageURLString = thumbnailUrl.absoluteString case let .html(html: html, title: title): - print("SAVING HTML", html, title ?? "no title") linkedItem.contentReader = "WEB" linkedItem.originalHtml = html linkedItem.title = title ?? self.titleFromPdfFile(pageScrape.url) @@ -165,7 +168,6 @@ public final class DataService: ObservableObject { print("Failed to save ArticleContent", error.localizedDescription, error) throw error } - return linkedItem } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePDF.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePDF.swift index 437e23a3b..c8051e48e 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePDF.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePDF.swift @@ -3,14 +3,14 @@ import Foundation import Models import SwiftGraphQL -private struct UploadFileRequestPayload { - let uploadID: String? - let uploadFileID: String? - let urlString: String? +public struct UploadFileRequestPayload { + public let uploadID: String? + public let uploadFileID: String? + public let urlString: String? } -private extension DataService { - public func uploadFileRequest(id: String, url: String) async throws -> URL { +public extension DataService { + func uploadFileRequest(id: String, url: String) async throws -> UploadFileRequestPayload { enum MutationResult { case success(payload: UploadFileRequestPayload) case error(errorCode: Enums.UploadFileRequestErrorCode?) @@ -59,7 +59,7 @@ private extension DataService { switch payload.data { case let .success(payload): if let urlString = payload.urlString, let url = URL(string: urlString) { - continuation.resume(returning: url) + continuation.resume(returning: payload) } else { continuation.resume(throwing: SaveArticleError.unknown(description: "No upload URL")) } @@ -78,29 +78,49 @@ private extension DataService { } } - public func uploadFile(localPdfURL: String?, url: URL) -> URLSessionTask? { + func uploadFile(id _: String, localPdfURL: URL, url: URL) async throws { + var request = URLRequest(url: url) + request.httpMethod = "PUT" + request.addValue("application/pdf", forHTTPHeaderField: "content-type") + + return try await withCheckedThrowingContinuation { continuation in + let task = networker.urlSession.uploadTask(with: request, fromFile: localPdfURL) { _, response, _ in + print("UPLOAD RESPONSE", response) + if let httpResponse = response as? HTTPURLResponse, 200 ... 299 ~= httpResponse.statusCode { + continuation.resume() + } else { + continuation.resume(throwing: SaveArticleError.unknown(description: "Invalid response")) + } + } + task.resume() + } + } + + func uploadFileInBackground(id: String, localPdfURL: String?, url: URL, usingSession session: URLSession) -> URLSessionTask? { if let localPdfURL = localPdfURL, let localUrl = URL(string: localPdfURL) { var request = URLRequest(url: url) request.httpMethod = "PUT" request.setValue("application/pdf", forHTTPHeaderField: "content-type") + request.setValue(id, forHTTPHeaderField: "clientRequestId") - let task = networker.backgroundSession.uploadTask(with: request, fromFile: localUrl) - task.resume() + let task = session.uploadTask(with: request, fromFile: localUrl) return task } else { + // TODO: How should we handle this scenario? + print("NOT UPLOADING PDF DOCUMENT YET") return nil } } // swiftlint:disable:next line_length - func saveFilePublisher(pageScrapePayload: PageScrapePayload, uploadFileId: String, requestId: String) -> AnyPublisher { + func saveFilePublisher(requestId: String, uploadFileId: String, url: String) async throws { enum MutationResult { case saved(requestId: String, url: String) case error(errorCode: Enums.SaveErrorCode) } let input = InputObjects.SaveFileInput( - url: pageScrapePayload.url, + url: url, source: "ios-file", clientRequestId: requestId, uploadFileId: uploadFileId @@ -120,34 +140,31 @@ private extension DataService { let path = appEnvironment.graphqlPath let headers = networker.defaultHeaders - return Deferred { - Future { promise in - send(mutation, to: path, headers: headers) { result in - switch result { - case let .success(payload): - if let graphqlError = payload.errors { - promise(.failure(.unknown(description: graphqlError.first.debugDescription))) - } - - switch payload.data { - case .saved: - promise(.success(())) - case let .error(errorCode: errorCode): - switch errorCode { - case .unauthorized: - promise(.failure(.unauthorized)) - default: - promise(.failure(.unknown(description: errorCode.rawValue))) - } - } - case let .failure(error): - promise(.failure(SaveError.make(from: error))) + return try await withCheckedThrowingContinuation { continuation in + send(mutation, to: path, headers: headers) { result in + switch result { + case let .success(payload): + if let graphqlError = payload.errors { + continuation.resume(throwing: SaveArticleError.unknown(description: graphqlError.first.debugDescription)) + return } + + switch payload.data { + case .saved: + continuation.resume() + case let .error(errorCode: errorCode): + switch errorCode { + case .unauthorized: + continuation.resume(throwing: SaveArticleError.unauthorized) + default: + continuation.resume(throwing: SaveArticleError.unknown(description: errorCode.rawValue)) + } + } + case let .failure(error): + continuation.resume(throwing: SaveArticleError.make(from: error)) } } } - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Networking/Networker.swift b/apple/OmnivoreKit/Sources/Services/DataService/Networking/Networker.swift index d9a5d9c20..33f0e1da1 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Networking/Networker.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Networking/Networker.swift @@ -4,6 +4,7 @@ import Models public final class Networker: NSObject, URLSessionTaskDelegate { let urlSession: URLSession let appEnvironment: AppEnvironment + var uploadQueue: [String: URLSessionUploadTask] = [:] var defaultHeaders: [String: String] { var headers = URLRequest.defaultHeaders @@ -20,26 +21,26 @@ public final class Networker: NSObject, URLSessionTaskDelegate { self.urlSession = .shared } - lazy var backgroundSession: URLSession = { - let sessionConfig = URLSessionConfiguration.background(withIdentifier: "app.omnivoreapp.BackgroundSessionConfig") + public func createBackgroundSession() -> URLSession { + let sessionConfig = URLSessionConfiguration.background(withIdentifier: "app.omnivoreapp.BackgroundSessionConfig-") sessionConfig.sharedContainerIdentifier = "group.app.omnivoreapp" return URLSession(configuration: sessionConfig, delegate: self, delegateQueue: nil) - }() - - public func urlSession(_: URLSession, task: URLSessionTask, didCompleteWithError: Error?) { - if let httpResponse = task.response as? HTTPURLResponse { - print("httpRespinse status code", httpResponse.statusCode) - } - print("finished upload of file:", task.taskIdentifier, task.currentRequest, task.response, "with error", didCompleteWithError) } - public func urlSession(_: URLSession, - task: URLSessionTask, - didSendBodyData _: Int64, - totalBytesSent: Int64, - totalBytesExpectedToSend _: Int64) - { - print("sent background data:", task.taskIdentifier, totalBytesSent) + public func urlSession(_: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + print("finished upload on original request", task.originalRequest, "error", error) + if let httpResponse = task.response as? HTTPURLResponse { + if 200 ... 299 ~= httpResponse.statusCode { + // success + if let requestId = task.originalRequest?.value(forHTTPHeaderField: "clientRequestId") { + print("COMPLETED UPLOADED REQUEST ID", requestId) + DispatchQueue.main.async { + NotificationCenter.default.post(name: NSNotification.LocallyCreatedItemSynced, object: nil, userInfo: ["objectID": requestId]) + } + } + } + print("DONE") + } } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift b/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift index 115a84bc4..b53feb3bf 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift @@ -2,8 +2,8 @@ import CoreData import Foundation import Models -extension DataService { - func syncOfflineItemsWithServerIfNeeded() async throws { +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]() @@ -35,20 +35,61 @@ extension DataService { } } - public func syncLocalCreatedLinkedItem(item: LinkedItem) { +// func syncPdf(item: LinkedItem, usingSession session: URLSession) async throws -> Bool { +// try backgroundContext.performAndWait { +// item.serverSyncStatus = Int64(ServerSyncStatus.isSyncing.rawValue) +// try self.backgroundContext.save() +// } +// +// let id = item.unwrappedID +// let localPdfURL = item.localPdfURL +// let url = item.unwrappedPageURLString +// let uploadRequestUrl = try await uploadFileRequest(id: id, url: url) +// return await try uploadFile(id: id, localPdfURL: localPdfURL, url: uploadRequestUrl, usingSession: session) +// } + + func syncPdf(id: String, localPdfURL: URL, url: String) async throws { +// try backgroundContext.performAndWait { +// item.serverSyncStatus = Int64(ServerSyncStatus.isSyncing.rawValue) +// try self.backgroundContext.save() +// } + + let uploadRequest = try await uploadFileRequest(id: id, url: url) + if let urlString = uploadRequest.urlString, let uploadUrl = URL(string: urlString) { + try await uploadFile(id: id, localPdfURL: localPdfURL, url: uploadUrl) + // try await services.dataService.saveFilePublisher(requestId: requestId, uploadFileId: uploadFileID, url: url) + } else { + throw SaveArticleError.badData + } + } + + func syncPage(id: String, originalHtml: String, title: String?, url: String) async throws { + // try backgroundContext.performAndWait { + // item.serverSyncStatus = Int64(ServerSyncStatus.isSyncing.rawValue) + // try self.backgroundContext.save() + // } + try await savePage(id: id, url: url, title: title ?? url, originalHtml: originalHtml) + } + + func syncUrl(id: String, url: String) async throws { + try await saveURL(id: id, url: url) + } + + func syncLocalCreatedLinkedItem(item: LinkedItem) { switch item.contentReader { case "PDF": - let id = item.unwrappedID - let localPdfURL = item.localPdfURL - let url = item.unwrappedPageURLString - Task { - let uploadRequestUrl = try await uploadFileRequest(id: id, url: url) - await uploadFile(localPdfURL: localPdfURL, url: uploadRequestUrl) - try await backgroundContext.perform { - item.serverSyncStatus = Int64(ServerSyncStatus.isNSync.rawValue) - try self.backgroundContext.save() - } - } +// let id = item.unwrappedID +// let localPdfURL = item.localPdfURL +// let url = item.unwrappedPageURLString +// Task { +// let uploadRequestUrl = try await uploadFileRequest(id: id, url: url) +// uploadFile(id: id, localPdfURL: localPdfURL, url: uploadRequestUrl) +// try await backgroundContext.perform { +// item.serverSyncStatus = Int64(ServerSyncStatus.isNSync.rawValue) +// try self.backgroundContext.save() +// } +// } + break case "WEB": let id = item.unwrappedID let url = item.unwrappedPageURLString @@ -77,9 +118,7 @@ extension DataService { switch syncStatus { case .needsCreation: - // TODO: We will want to sync items that need creation in the background - // these items are forced to sync when saved, but should be re-tried in - // the background. + item.serverSyncStatus = Int64(ServerSyncStatus.isSyncing.rawValue) syncLocalCreatedLinkedItem(item: item) case .isNSync, .isSyncing: break @@ -129,4 +168,23 @@ extension DataService { } } } + + @objc + func locallyCreatedItemSynced(notification: NSNotification) { + print("SYNCED LOCALLY CREATED ITEM", notification) + if let objectId = notification.userInfo?["objectID"] as? String { + do { + try backgroundContext.performAndWait { + let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "id == %@", objectId) + if let existingItem = try? self.backgroundContext.fetch(fetchRequest).first { + existingItem.serverSyncStatus = Int64(ServerSyncStatus.isNSync.rawValue) + try self.backgroundContext.save() + } + } + } catch { + print("ERROR", error) + } + } + } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/SaveService.swift b/apple/OmnivoreKit/Sources/Services/DataService/SaveService.swift new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Services/DataService/SaveService.swift @@ -0,0 +1 @@ + diff --git a/apple/OmnivoreKit/Sources/Services/NSNotification+BackgroundSync.swift b/apple/OmnivoreKit/Sources/Services/NSNotification+BackgroundSync.swift deleted file mode 100644 index fe03fbc66..000000000 --- a/apple/OmnivoreKit/Sources/Services/NSNotification+BackgroundSync.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// File.swift -// -// -// Created by Jackson Harper on 5/31/22. -// - -import Foundation From ee4dc0aaae66e618016fd91cdeb05b475803e66c Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 1 Jun 2022 20:10:16 -0700 Subject: [PATCH 20/32] Correct error message typo --- .../Sources/App/Views/WebReader/WebReaderViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift index 1a03c96dd..f54368817 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift @@ -21,7 +21,7 @@ struct SafariWebLink: Identifiable { if let fetchError = error as? ContentFetchError { switch fetchError { case .network: - errorMessage = "We were unable to retrieve your content. Please ccheck network connectivity and try again." + errorMessage = "We were unable to retrieve your content. Please check network connectivity and try again." default: errorMessage = "We were unable to parse your content." } From e773ea7e2bb712003cf411fce85eae82758f243d Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 1 Jun 2022 20:10:33 -0700 Subject: [PATCH 21/32] Remove debug --- .../Sources/Services/DataService/DataService.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift index 92eea91ad..a8d79be73 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift @@ -41,12 +41,6 @@ public final class DataService: ObservableObject { } } } - - NotificationCenter.default - .addObserver(self, - selector: #selector(locallyCreatedItemSynced), - name: NSNotification.LocallyCreatedItemSynced, - object: nil) } public var currentViewer: Viewer? { From 1386cc250a6d5051623fe87ae5a7eade425dbf9d Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 1 Jun 2022 20:22:45 -0700 Subject: [PATCH 22/32] When opening library items that were created locally, make sure they are synced --- .../DataService/Networking/Networker.swift | 22 ---- .../Services/DataService/OfflineSync.swift | 108 ++++++++++-------- .../Queries/ArticleContentQuery.swift | 46 ++++++++ 3 files changed, 108 insertions(+), 68 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Networking/Networker.swift b/apple/OmnivoreKit/Sources/Services/DataService/Networking/Networker.swift index 33f0e1da1..5da9691de 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Networking/Networker.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Networking/Networker.swift @@ -20,28 +20,6 @@ public final class Networker: NSObject, URLSessionTaskDelegate { self.appEnvironment = appEnvironment self.urlSession = .shared } - - public func createBackgroundSession() -> URLSession { - let sessionConfig = URLSessionConfiguration.background(withIdentifier: "app.omnivoreapp.BackgroundSessionConfig-") - sessionConfig.sharedContainerIdentifier = "group.app.omnivoreapp" - return URLSession(configuration: sessionConfig, delegate: self, delegateQueue: nil) - } - - public func urlSession(_: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { - print("finished upload on original request", task.originalRequest, "error", error) - if let httpResponse = task.response as? HTTPURLResponse { - if 200 ... 299 ~= httpResponse.statusCode { - // success - if let requestId = task.originalRequest?.value(forHTTPHeaderField: "clientRequestId") { - print("COMPLETED UPLOADED REQUEST ID", requestId) - DispatchQueue.main.async { - NotificationCenter.default.post(name: NSNotification.LocallyCreatedItemSynced, object: nil, userInfo: ["objectID": requestId]) - } - } - } - print("DONE") - } - } } extension Networker { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift b/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift index b53feb3bf..5f029fb1e 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift @@ -35,61 +35,81 @@ public extension DataService { } } -// func syncPdf(item: LinkedItem, usingSession session: URLSession) async throws -> Bool { -// try backgroundContext.performAndWait { -// item.serverSyncStatus = Int64(ServerSyncStatus.isSyncing.rawValue) -// try self.backgroundContext.save() -// } -// -// let id = item.unwrappedID -// let localPdfURL = item.localPdfURL -// let url = item.unwrappedPageURLString -// let uploadRequestUrl = try await uploadFileRequest(id: id, url: url) -// return await try uploadFile(id: id, localPdfURL: localPdfURL, url: uploadRequestUrl, usingSession: session) -// } + private func updateLinkedItemStatus(id: String, status: ServerSyncStatus) async throws { + try backgroundContext.performAndWait { + let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "id == %@", id) + + guard let linkedItem = (try? backgroundContext.fetch(fetchRequest))?.first else { return } + linkedItem.serverSyncStatus = Int64(status.rawValue) + } + } func syncPdf(id: String, localPdfURL: URL, url: String) async throws { -// try backgroundContext.performAndWait { -// item.serverSyncStatus = Int64(ServerSyncStatus.isSyncing.rawValue) -// try self.backgroundContext.save() -// } + do { + let uploadRequest = try await uploadFileRequest(id: id, url: url) + if let urlString = uploadRequest.urlString, let uploadUrl = URL(string: urlString) { + try await uploadFile(id: id, localPdfURL: localPdfURL, url: uploadUrl) + // try await services.dataService.saveFilePublisher(requestId: requestId, uploadFileId: uploadFileID, url: url) + } else { + throw SaveArticleError.badData + } - let uploadRequest = try await uploadFileRequest(id: id, url: url) - if let urlString = uploadRequest.urlString, let uploadUrl = URL(string: urlString) { - try await uploadFile(id: id, localPdfURL: localPdfURL, url: uploadUrl) - // try await services.dataService.saveFilePublisher(requestId: requestId, uploadFileId: uploadFileID, url: url) - } else { - throw SaveArticleError.badData + try await updateLinkedItemStatus(id: id, status: .isNSync) + try backgroundContext.performAndWait { + try backgroundContext.save() + } + } catch { + backgroundContext.rollback() + throw error } } func syncPage(id: String, originalHtml: String, title: String?, url: String) async throws { - // try backgroundContext.performAndWait { - // item.serverSyncStatus = Int64(ServerSyncStatus.isSyncing.rawValue) - // try self.backgroundContext.save() - // } - try await savePage(id: id, url: url, title: title ?? url, originalHtml: originalHtml) + do { + try await savePage(id: id, url: url, title: title ?? url, originalHtml: originalHtml) + try await updateLinkedItemStatus(id: id, status: .isNSync) + try backgroundContext.performAndWait { + try backgroundContext.save() + } + } catch { + backgroundContext.performAndWait { + backgroundContext.rollback() + } + throw error + } } func syncUrl(id: String, url: String) async throws { - try await saveURL(id: id, url: url) + do { + try await updateLinkedItemStatus(id: id, status: .isSyncing) + try await saveURL(id: id, url: url) + try backgroundContext.performAndWait { + try backgroundContext.save() + } + } catch { + backgroundContext.performAndWait { + backgroundContext.rollback() + } + throw error + } } func syncLocalCreatedLinkedItem(item: LinkedItem) { switch item.contentReader { case "PDF": -// let id = item.unwrappedID -// let localPdfURL = item.localPdfURL -// let url = item.unwrappedPageURLString -// Task { -// let uploadRequestUrl = try await uploadFileRequest(id: id, url: url) -// uploadFile(id: id, localPdfURL: localPdfURL, url: uploadRequestUrl) -// try await backgroundContext.perform { -// item.serverSyncStatus = Int64(ServerSyncStatus.isNSync.rawValue) -// try self.backgroundContext.save() -// } -// } - break + let id = item.unwrappedID + let localPdfURL = item.localPdfURL + let url = item.unwrappedPageURLString + + if let pdfUrlStr = localPdfURL, let localPdfURL = URL(string: pdfUrlStr) { + Task { + try await syncPdf(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 let url = item.unwrappedPageURLString @@ -98,13 +118,9 @@ public extension DataService { Task { if let originalHtml = originalHtml { - try await savePage(id: id, url: url, title: title, originalHtml: originalHtml) + try await syncPage(id: id, originalHtml: originalHtml, title: title, url: url) } else { - try await saveURL(id: id, url: url) - } - try await backgroundContext.perform { - item.serverSyncStatus = Int64(ServerSyncStatus.isNSync.rawValue) - try self.backgroundContext.save() + try await syncUrl(id: id, url: url) } } default: diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift index a8ac006c1..c67f7e43c 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift @@ -85,6 +85,9 @@ extension DataService { return cachedContent } + // If the page was locally created, make sure they are synced before we pull content + await syncUnsyncedArticleContent(itemID: itemID) + enum QueryResult { case success(result: ArticleProps) case error(error: String) @@ -304,6 +307,49 @@ extension DataService { ) } } + + 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 + } + + if let id = id, let url = url, let title = title, + let serverSyncStatus = serverSyncStatus, + serverSyncStatus != ServerSyncStatus.isNSync.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) + } + try backgroundContext.performAndWait { + try backgroundContext.save() + } + } catch { + // We don't propogate these errors, we just let it pass through so + // the user can attempt to fetch content again. + } + } + } } private extension ArticleContentStatus { From 207a06ed742524db3cb289ea45946231b157a50c Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 1 Jun 2022 20:47:48 -0700 Subject: [PATCH 23/32] Retry loading content if it fails This happens when syncing content, as there is a slight delay between content being synced and content being available Refetching handles this case and any other fetch errors. --- .../Sources/App/Views/WebReader/WebReaderViewModel.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift index f54368817..a7a96a351 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift @@ -12,12 +12,15 @@ struct SafariWebLink: Identifiable { @Published var articleContent: ArticleContent? @Published var errorMessage: String? - func loadContent(dataService: DataService, itemID: String) async { + func loadContent(dataService: DataService, itemID: String, retryCount: Int = 0) async { errorMessage = nil do { articleContent = try await dataService.fetchArticleContent(itemID: itemID) } catch { + if retryCount == 0 { + await loadContent(dataService: dataService, itemID: itemID, retryCount: 1) + } if let fetchError = error as? ContentFetchError { switch fetchError { case .network: From fc990fb2ab44ef8e2cb6afdd9b92fbae59a2e9fb Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 1 Jun 2022 21:04:38 -0700 Subject: [PATCH 24/32] Return recursive call --- .../Sources/App/Views/WebReader/WebReaderViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift index a7a96a351..8255a1289 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift @@ -19,7 +19,7 @@ struct SafariWebLink: Identifiable { articleContent = try await dataService.fetchArticleContent(itemID: itemID) } catch { if retryCount == 0 { - await loadContent(dataService: dataService, itemID: itemID, retryCount: 1) + return await loadContent(dataService: dataService, itemID: itemID, retryCount: 1) } if let fetchError = error as? ContentFetchError { switch fetchError { From e0e7a7a13179cb1bbd5864ee18ec6e5efd5fe74e Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 1 Jun 2022 21:05:10 -0700 Subject: [PATCH 25/32] Remove debug --- .../App/AppExtensions/Share/ExtensionSaveService.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift index 649d9757a..0f887e491 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift @@ -113,10 +113,6 @@ class ExtensionSaveService { } override func cancel() { -// task?.cancel() -// finishOperation() -// -// storeUnresolvedSavedItem() super.cancel() } From 27ba6632731a50eb6226a3451609e31d0767f0f1 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 2 Jun 2022 08:13:31 -0700 Subject: [PATCH 26/32] bump apple version numbers and check build number changes to reset coredata --- apple/Omnivore.xcodeproj/project.pbxproj | 24 +++++++++---------- .../Services/DataService/DataService.swift | 20 +++++++++++----- .../Sources/Utils/UserDefaultKeys.swift | 1 + 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/apple/Omnivore.xcodeproj/project.pbxproj b/apple/Omnivore.xcodeproj/project.pbxproj index bd10aa72a..0229d82b5 100644 --- a/apple/Omnivore.xcodeproj/project.pbxproj +++ b/apple/Omnivore.xcodeproj/project.pbxproj @@ -1239,7 +1239,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.7.0; + MARKETING_VERSION = 1.8.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "app.omnivore.app.ShareExtension-Mac"; @@ -1270,7 +1270,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.7.0; + MARKETING_VERSION = 1.8.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "app.omnivore.app.ShareExtension-Mac"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1352,7 +1352,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.7.0; + MARKETING_VERSION = 1.8.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app; @@ -1386,7 +1386,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.7.0; + MARKETING_VERSION = 1.8.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.7.1; + MARKETING_VERSION = 1.8.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.7.1; + MARKETING_VERSION = 1.8.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.7.1; + MARKETING_VERSION = 1.8.0; MTL_FAST_MATH = YES; OTHER_LDFLAGS = ( "-framework", @@ -1551,7 +1551,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.7.0; + MARKETING_VERSION = 1.8.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_LDFLAGS = ( @@ -1589,7 +1589,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.7.0; + MARKETING_VERSION = 1.8.0; MTL_FAST_MATH = YES; OTHER_LDFLAGS = ( "-framework", @@ -1674,7 +1674,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.7.1; + MARKETING_VERSION = 1.8.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.7.1; + MARKETING_VERSION = 1.8.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.7.1; + MARKETING_VERSION = 1.8.0; PRODUCT_BUNDLE_IDENTIFIER = "app.omnivore.app.share-extension"; PRODUCT_NAME = ShareExtension; SDKROOT = iphoneos; diff --git a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift index a8d79be73..752155036 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift @@ -32,7 +32,7 @@ public final class DataService: ObservableObject { self.backgroundContext = persistentContainer.newBackgroundContext() backgroundContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump - if isFirstTimeRunningNewAppVersion() { + if isFirstTimeRunningNewAppBuild() { resetCoreData() } else { persistentContainer.loadPersistentStores { _, error in @@ -91,14 +91,22 @@ public final class DataService: ObservableObject { backgroundContext = persistentContainer.newBackgroundContext() } - private func isFirstTimeRunningNewAppVersion() -> Bool { - let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") - guard let appVersion = appVersion as? String else { return false } + private func isFirstTimeRunningNewAppBuild() -> Bool { + let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String + let buildNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String + + guard let appVersion = appVersion, let buildNumber = buildNumber else { return false } let lastUsedAppVersion = UserDefaults.standard.string(forKey: UserDefaultKey.lastUsedAppVersion.rawValue) - let isFirstRun = (lastUsedAppVersion ?? "unknown") != appVersion UserDefaults.standard.set(appVersion, forKey: UserDefaultKey.lastUsedAppVersion.rawValue) - return isFirstRun + + let lastUsedAppBuildNumber = UserDefaults.standard.string(forKey: UserDefaultKey.lastUsedAppBuildNumber.rawValue) + UserDefaults.standard.set(buildNumber, forKey: UserDefaultKey.lastUsedAppBuildNumber.rawValue) + + let isFirstRunOfVersion = (lastUsedAppVersion ?? "unknown") != appVersion + let isFirstRunWithBuildNumber = (lastUsedAppBuildNumber ?? "unknown") != buildNumber + + return isFirstRunOfVersion || isFirstRunWithBuildNumber } public func persistPageScrapePayload(_ pageScrape: PageScrapePayload, requestId: String) async throws { diff --git a/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift b/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift index c32f00401..5b496df3f 100644 --- a/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift +++ b/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift @@ -7,4 +7,5 @@ public enum UserDefaultKey: String { case homeFeedlayoutPreference case lastSelectedLinkedItemFilter case lastUsedAppVersion + case lastUsedAppBuildNumber } From 7a645489beb4cfd21de74864e594e7afa9a89605 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 2 Jun 2022 10:48:25 -0700 Subject: [PATCH 27/32] Normalize URLs and match on URL rather than id when saving items locally --- .../Services/DataService/DataService.swift | 54 +++++++++++++++++-- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift index 752155036..0f8078d4b 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift @@ -10,6 +10,16 @@ import Utils let logger = Logger(subsystem: "app.omnivore", category: "data-service") +private extension String { + func replacingRegex(pattern: String, replaceWith: String = "") -> String { + do { + let regex = try NSRegularExpression(pattern: pattern, options: [.caseInsensitive, .anchorsMatchLines]) + let range = NSRange(location: 0, length: utf16.count) + return regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: replaceWith) + } catch { return self } + } +} + public final class DataService: ObservableObject { public static var registerIntercomUser: ((String) -> Void)? public static var showIntercomMessenger: (() -> Void)? @@ -109,19 +119,55 @@ public final class DataService: ObservableObject { return isFirstRunOfVersion || isFirstRunWithBuildNumber } + // based losesly on the normalize-url npm package which we use on the backend + func normalizeURL(_ dirtyURL: String) -> String { + var urlString = dirtyURL + + urlString = urlString.trimmingCharacters(in: .whitespacesAndNewlines) + + if var urlObject = URLComponents(string: urlString) { + // Remove auth + if /* options.stripAuthentication */ true { + urlObject.user = nil + urlObject.password = nil + } + + // Remove hash + if /* options.stripHash */ true { + urlObject.fragment = nil + } + + urlObject.queryItems = urlObject.queryItems?.filter { item in + item.name.starts(with: "utm_") + } + + if /* options.removeTrailingSlash */ true { + urlObject.path = urlObject.path.replacingRegex(pattern: "/$", replaceWith: "") + } + + if let finalUrl = urlObject.url { + return finalUrl.absoluteString + } + } + + return dirtyURL + } + public func persistPageScrapePayload(_ pageScrape: PageScrapePayload, requestId: String) async throws { + let normalizedURL = normalizeURL(pageScrape.url) + try await backgroundContext.perform { [weak self] in guard let self = self else { return } let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() - fetchRequest.predicate = NSPredicate(format: "id == %@", requestId) + fetchRequest.predicate = NSPredicate(format: "pageURLString = %@", normalizedURL) let currentTime = Date() let existingItem = try? self.backgroundContext.fetch(fetchRequest).first let linkedItem = existingItem ?? LinkedItem(entity: LinkedItem.entity(), insertInto: self.backgroundContext) - linkedItem.id = requestId - linkedItem.title = pageScrape.url - linkedItem.pageURLString = pageScrape.url + linkedItem.id = existingItem?.unwrappedID ?? requestId + linkedItem.title = normalizedURL + linkedItem.pageURLString = normalizedURL linkedItem.serverSyncStatus = Int64(ServerSyncStatus.needsCreation.rawValue) linkedItem.savedAt = currentTime linkedItem.createdAt = currentTime From 2ed1ea29b9b2f6b84e63794623f45c15eef6c64c Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 2 Jun 2022 10:58:12 -0700 Subject: [PATCH 28/32] Move NormalizeURL into its own function --- .../Services/DataService/DataService.swift | 44 ---------------- .../Sources/Utils/NormalizeURL.swift | 52 +++++++++++++++++++ 2 files changed, 52 insertions(+), 44 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/Utils/NormalizeURL.swift diff --git a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift index 0f8078d4b..3e005425a 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift @@ -10,16 +10,6 @@ import Utils let logger = Logger(subsystem: "app.omnivore", category: "data-service") -private extension String { - func replacingRegex(pattern: String, replaceWith: String = "") -> String { - do { - let regex = try NSRegularExpression(pattern: pattern, options: [.caseInsensitive, .anchorsMatchLines]) - let range = NSRange(location: 0, length: utf16.count) - return regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: replaceWith) - } catch { return self } - } -} - public final class DataService: ObservableObject { public static var registerIntercomUser: ((String) -> Void)? public static var showIntercomMessenger: (() -> Void)? @@ -119,40 +109,6 @@ public final class DataService: ObservableObject { return isFirstRunOfVersion || isFirstRunWithBuildNumber } - // based losesly on the normalize-url npm package which we use on the backend - func normalizeURL(_ dirtyURL: String) -> String { - var urlString = dirtyURL - - urlString = urlString.trimmingCharacters(in: .whitespacesAndNewlines) - - if var urlObject = URLComponents(string: urlString) { - // Remove auth - if /* options.stripAuthentication */ true { - urlObject.user = nil - urlObject.password = nil - } - - // Remove hash - if /* options.stripHash */ true { - urlObject.fragment = nil - } - - urlObject.queryItems = urlObject.queryItems?.filter { item in - item.name.starts(with: "utm_") - } - - if /* options.removeTrailingSlash */ true { - urlObject.path = urlObject.path.replacingRegex(pattern: "/$", replaceWith: "") - } - - if let finalUrl = urlObject.url { - return finalUrl.absoluteString - } - } - - return dirtyURL - } - public func persistPageScrapePayload(_ pageScrape: PageScrapePayload, requestId: String) async throws { let normalizedURL = normalizeURL(pageScrape.url) diff --git a/apple/OmnivoreKit/Sources/Utils/NormalizeURL.swift b/apple/OmnivoreKit/Sources/Utils/NormalizeURL.swift new file mode 100644 index 000000000..3d31dbe52 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Utils/NormalizeURL.swift @@ -0,0 +1,52 @@ +// +// NormalizeURL.swift +// +// +// Created by Jackson Harper on 6/2/22. +// + +import Foundation + +// based losesly on the normalize-url npm package which we use on the backend +public func normalizeURL(_ dirtyURL: String) -> String { + var urlString = dirtyURL + + urlString = urlString.trimmingCharacters(in: .whitespacesAndNewlines) + + if var urlObject = URLComponents(string: urlString) { + // Remove auth + if /* options.stripAuthentication */ true { + urlObject.user = nil + urlObject.password = nil + } + + // Remove hash + if /* options.stripHash */ true { + urlObject.fragment = nil + } + + urlObject.queryItems = urlObject.queryItems?.filter { item in + item.name.starts(with: "utm_") + } + + if /* options.removeTrailingSlash */ true { + urlObject.path = urlObject.path.replacingRegex(pattern: "/$", replaceWith: "") + } + + if let finalUrl = urlObject.url { + return finalUrl.absoluteString + } + } + + return dirtyURL +} + +private extension String { + func replacingRegex(pattern: String, replaceWith: String = "") -> String { + do { + let regex = try NSRegularExpression(pattern: pattern, options: [.caseInsensitive, .anchorsMatchLines]) + let range = NSRange(location: 0, length: utf16.count) + return regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: replaceWith) + } catch { return self } + } +} From 92f8218960c7c87d3ac95be145a939ebfceab7bf Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 2 Jun 2022 11:37:04 -0700 Subject: [PATCH 29/32] Fetch icon URLs from pages --- .../Share/ExtensionSaveService.swift | 2 +- .../Sources/Models/PageScrapePayload.swift | 17 ++++++----------- .../Services/DataService/DataService.swift | 3 ++- .../DataService/Mutations/SaveArticle.swift | 2 +- apple/Sources/ShareExtension/ShareExtension.js | 10 +++++++++- 5 files changed, 19 insertions(+), 15 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift index 0f887e491..258bb853a 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift @@ -140,7 +140,7 @@ class ExtensionSaveService { try await services.dataService.syncUrl(id: requestId, url: pageScrapePayload.url) case let .pdf(localUrl): try await services.dataService.syncPdf(id: requestId, localPdfURL: localUrl, url: pageScrapePayload.url) - case let .html(html, title): + case let .html(html, title, _): try await services.dataService.syncPage(id: requestId, originalHtml: html, title: title, url: pageScrapePayload.url) } diff --git a/apple/OmnivoreKit/Sources/Models/PageScrapePayload.swift b/apple/OmnivoreKit/Sources/Models/PageScrapePayload.swift index f5bd16e6e..ee4435cc2 100644 --- a/apple/OmnivoreKit/Sources/Models/PageScrapePayload.swift +++ b/apple/OmnivoreKit/Sources/Models/PageScrapePayload.swift @@ -9,15 +9,9 @@ import UniformTypeIdentifiers let URLREGEX = #"[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)"# public struct PageScrapePayload { - public struct HTMLPayload { - let url: String - let title: String? - let html: String - } - public enum ContentType { case none - case html(html: String, title: String?) + case html(html: String, title: String?, iconURL: String?) case pdf(localUrl: URL) } @@ -34,9 +28,9 @@ public struct PageScrapePayload { self.contentType = .pdf(localUrl: localUrl) } - init(url: String, title: String?, html: String) { + init(url: String, title: String?, html: String, iconURL: String? = nil) { self.url = url - self.contentType = .html(html: html, title: title) + self.contentType = .html(html: html, title: title, iconURL: iconURL) } } @@ -280,8 +274,9 @@ private extension PageScrapePayload { static func makeFromDictionary(_ dictionary: NSDictionary) -> PageScrapePayload? { let results = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as? NSDictionary guard let url = results?["url"] as? String else { return nil } - let html = results?["documentHTML"] as? String + let html = results?["originalHTML"] as? String let title = results?["title"] as? String + let iconURL = results?["iconURL"] as? String let contentType = results?["contentType"] as? String // If we were not able to capture any HTML, treat this as a URL and @@ -297,7 +292,7 @@ private extension PageScrapePayload { } if let html = html { - return PageScrapePayload(url: url, title: title, html: html) + return PageScrapePayload(url: url, title: title, html: html, iconURL: iconURL) } return PageScrapePayload(url: url) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift index 3e005425a..b2db01cab 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift @@ -154,9 +154,10 @@ public final class DataService: ObservableObject { // self.createThumbnailFor(inputUrl: localUrl, at: thumbnailUrl) // linkedItem.imageURLString = thumbnailUrl.absoluteString - case let .html(html: html, title: title): + case let .html(html: html, title: title, iconURL: iconURL): linkedItem.contentReader = "WEB" linkedItem.originalHtml = html + linkedItem.imageURLString = iconURL linkedItem.title = title ?? self.titleFromPdfFile(pageScrape.url) case .none: print("SAVING URL", linkedItem.unwrappedPageURLString) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SaveArticle.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SaveArticle.swift index bd1b0fdfa..9406867ad 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SaveArticle.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SaveArticle.swift @@ -92,7 +92,7 @@ public extension DataService { } let preparedDocument: InputObjects.PreparedDocumentInput? = { - if case let .html(html, title) = pageScrapePayload.contentType { + if case let .html(html, title, _) = pageScrapePayload.contentType { return InputObjects.PreparedDocumentInput( document: html, pageInfo: InputObjects.PageInfoInput(title: OptionalArgument(title)) diff --git a/apple/Sources/ShareExtension/ShareExtension.js b/apple/Sources/ShareExtension/ShareExtension.js index fd7c4c0b6..24de12ff9 100644 --- a/apple/Sources/ShareExtension/ShareExtension.js +++ b/apple/Sources/ShareExtension/ShareExtension.js @@ -1,12 +1,20 @@ var ShareExtension = function() {}; +function iconURL() { + try { + return document.querySelector("link[rel='apple-touch-icon'], link[rel='shortcut icon'], link[rel='icon']").href + } catch {} + return null +} + ShareExtension.prototype = { run: function(arguments) { arguments.completionFunction({ 'url': window.location.href, 'title': document.title.toString(), + 'iconURL': iconURL(), 'contentType': document.contentType, - 'documentHTML': new XMLSerializer().serializeToString(document), + 'originalHTML': new XMLSerializer().serializeToString(document) }); } }; From ef72e09006f48cb90640a8bbe37cd967ab518dfd Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 2 Jun 2022 15:29:57 -0700 Subject: [PATCH 30/32] Start implementing the new share extension, mostly so we can show the sync status --- .../Share/ExtensionSaveService.swift | 49 +++- .../Share/ShareExtensionScene.swift | 19 +- .../WebReader/WebReaderLoadingContainer.swift | 2 + .../Queries/ArticleContentQuery.swift | 2 +- .../Sources/Views/ShareExtensionView.swift | 255 ++++++++++-------- .../ShareExtensionViewController.swift | 2 +- 6 files changed, 192 insertions(+), 137 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift index 258bb853a..f1ce8f5a2 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift @@ -10,8 +10,6 @@ import Models import Services import Views -typealias UpdateStatusFunc = (ShareExtensionStatus) -> Void - class ExtensionSaveService { let queue: OperationQueue @@ -19,7 +17,7 @@ class ExtensionSaveService { self.queue = OperationQueue() } - private func queueSaveOperation(_ pageScrape: PageScrapePayload, updateStatusFunc: UpdateStatusFunc?) { + private func queueSaveOperation(_ pageScrape: PageScrapePayload, requestId: String, shareExtensionViewModel: ShareExtensionChildViewModel) { ProcessInfo().performExpiringActivity(withReason: "app.omnivore.SaveActivity") { [self] expiring in guard !expiring else { self.queue.cancelAllOperations() @@ -27,22 +25,47 @@ class ExtensionSaveService { return } - let operation = SaveOperation(pageScrapePayload: pageScrape, updateStatusFunc: updateStatusFunc) + let operation = SaveOperation(pageScrapePayload: pageScrape, requestId: requestId, shareExtensionViewModel: shareExtensionViewModel) self.queue.addOperation(operation) self.queue.waitUntilAllOperationsAreFinished() } } - public func save(_ extensionContext: NSExtensionContext, updateStatusFunc: UpdateStatusFunc?) { + public func save(_ extensionContext: NSExtensionContext, requestId: String, shareExtensionViewModel: ShareExtensionChildViewModel) { PageScraper.scrape(extensionContext: extensionContext) { [weak self] result in guard let self = self else { return } switch result { case let .success(payload): - self.queueSaveOperation(payload, updateStatusFunc: updateStatusFunc) + DispatchQueue.main.async { + shareExtensionViewModel.status = .saved + + let url = URLComponents(string: payload.url) + let hostname = URL(string: payload.url)?.host ?? "" + + switch payload.contentType { + case let .html(html: _, title: title, iconURL: iconURL): + shareExtensionViewModel.title = title + shareExtensionViewModel.iconURL = iconURL + shareExtensionViewModel.url = hostname + case .none: + shareExtensionViewModel.url = hostname + shareExtensionViewModel.title = "Saving: " + payload.url + if var url = url { + url.path = "/favicon.ico" + shareExtensionViewModel.iconURL = url.url?.absoluteString + } + case let .pdf(localUrl: _): + shareExtensionViewModel.title = "Saving: " + payload.url + shareExtensionViewModel.url = hostname + } + } + self.queueSaveOperation(payload, requestId: requestId, shareExtensionViewModel: shareExtensionViewModel) case let .failure(error): - print("failed", error) + DispatchQueue.main.async { + shareExtensionViewModel.status = .failed(error: .unknown(description: "Could not retrieve content")) + } } } } @@ -51,7 +74,7 @@ class ExtensionSaveService { let requestId: String let services: Services let pageScrapePayload: PageScrapePayload - let updateStatusFunc: UpdateStatusFunc? + let shareExtensionViewModel: ShareExtensionChildViewModel var queue: OperationQueue? var uploadTask: URLSessionTask? @@ -62,13 +85,13 @@ class ExtensionSaveService { case finished } - init(pageScrapePayload: PageScrapePayload, updateStatusFunc: UpdateStatusFunc? = nil) { + init(pageScrapePayload: PageScrapePayload, requestId: String, shareExtensionViewModel: ShareExtensionChildViewModel) { self.pageScrapePayload = pageScrapePayload - self.updateStatusFunc = updateStatusFunc + self.requestId = requestId + self.shareExtensionViewModel = shareExtensionViewModel self.state = .created self.services = Services() - self.requestId = UUID().uuidString.lowercased() } open var state: State = .created { @@ -118,9 +141,7 @@ class ExtensionSaveService { private func updateStatus(newStatus: ShareExtensionStatus) { DispatchQueue.main.async { - if let updateStatusFunc = self.updateStatusFunc { - updateStatusFunc(newStatus) - } + self.shareExtensionViewModel.status = newStatus } } diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift index f5b86535c..a5ea021e1 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift @@ -20,29 +20,27 @@ public extension PlatformViewController { } } -final class ShareExtensionViewModel: ObservableObject { +public class ShareExtensionViewModel: ObservableObject { @Published var title: String? @Published var status: ShareExtensionStatus = .processing @Published var debugText: String? - var subscriptions = Set() - var backgroundTask: UIBackgroundTaskIdentifier? - let requestID = UUID().uuidString.lowercased() let saveService = ExtensionSaveService() + let requestId = UUID().uuidString.lowercased() func handleReadNowAction(extensionContext: NSExtensionContext?) { #if os(iOS) if let application = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) as? UIApplication { - let deepLinkUrl = NSURL(string: "omnivore://shareExtensionRequestID/\(requestID)") + let deepLinkUrl = NSURL(string: "omnivore://shareExtensionRequestID/\(requestId)") application.perform(NSSelectorFromString("openURL:"), with: deepLinkUrl) } #endif extensionContext?.completeRequest(returningItems: [], completionHandler: nil) } - func savePage(extensionContext: NSExtensionContext?) { + func savePage(extensionContext: NSExtensionContext?, shareExtensionViewModel: ShareExtensionChildViewModel) { if let extensionContext = extensionContext { - saveService.save(extensionContext, updateStatusFunc: updateStatus) + saveService.save(extensionContext, requestId: requestId, shareExtensionViewModel: shareExtensionViewModel) } else { updateStatus(.failed(error: .unknown(description: "Internal Error"))) } @@ -58,13 +56,12 @@ final class ShareExtensionViewModel: ObservableObject { struct ShareExtensionView: View { let extensionContext: NSExtensionContext? @StateObject private var viewModel = ShareExtensionViewModel() + @StateObject private var childViewModel = ShareExtensionChildViewModel() var body: some View { ShareExtensionChildView( - debugText: viewModel.debugText, - title: viewModel.title, - status: viewModel.status, - onAppearAction: { viewModel.savePage(extensionContext: extensionContext) }, + viewModel: childViewModel, + onAppearAction: { viewModel.savePage(extensionContext: extensionContext, shareExtensionViewModel: childViewModel) }, readNowButtonAction: { viewModel.handleReadNowAction(extensionContext: extensionContext) }, dismissButtonTappedAction: { _, _ in extensionContext?.completeRequest(returningItems: [], completionHandler: nil) diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift index 087be99c0..046d0673a 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift @@ -24,6 +24,8 @@ import Utils guard let username = username else { return } + // If the page was locally created, make sure they are synced before we pull content + await dataService.syncUnsyncedArticleContent(itemID: requestID) await fetchLinkedItem(dataService: dataService, requestID: requestID, username: username) } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift index c67f7e43c..29aca5822 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift @@ -308,7 +308,7 @@ extension DataService { } } - func syncUnsyncedArticleContent(itemID: String) async { + public func syncUnsyncedArticleContent(itemID: String) async { let linkedItemFetchRequest: NSFetchRequest = LinkedItem.fetchRequest() linkedItemFetchRequest.predicate = NSPredicate( format: "id == %@", itemID diff --git a/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift b/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift index acc4bd060..21b318978 100644 --- a/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift +++ b/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift @@ -2,6 +2,15 @@ import Models import SwiftUI import Utils +public class ShareExtensionChildViewModel: ObservableObject { + @Published public var status: ShareExtensionStatus = .processing + @Published public var title: String? + @Published public var url: String? + @Published public var iconURL: String? + + public init() {} +} + public enum ShareExtensionStatus { case processing case saved @@ -25,6 +34,32 @@ public enum ShareExtensionStatus { } } +struct CornerRadiusStyle: ViewModifier { + var radius: CGFloat + var corners: UIRectCorner + + struct CornerRadiusShape: Shape { + var radius = CGFloat.infinity + var corners = UIRectCorner.allCorners + + func path(in rect: CGRect) -> Path { + let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius)) + return Path(path.cgPath) + } + } + + func body(content: Content) -> some View { + content + .clipShape(CornerRadiusShape(radius: radius, corners: corners)) + } +} + +extension View { + func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View { + ModifiedContent(content: self, modifier: CornerRadiusStyle(radius: radius, corners: corners)) + } +} + private extension SaveArticleError { var displayMessage: String { switch self { @@ -89,32 +124,26 @@ struct CheckmarkButtonView: View { } public struct ShareExtensionChildView: View { - let debugText: String? - let title: String? - let status: ShareExtensionStatus + let viewModel: ShareExtensionChildViewModel let onAppearAction: () -> Void let readNowButtonAction: () -> Void let dismissButtonTappedAction: (ReminderTime?, Bool) -> Void + @State var reminderTime: ReminderTime? + @State var hideUntilReminded = false + public init( - debugText: String?, - title: String?, - status: ShareExtensionStatus, + viewModel: ShareExtensionChildViewModel, onAppearAction: @escaping () -> Void, readNowButtonAction: @escaping () -> Void, dismissButtonTappedAction: @escaping (ReminderTime?, Bool) -> Void ) { - self.debugText = debugText - self.title = title - self.status = status + self.viewModel = viewModel self.onAppearAction = onAppearAction self.readNowButtonAction = readNowButtonAction self.dismissButtonTappedAction = dismissButtonTappedAction } - @State var reminderTime: ReminderTime? - @State var hideUntilReminded = false - private func handleReminderTimeSelection(_ selectedTime: ReminderTime) { if selectedTime == reminderTime { reminderTime = nil @@ -125,111 +154,117 @@ public struct ShareExtensionChildView: View { } } + private var titleText: String { + switch viewModel.status { + case .saved, .synced: + return "Saved to Omnivore" + case .processing: + return "Saving to Omnivore" + default: + return "Error saving to Omnivore" + } + } + + private var cloudIconName: String { + switch viewModel.status { + case .synced: + return "checkmark.icloud" + case .saved, .processing: + return "icloud" + case .failed(error: _), .syncFailed(error: _): + return "exclamationmark.icloud" + } + } + + private var cloudIconColor: Color { + switch viewModel.status { + case .saved, .processing: + return .appGrayText + case .failed(error: _), .syncFailed(error: _): + return .red + case .synced: + return .blue + } + } + + public var previewCard: some View { + HStack { + if let iconURLStr = viewModel.iconURL, let iconURL = URL(string: iconURLStr) { + AsyncLoadingImage(url: iconURL) { imageStatus in + if case let AsyncImageStatus.loaded(image) = imageStatus { + image + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 61, height: 61) + } else if case AsyncImageStatus.loading = imageStatus { + Color.appButtonBackground + .aspectRatio(contentMode: .fill) + .frame(width: 61, height: 61) + } else { + EmptyView() + } + } + } else { + EmptyView() + .frame(width: 61, height: 61) + } + VStack(alignment: .leading) { + Text(viewModel.title ?? "") + .lineLimit(1) + .foregroundColor(.appGrayText) + .font(Font.system(size: 15, weight: .semibold)) + Text(viewModel.url ?? "") + .lineLimit(1) + .foregroundColor(.appGrayText) + .font(Font.system(size: 12, weight: .regular)) + } + Spacer() + VStack { + Spacer() + Image(systemName: cloudIconName) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 12, height: 12, alignment: .trailing) + .foregroundColor(cloudIconColor) + // .padding(.trailing, 6) + .padding(EdgeInsets(top: 0, leading: 0, bottom: 8, trailing: 8)) + } + } + .background(Color(hex: "#363636")) + .frame(maxWidth: .infinity, maxHeight: 61) + .cornerRadius(8) + } + public var body: some View { VStack(alignment: .leading) { - #if DEBUG - if let debugText = debugText { - Text(debugText) - } - #endif + Text(titleText) + .foregroundColor(.appGrayText) + .font(Font.system(size: 17, weight: .semibold)) + .frame(maxWidth: .infinity, alignment: .center) + .padding(.top, 23) + .padding(.bottom, 16) - if let title = title { - Text(title) - .font(.appHeadline) - .lineLimit(1) - .padding(.trailing, 50) - Divider() - } + Rectangle() + .foregroundColor(.appGrayText) + .frame(maxWidth: .infinity, maxHeight: 1) + .opacity(0.06) + .padding(.top, 0) + .padding(.bottom, 16) + + previewCard + .padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) Spacer() - switch status { - case .processing: - HStack { - Spacer() - Text("Saving...") - Spacer() - } - case .saved: - HStack { - Spacer() - Text("Syncing...") - Spacer() - } - case .synced: - HStack(spacing: 4) { - Text("Saved to Omnivore") - .font(.appTitleThree) - .foregroundColor(.appGrayText) - .padding(.trailing, 16) - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) - .lineLimit(nil) - } - .padding() - case let .failed(error: error): - HStack { - Spacer() - Text("Failed to save:" + error.displayMessage) - Spacer() - } - case let .syncFailed(error: error): - HStack { - Spacer() - Text("Failed to sync:" + error.displayMessage) - Spacer() - } - } - - ScrollView { - if FeatureFlag.enableRemindersFromShareExtension { - VStack(spacing: 0) { - CheckmarkButtonView( - titleText: "Remind me tonight", - isSelected: reminderTime == .tonight, - action: { handleReminderTimeSelection(.tonight) } - ) - - Divider() - - CheckmarkButtonView( - titleText: "Remind me tomorrow", - isSelected: reminderTime == .tomorrow, - action: { handleReminderTimeSelection(.tomorrow) } - ) - - Divider() - - CheckmarkButtonView( - titleText: "Remind me this weekend", - isSelected: reminderTime == .thisWeekend, - action: { handleReminderTimeSelection(.thisWeekend) } - ) - } - .cornerRadius(8) - } - - if FeatureFlag.enableSnoozeFromShareExtension { - CheckmarkButtonView( - titleText: "Hide it until then", - isSelected: hideUntilReminded, - action: { hideUntilReminded.toggle() } - ) - .cornerRadius(8) - .padding(.top, 16) - } - } - .padding(.horizontal) - HStack { - if case ShareExtensionStatus.saved = status, FeatureFlag.enableReadNow { - Button( - action: { readNowButtonAction() }, - label: { Text("Read Now").frame(maxWidth: .infinity) } - ) - .buttonStyle(RoundedRectButtonStyle()) - } - if case ShareExtensionStatus.processing = status, FeatureFlag.enableReadNow { + // if case ShareExtensionStatus.saved = status || case ShareExtensionStatus.synced = status { + Button( + action: { readNowButtonAction() }, + label: { Text("Read Now").frame(maxWidth: .infinity) } + ) + .buttonStyle(RoundedRectButtonStyle()) + // } + if case ShareExtensionStatus.processing = viewModel.status, FeatureFlag.enableReadNow { Button(action: {}, label: { ProgressView().frame(maxWidth: .infinity) }) .buttonStyle(RoundedRectButtonStyle()) } diff --git a/apple/Sources/ShareExtension/ShareExtensionViewController.swift b/apple/Sources/ShareExtension/ShareExtensionViewController.swift index ba45041e6..9b118b175 100644 --- a/apple/Sources/ShareExtension/ShareExtensionViewController.swift +++ b/apple/Sources/ShareExtension/ShareExtensionViewController.swift @@ -12,7 +12,7 @@ import Utils embed( childViewController: UIViewController.makeShareExtensionController(extensionContext: extensionContext), - heightRatio: 0.3 + heightRatio: 0.5 ) } } From 5547a7bb6e82c9abb94a7cec8fe66f31bf39ecbc Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 2 Jun 2022 16:17:27 -0700 Subject: [PATCH 31/32] Improve preview images from content --- .../Sources/Views/ShareExtensionView.swift | 18 ++++++++---------- apple/Sources/ShareExtension/ShareExtension.js | 3 +++ .../ShareExtensionViewController.swift | 2 +- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift b/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift index 21b318978..011862f6c 100644 --- a/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift +++ b/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift @@ -196,12 +196,14 @@ public struct ShareExtensionChildView: View { .resizable() .aspectRatio(contentMode: .fill) .frame(width: 61, height: 61) + .clipped() } else if case AsyncImageStatus.loading = imageStatus { Color.appButtonBackground .aspectRatio(contentMode: .fill) .frame(width: 61, height: 61) } else { EmptyView() + .frame(width: 61, height: 61) } } } else { @@ -257,16 +259,12 @@ public struct ShareExtensionChildView: View { Spacer() HStack { - // if case ShareExtensionStatus.saved = status || case ShareExtensionStatus.synced = status { - Button( - action: { readNowButtonAction() }, - label: { Text("Read Now").frame(maxWidth: .infinity) } - ) - .buttonStyle(RoundedRectButtonStyle()) - // } - if case ShareExtensionStatus.processing = viewModel.status, FeatureFlag.enableReadNow { - Button(action: {}, label: { ProgressView().frame(maxWidth: .infinity) }) - .buttonStyle(RoundedRectButtonStyle()) + if FeatureFlag.enableReadNow { + Button( + action: { readNowButtonAction() }, + label: { Text("Read Now").frame(maxWidth: .infinity) } + ) + .buttonStyle(RoundedRectButtonStyle()) } Button( action: { diff --git a/apple/Sources/ShareExtension/ShareExtension.js b/apple/Sources/ShareExtension/ShareExtension.js index 24de12ff9..1b2f22b4d 100644 --- a/apple/Sources/ShareExtension/ShareExtension.js +++ b/apple/Sources/ShareExtension/ShareExtension.js @@ -2,6 +2,9 @@ var ShareExtension = function() {}; function iconURL() { try { + const previewImage = document.querySelector("meta[property='og:image'], meta[name='twitter:image']").content + if (previewImage) { return previewImage } + return document.querySelector("link[rel='apple-touch-icon'], link[rel='shortcut icon'], link[rel='icon']").href } catch {} return null diff --git a/apple/Sources/ShareExtension/ShareExtensionViewController.swift b/apple/Sources/ShareExtension/ShareExtensionViewController.swift index 9b118b175..96a9c88ce 100644 --- a/apple/Sources/ShareExtension/ShareExtensionViewController.swift +++ b/apple/Sources/ShareExtension/ShareExtensionViewController.swift @@ -12,7 +12,7 @@ import Utils embed( childViewController: UIViewController.makeShareExtensionController(extensionContext: extensionContext), - heightRatio: 0.5 + heightRatio: 0.55 ) } } From b736bc721a8654728e291b641e45e5f76545bb1c Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 2 Jun 2022 16:25:27 -0700 Subject: [PATCH 32/32] Better image previews when saving from URL --- .../App/AppExtensions/Share/ExtensionSaveService.swift | 4 ++-- apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift index f1ce8f5a2..943dd94ec 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift @@ -51,13 +51,13 @@ class ExtensionSaveService { shareExtensionViewModel.url = hostname case .none: shareExtensionViewModel.url = hostname - shareExtensionViewModel.title = "Saving: " + payload.url + shareExtensionViewModel.title = payload.url if var url = url { url.path = "/favicon.ico" shareExtensionViewModel.iconURL = url.url?.absoluteString } case let .pdf(localUrl: _): - shareExtensionViewModel.title = "Saving: " + payload.url + shareExtensionViewModel.title = payload.url shareExtensionViewModel.url = hostname } } diff --git a/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift b/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift index 011862f6c..14f25c748 100644 --- a/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift +++ b/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift @@ -197,13 +197,10 @@ public struct ShareExtensionChildView: View { .aspectRatio(contentMode: .fill) .frame(width: 61, height: 61) .clipped() - } else if case AsyncImageStatus.loading = imageStatus { + } else { Color.appButtonBackground .aspectRatio(contentMode: .fill) .frame(width: 61, height: 61) - } else { - EmptyView() - .frame(width: 61, height: 61) } } } else {