mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #760 from omnivore-app/fix/ios-scrape-failures
Fix issue with ShareExtension.js not executing on iOS
This commit is contained in:
commit
0529c23c00
33 changed files with 568 additions and 313 deletions
|
|
@ -18,7 +18,7 @@ class ExtensionSaveService {
|
|||
self.queue = OperationQueue()
|
||||
}
|
||||
|
||||
private func queueSaveOperation(_ pageScrape: PageScrapePayload, requestId: String, shareExtensionViewModel: ShareExtensionChildViewModel) {
|
||||
private func queueSaveOperation(_ pageScrape: PageScrapePayload, shareExtensionViewModel: ShareExtensionChildViewModel) {
|
||||
ProcessInfo().performExpiringActivity(withReason: "app.omnivore.SaveActivity") { [self] expiring in
|
||||
guard !expiring else {
|
||||
self.queue.cancelAllOperations()
|
||||
|
|
@ -26,14 +26,14 @@ class ExtensionSaveService {
|
|||
return
|
||||
}
|
||||
|
||||
let operation = SaveOperation(pageScrapePayload: pageScrape, requestId: requestId, shareExtensionViewModel: shareExtensionViewModel)
|
||||
let operation = SaveOperation(pageScrapePayload: pageScrape, shareExtensionViewModel: shareExtensionViewModel)
|
||||
|
||||
self.queue.addOperation(operation)
|
||||
self.queue.waitUntilAllOperationsAreFinished()
|
||||
}
|
||||
}
|
||||
|
||||
public func save(_ extensionContext: NSExtensionContext, requestId: String, shareExtensionViewModel: ShareExtensionChildViewModel) {
|
||||
public func save(_ extensionContext: NSExtensionContext, shareExtensionViewModel: ShareExtensionChildViewModel) {
|
||||
PageScraper.scrape(extensionContext: extensionContext) { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
|
||||
|
|
@ -68,8 +68,8 @@ class ExtensionSaveService {
|
|||
}
|
||||
}
|
||||
}
|
||||
self.queueSaveOperation(payload, requestId: requestId, shareExtensionViewModel: shareExtensionViewModel)
|
||||
case let .failure:
|
||||
self.queueSaveOperation(payload, shareExtensionViewModel: shareExtensionViewModel)
|
||||
case .failure:
|
||||
DispatchQueue.main.async {
|
||||
shareExtensionViewModel.status = .failed(error: .unknown(description: "Could not retrieve content"))
|
||||
}
|
||||
|
|
@ -78,7 +78,6 @@ class ExtensionSaveService {
|
|||
}
|
||||
|
||||
class SaveOperation: Operation, URLSessionDelegate {
|
||||
let requestId: String
|
||||
let services: Services
|
||||
let pageScrapePayload: PageScrapePayload
|
||||
let shareExtensionViewModel: ShareExtensionChildViewModel
|
||||
|
|
@ -92,9 +91,8 @@ class ExtensionSaveService {
|
|||
case finished
|
||||
}
|
||||
|
||||
init(pageScrapePayload: PageScrapePayload, requestId: String, shareExtensionViewModel: ShareExtensionChildViewModel) {
|
||||
init(pageScrapePayload: PageScrapePayload, shareExtensionViewModel: ShareExtensionChildViewModel) {
|
||||
self.pageScrapePayload = pageScrapePayload
|
||||
self.requestId = requestId
|
||||
self.shareExtensionViewModel = shareExtensionViewModel
|
||||
|
||||
self.state = .created
|
||||
|
|
@ -138,7 +136,7 @@ class ExtensionSaveService {
|
|||
queue = OperationQueue()
|
||||
|
||||
Task {
|
||||
await persist(services: self.services, pageScrapePayload: self.pageScrapePayload, requestId: self.requestId)
|
||||
await persist(services: self.services, pageScrapePayload: self.pageScrapePayload)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -146,38 +144,48 @@ class ExtensionSaveService {
|
|||
super.cancel()
|
||||
}
|
||||
|
||||
private func updateStatus(newStatus: ShareExtensionStatus) {
|
||||
private func updateStatus(_ requestId: String?, newStatus: ShareExtensionStatus) {
|
||||
DispatchQueue.main.async {
|
||||
self.shareExtensionViewModel.status = newStatus
|
||||
if let requestId = requestId {
|
||||
self.shareExtensionViewModel.requestId = requestId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func persist(services: Services, pageScrapePayload: PageScrapePayload, requestId: String) async {
|
||||
private func persist(services: Services, pageScrapePayload: PageScrapePayload) async {
|
||||
var requestId = shareExtensionViewModel.requestId
|
||||
|
||||
do {
|
||||
try await services.dataService.persistPageScrapePayload(pageScrapePayload, requestId: requestId)
|
||||
} catch {
|
||||
updateStatus(newStatus: .failed(error: SaveArticleError.unknown(description: "Unable to access content")))
|
||||
updateStatus(nil, newStatus: .failed(error: SaveArticleError.unknown(description: "Unable to access content")))
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
updateStatus(newStatus: .saved)
|
||||
updateStatus(requestId, newStatus: .saved)
|
||||
|
||||
switch pageScrapePayload.contentType {
|
||||
case .none:
|
||||
try await services.dataService.syncUrl(id: requestId, url: pageScrapePayload.url)
|
||||
requestId = try await services.dataService.createPageFromUrl(id: requestId, url: pageScrapePayload.url)
|
||||
case let .pdf(localUrl):
|
||||
try await services.dataService.syncPdf(id: requestId, localPdfURL: localUrl, url: pageScrapePayload.url)
|
||||
try await services.dataService.createPageFromPdf(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)
|
||||
requestId = try await services.dataService.createPage(
|
||||
id: requestId,
|
||||
originalHtml: html,
|
||||
title: title,
|
||||
url: pageScrapePayload.url
|
||||
)
|
||||
}
|
||||
|
||||
} catch {
|
||||
updateStatus(newStatus: .syncFailed(error: SaveArticleError.unknown(description: "Unknown Error")))
|
||||
updateStatus(nil, newStatus: .syncFailed(error: SaveArticleError.unknown(description: "Unknown Error")))
|
||||
return
|
||||
}
|
||||
|
||||
updateStatus(newStatus: .synced)
|
||||
updateStatus(requestId, newStatus: .synced)
|
||||
state = .finished
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,13 +22,11 @@ public extension PlatformViewController {
|
|||
|
||||
public class ShareExtensionViewModel: ObservableObject {
|
||||
@Published var title: String?
|
||||
@Published var status: ShareExtensionStatus = .processing
|
||||
@Published var debugText: String?
|
||||
|
||||
let saveService = ExtensionSaveService()
|
||||
let requestId = UUID().uuidString.lowercased()
|
||||
|
||||
func handleReadNowAction(extensionContext: NSExtensionContext?) {
|
||||
func handleReadNowAction(requestId: String, extensionContext: NSExtensionContext?) {
|
||||
#if os(iOS)
|
||||
if let application = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) as? UIApplication {
|
||||
let deepLinkUrl = NSURL(string: "omnivore://shareExtensionRequestID/\(requestId)")
|
||||
|
|
@ -40,15 +38,11 @@ public class ShareExtensionViewModel: ObservableObject {
|
|||
|
||||
func savePage(extensionContext: NSExtensionContext?, shareExtensionViewModel: ShareExtensionChildViewModel) {
|
||||
if let extensionContext = extensionContext {
|
||||
saveService.save(extensionContext, requestId: requestId, shareExtensionViewModel: shareExtensionViewModel)
|
||||
saveService.save(extensionContext, shareExtensionViewModel: shareExtensionViewModel)
|
||||
} else {
|
||||
updateStatus(.failed(error: .unknown(description: "Internal Error")))
|
||||
}
|
||||
}
|
||||
|
||||
private func updateStatus(_ newStatus: ShareExtensionStatus) {
|
||||
DispatchQueue.main.async {
|
||||
self.status = newStatus
|
||||
DispatchQueue.main.async {
|
||||
shareExtensionViewModel.status = .failed(error: .unknown(description: "Internal Error"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -62,7 +56,7 @@ struct ShareExtensionView: View {
|
|||
ShareExtensionChildView(
|
||||
viewModel: childViewModel,
|
||||
onAppearAction: { viewModel.savePage(extensionContext: extensionContext, shareExtensionViewModel: childViewModel) },
|
||||
readNowButtonAction: { viewModel.handleReadNowAction(extensionContext: extensionContext) },
|
||||
readNowButtonAction: { viewModel.handleReadNowAction(requestId: $0, extensionContext: extensionContext) },
|
||||
dismissButtonTappedAction: { _, _ in
|
||||
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,15 +21,14 @@ import Utils
|
|||
let url: URL
|
||||
}
|
||||
|
||||
let pdfURL: URL
|
||||
let viewModel: PDFViewerViewModel
|
||||
|
||||
@StateObject var pdfStateObject = PDFStateObject()
|
||||
@State var readerView: Bool = false
|
||||
@State private var shareLink: ShareLink?
|
||||
@State private var errorMessage: String?
|
||||
|
||||
init(remoteURL: URL, viewModel: PDFViewerViewModel) {
|
||||
self.pdfURL = viewModel.pdfItem.localPdfURL ?? remoteURL
|
||||
init(viewModel: PDFViewerViewModel) {
|
||||
self.viewModel = viewModel
|
||||
}
|
||||
|
||||
|
|
@ -135,12 +134,21 @@ import Utils
|
|||
.sheet(item: $shareLink) {
|
||||
ShareSheet(activityItems: [$0.url])
|
||||
}
|
||||
} else if let errorMessage = errorMessage {
|
||||
Text(errorMessage)
|
||||
} else {
|
||||
ProgressView()
|
||||
.task {
|
||||
let document = HighlightedDocument(url: pdfURL, viewModel: viewModel)
|
||||
pdfStateObject.document = document
|
||||
pdfStateObject.coordinator = PDFViewCoordinator(document: document, viewModel: viewModel)
|
||||
// NOTE: the issue here is the PDF is downloaded, but saved to a URL we don't know about
|
||||
// because it is changed.
|
||||
let pdfURL = await viewModel.downloadPDF(dataService: dataService)
|
||||
if let pdfURL = pdfURL {
|
||||
let document = HighlightedDocument(url: pdfURL, viewModel: viewModel)
|
||||
pdfStateObject.document = document
|
||||
pdfStateObject.coordinator = PDFViewCoordinator(document: document, viewModel: viewModel)
|
||||
} else {
|
||||
errorMessage = "Unable to download PDF: \(pdfURL)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ public final class PDFViewerViewModel: ObservableObject {
|
|||
@Published public var readerView: Bool = false
|
||||
|
||||
public let pdfItem: PDFItem
|
||||
private var storedURL: URL?
|
||||
|
||||
var subscriptions = Set<AnyCancellable>()
|
||||
|
||||
|
|
@ -81,4 +80,25 @@ public final class PDFViewerViewModel: ObservableObject {
|
|||
|
||||
return components?.url
|
||||
}
|
||||
|
||||
public var itemDownloaded: Bool {
|
||||
if let localPdfURL = pdfItem.localPdfURL, FileManager.default.fileExists(atPath: localPdfURL.path) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public func downloadPDF(dataService: DataService) async -> URL? {
|
||||
do {
|
||||
if itemDownloaded {
|
||||
return pdfItem.localPdfURL
|
||||
}
|
||||
if let localURL = try await dataService.fetchPDFData(slug: pdfItem.slug, pageURLString: pdfItem.originalArticleURL) {
|
||||
return localURL
|
||||
}
|
||||
} catch {
|
||||
print("error downloading PDF", error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -300,7 +300,7 @@ struct LinkItemDetailView: View {
|
|||
@ViewBuilder private var fixedNavBarReader: some View {
|
||||
if let pdfItem = viewModel.pdfItem, let pdfURL = pdfItem.pdfURL {
|
||||
#if os(iOS)
|
||||
PDFViewer(remoteURL: pdfURL, viewModel: PDFViewerViewModel(pdfItem: pdfItem))
|
||||
PDFViewer(viewModel: PDFViewerViewModel(pdfItem: pdfItem))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#elseif os(macOS)
|
||||
PDFWrapperView(pdfURL: pdfURL)
|
||||
|
|
|
|||
|
|
@ -6,9 +6,8 @@ import WebKit
|
|||
|
||||
#if os(iOS)
|
||||
struct WebReader: UIViewRepresentable {
|
||||
let htmlContent: String
|
||||
let highlightsJSONString: String
|
||||
let item: LinkedItem
|
||||
let articleContent: ArticleContent
|
||||
let openLinkAction: (URL) -> Void
|
||||
let webViewActionHandler: (WKScriptMessage, WKScriptMessageReplyHandler?) -> Void
|
||||
let navBarVisibilityRatioUpdater: (Double) -> Void
|
||||
|
|
@ -143,9 +142,8 @@ import WebKit
|
|||
|
||||
webView.loadHTMLString(
|
||||
WebReaderContent(
|
||||
htmlContent: htmlContent,
|
||||
highlightsJSONString: highlightsJSONString,
|
||||
item: item,
|
||||
articleContent: articleContent,
|
||||
isDark: UITraitCollection.current.userInterfaceStyle == .dark,
|
||||
fontSize: fontSize(),
|
||||
lineHeight: lineHeight(),
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ import WebKit
|
|||
Button(
|
||||
action: {
|
||||
dataService.archiveLink(objectID: item.objectID, archived: !item.isArchived)
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
Snackbar.show(message: !item.isArchived ? "Link archived" : "Link moved to Inbox")
|
||||
},
|
||||
label: {
|
||||
|
|
@ -122,6 +123,7 @@ import WebKit
|
|||
Button("Remove Link", role: .destructive) {
|
||||
Snackbar.show(message: "Link removed")
|
||||
dataService.removeLink(objectID: item.objectID)
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
Button("Cancel", role: .cancel, action: {})
|
||||
}
|
||||
|
|
@ -134,9 +136,8 @@ import WebKit
|
|||
ZStack {
|
||||
if let articleContent = viewModel.articleContent {
|
||||
WebReader(
|
||||
htmlContent: articleContent.htmlContent,
|
||||
highlightsJSONString: articleContent.highlightsJSONString,
|
||||
item: item,
|
||||
articleContent: articleContent,
|
||||
openLinkAction: {
|
||||
#if os(macOS)
|
||||
NSWorkspace.shared.open($0)
|
||||
|
|
|
|||
|
|
@ -7,16 +7,14 @@ struct WebReaderContent {
|
|||
let textFontSize: Int
|
||||
let lineHeight: Int
|
||||
let margin: Int
|
||||
let htmlContent: String
|
||||
let highlightsJSONString: String
|
||||
let item: LinkedItem
|
||||
let themeKey: String
|
||||
let fontFamily: WebFont
|
||||
let articleContent: ArticleContent
|
||||
|
||||
init(
|
||||
htmlContent: String,
|
||||
highlightsJSONString: String,
|
||||
item: LinkedItem,
|
||||
articleContent: ArticleContent,
|
||||
isDark: Bool,
|
||||
fontSize: Int,
|
||||
lineHeight: Int,
|
||||
|
|
@ -26,11 +24,10 @@ struct WebReaderContent {
|
|||
self.textFontSize = fontSize
|
||||
self.lineHeight = lineHeight
|
||||
self.margin = margin
|
||||
self.htmlContent = htmlContent
|
||||
self.highlightsJSONString = highlightsJSONString
|
||||
self.item = item
|
||||
self.themeKey = isDark ? "Gray" : "LightGray"
|
||||
self.fontFamily = fontFamily
|
||||
self.articleContent = articleContent
|
||||
}
|
||||
|
||||
// swiftlint:disable line_length
|
||||
|
|
@ -52,7 +49,7 @@ struct WebReaderContent {
|
|||
<body>
|
||||
<div id="root" />
|
||||
<div id='_omnivore-htmlContent' style="display: none;">
|
||||
\(htmlContent)
|
||||
\(articleContent.htmlContent)
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
window.omnivoreEnv = {
|
||||
|
|
@ -70,14 +67,14 @@ struct WebReaderContent {
|
|||
savedAt: \(savedAt),
|
||||
publishedAt: \(publishedAt),
|
||||
url: `\(item.unwrappedPageURLString)`,
|
||||
title: `\(item.unwrappedTitle.replacingOccurrences(of: "`", with: "\\`"))`,
|
||||
title: `\(articleContent.title.replacingOccurrences(of: "`", with: "\\`"))`,
|
||||
content: document.getElementById('_omnivore-htmlContent').innerHTML,
|
||||
originalArticleUrl: "\(item.unwrappedPageURLString)",
|
||||
contentReader: "WEB",
|
||||
readingProgressPercent: \(item.readingProgress),
|
||||
readingProgressAnchorIndex: \(item.readingProgressAnchor),
|
||||
labels: \(item.labelsJSONString),
|
||||
highlights: \(highlightsJSONString),
|
||||
highlights: \(articleContent.highlightsJSONString),
|
||||
}
|
||||
|
||||
window.fontSize = \(textFontSize)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import CoreData
|
||||
import Models
|
||||
import Services
|
||||
import SwiftUI
|
||||
|
|
@ -24,9 +25,32 @@ import Utils
|
|||
|
||||
guard let username = username else { return }
|
||||
|
||||
let existing = existingItemOrItemId(dataService: dataService, requestID: requestID)
|
||||
if let existingItem = existing.existingItem, existingItem.isReadyToRead {
|
||||
item = existingItem
|
||||
return
|
||||
}
|
||||
|
||||
// If the page was locally created, make sure they are synced before we pull content
|
||||
await dataService.syncUnsyncedArticleContent(itemID: requestID)
|
||||
await fetchLinkedItem(dataService: dataService, requestID: requestID, username: username)
|
||||
await dataService.syncUnsyncedArticleContent(itemID: existing.itemID)
|
||||
|
||||
// Fetch the item and it's content
|
||||
let item = await fetchLinkedItem(dataService: dataService, requestID: existing.itemID, username: username)
|
||||
if let item = item, let itemID = item.id {
|
||||
do {
|
||||
let articleContent = try await dataService.fetchArticleContent(itemID: itemID, username: username, requestCount: 0)
|
||||
// We've fetched the article content, now reload the item from core data
|
||||
if let linkedItem = dataService.viewContext.object(with: item.objectID) as? LinkedItem {
|
||||
self.item = linkedItem
|
||||
} else {
|
||||
self.item = nil
|
||||
}
|
||||
} catch {
|
||||
self.item = nil
|
||||
}
|
||||
} else {
|
||||
self.item = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchLinkedItem(
|
||||
|
|
@ -34,34 +58,67 @@ import Utils
|
|||
requestID: String,
|
||||
username: String,
|
||||
requestCount: Int = 1
|
||||
) async {
|
||||
) async -> LinkedItem? {
|
||||
guard requestCount < 7 else {
|
||||
errorMessage = "Unable to fetch item."
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
print("FETCHING", requestID, requestCount)
|
||||
|
||||
if let objectID = try? await dataService.fetchLinkedItem(username: username, itemID: requestID) {
|
||||
if let linkedItem = dataService.viewContext.object(with: objectID) as? LinkedItem {
|
||||
item = linkedItem
|
||||
print(" - FROM DATA SERVICE", linkedItem)
|
||||
return linkedItem
|
||||
} else {
|
||||
errorMessage = "Unable to fetch item."
|
||||
}
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
// Retry on error
|
||||
do {
|
||||
let retryDelayInNanoSeconds = UInt64(requestCount * 2 * 1_000_000_000)
|
||||
try await Task.sleep(nanoseconds: retryDelayInNanoSeconds)
|
||||
await fetchLinkedItem(
|
||||
|
||||
let existing = existingItemOrItemId(dataService: dataService, requestID: requestID)
|
||||
if let existingItem = existing.existingItem, existingItem.isReadyToRead {
|
||||
print(" - FROM CORE DATA SERVICE", existingItem)
|
||||
return existingItem
|
||||
}
|
||||
|
||||
let result = await fetchLinkedItem(
|
||||
dataService: dataService,
|
||||
requestID: requestID,
|
||||
requestID: existing.itemID,
|
||||
username: username,
|
||||
requestCount: requestCount + 1
|
||||
)
|
||||
if let result = result {
|
||||
return result
|
||||
}
|
||||
} catch {
|
||||
errorMessage = "Unable to fetch item."
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func existingItemOrItemId(dataService: DataService, requestID: String) -> (existingItem: LinkedItem?, itemID: String) {
|
||||
let fetchRequest: NSFetchRequest<Models.LinkedItem> = LinkedItem.fetchRequest()
|
||||
fetchRequest.predicate = NSPredicate(format: "createdId == %@ OR id == %@", requestID, requestID)
|
||||
if let existingItem = try? dataService.viewContext.fetch(fetchRequest).first {
|
||||
// If the existing item is synced, we can use it
|
||||
if let itemID = existingItem.id, existingItem.serverSyncStatus == ServerSyncStatus.isNSync.rawValue {
|
||||
item = existingItem
|
||||
return (existingItem: item, itemID: itemID)
|
||||
}
|
||||
|
||||
// If the existing item is not synced, we might have an updated request id
|
||||
if let existingID = existingItem.id {
|
||||
return (existingItem: nil, itemID: existingID)
|
||||
}
|
||||
}
|
||||
|
||||
return (existingItem: nil, itemID: requestID)
|
||||
}
|
||||
|
||||
func trackReadEvent() {
|
||||
|
|
@ -84,12 +141,20 @@ import Utils
|
|||
@StateObject var viewModel = WebReaderLoadingContainerViewModel()
|
||||
|
||||
public var body: some View {
|
||||
if let item = viewModel.item {
|
||||
WebReaderContainerView(item: item)
|
||||
.navigationBarHidden(true)
|
||||
.navigationViewStyle(.stack)
|
||||
.accentColor(.appGrayTextContrast)
|
||||
.task { viewModel.trackReadEvent() }
|
||||
if let item = viewModel.item, item.isReadyToRead {
|
||||
if let pdfItem = PDFItem.make(item: item), let urlStr = item.pageURLString, let remoteUrl = URL(string: urlStr) {
|
||||
PDFViewer(viewModel: PDFViewerViewModel(pdfItem: pdfItem))
|
||||
.navigationBarHidden(true)
|
||||
.navigationViewStyle(.stack)
|
||||
.accentColor(.appGrayTextContrast)
|
||||
.task { viewModel.trackReadEvent() }
|
||||
} else {
|
||||
WebReaderContainerView(item: item)
|
||||
.navigationBarHidden(true)
|
||||
.navigationViewStyle(.stack)
|
||||
.accentColor(.appGrayTextContrast)
|
||||
.task { viewModel.trackReadEvent() }
|
||||
}
|
||||
} else if let errorMessage = viewModel.errorMessage {
|
||||
Text(errorMessage)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -24,12 +24,13 @@
|
|||
<attribute name="author" optional="YES" attributeType="String"/>
|
||||
<attribute name="contentReader" optional="YES" attributeType="String"/>
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="createdId" optional="YES" attributeType="String"/>
|
||||
<attribute name="descriptionText" optional="YES" attributeType="String"/>
|
||||
<attribute name="htmlContent" optional="YES" attributeType="String"/>
|
||||
<attribute name="id" attributeType="String"/>
|
||||
<attribute name="imageURLString" optional="YES" attributeType="String"/>
|
||||
<attribute name="isArchived" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="localPdfURL" optional="YES" attributeType="String"/>
|
||||
<attribute name="localPDF" optional="YES" attributeType="String"/>
|
||||
<attribute name="onDeviceImageURLString" optional="YES" attributeType="String"/>
|
||||
<attribute name="originalHtml" optional="YES" attributeType="String"/>
|
||||
<attribute name="pageURLString" attributeType="String"/>
|
||||
|
|
@ -42,6 +43,7 @@
|
|||
<attribute name="serverSyncStatus" attributeType="Integer 64" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="siteName" optional="YES" attributeType="String"/>
|
||||
<attribute name="slug" attributeType="String"/>
|
||||
<attribute name="state" optional="YES" attributeType="String"/>
|
||||
<attribute name="title" attributeType="String"/>
|
||||
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="highlights" toMany="YES" deletionRule="Cascade" destinationEntity="Highlight" inverseName="linkedItem" inverseEntity="Highlight"/>
|
||||
|
|
@ -89,7 +91,7 @@
|
|||
</entity>
|
||||
<elements>
|
||||
<element name="Highlight" positionX="27" positionY="225" width="128" height="224"/>
|
||||
<element name="LinkedItem" positionX="-18" positionY="63" width="128" height="404"/>
|
||||
<element name="LinkedItem" positionX="-18" positionY="63" width="128" height="434"/>
|
||||
<element name="LinkedItemLabel" positionX="-36" positionY="18" width="128" height="134"/>
|
||||
<element name="NewsletterEmail" positionX="0" positionY="180" width="128" height="74"/>
|
||||
<element name="Viewer" positionX="45" positionY="234" width="128" height="89"/>
|
||||
|
|
|
|||
|
|
@ -8,15 +8,18 @@ public enum ArticleContentStatus {
|
|||
}
|
||||
|
||||
public struct ArticleContent {
|
||||
public let title: String
|
||||
public let htmlContent: String
|
||||
public let highlightsJSONString: String
|
||||
public let contentStatus: ArticleContentStatus
|
||||
|
||||
public init(
|
||||
title: String,
|
||||
htmlContent: String,
|
||||
highlightsJSONString: String,
|
||||
contentStatus: ArticleContentStatus
|
||||
) {
|
||||
self.title = title
|
||||
self.htmlContent = htmlContent
|
||||
self.highlightsJSONString = highlightsJSONString
|
||||
self.contentStatus = contentStatus
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import CoreData
|
||||
import Foundation
|
||||
import Utils
|
||||
|
||||
public struct HomeFeedData { // TODO: rename this
|
||||
public let items: [NSManagedObjectID]
|
||||
|
|
@ -43,6 +44,15 @@ public extension LinkedItem {
|
|||
readingProgress >= 0.98
|
||||
}
|
||||
|
||||
var isReadyToRead: Bool {
|
||||
if isPDF {
|
||||
// If its a PDF we verify the local file is available
|
||||
return PDFUtils.exists(filename: localPDF)
|
||||
}
|
||||
// Check the state and whether we have HTML
|
||||
return state == "SUCCEEDED"
|
||||
}
|
||||
|
||||
var isPDF: Bool {
|
||||
if let contentReader = contentReader {
|
||||
return contentReader == "PDF"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import CoreData
|
||||
import Foundation
|
||||
import Utils
|
||||
|
||||
public struct PDFItem {
|
||||
public let objectID: NSManagedObjectID
|
||||
public let itemID: String
|
||||
public let pdfURL: URL?
|
||||
public let localPdfURL: URL?
|
||||
public let localPDF: String?
|
||||
public let title: String
|
||||
public let slug: String
|
||||
public let readingProgress: Double
|
||||
|
|
@ -22,7 +23,7 @@ public struct PDFItem {
|
|||
objectID: item.objectID,
|
||||
itemID: item.unwrappedID,
|
||||
pdfURL: URL(string: item.unwrappedPageURLString),
|
||||
localPdfURL: item.localPdfURL.flatMap { URL(string: $0) },
|
||||
localPDF: item.localPDF,
|
||||
title: item.unwrappedID,
|
||||
slug: item.unwrappedSlug,
|
||||
readingProgress: item.readingProgress,
|
||||
|
|
@ -33,4 +34,11 @@ public struct PDFItem {
|
|||
highlights: item.highlights.asArray(of: Highlight.self)
|
||||
)
|
||||
}
|
||||
|
||||
public var localPdfURL: URL? {
|
||||
if let localPDF = localPDF {
|
||||
return PDFUtils.localPdfURL(filename: localPDF)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,9 +42,9 @@ public extension LinkedItemSort {
|
|||
var sortDescriptors: [NSSortDescriptor] {
|
||||
switch self {
|
||||
case .newest /* , .relevance */:
|
||||
return [NSSortDescriptor(keyPath: \LinkedItem.createdAt, ascending: false)]
|
||||
return [NSSortDescriptor(keyPath: \LinkedItem.savedAt, ascending: false)]
|
||||
case .oldest:
|
||||
return [NSSortDescriptor(keyPath: \LinkedItem.createdAt, ascending: true)]
|
||||
return [NSSortDescriptor(keyPath: \LinkedItem.savedAt, ascending: true)]
|
||||
// case .recentlyRead:
|
||||
// return [NSSortDescriptor(keyPath: \LinkedItem.updatedAt, ascending: false)]
|
||||
case .recentlyPublished:
|
||||
|
|
|
|||
|
|
@ -53,7 +53,6 @@ public enum PageScraper {
|
|||
|
||||
var pageScrapePayload: PageScrapePayload?
|
||||
let PDFKey = UTType.pdf.identifier
|
||||
let publicFileKey = UTType.fileURL.identifier
|
||||
let propertyListKey = UTType.propertyList.identifier
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
|
@ -257,7 +256,20 @@ private extension PageScrapePayload {
|
|||
let localFile = UUID().uuidString.lowercased() + ".pdf"
|
||||
dest.appendPathComponent(localFile)
|
||||
do {
|
||||
print("EXISTING PDF URL", url)
|
||||
let attr = try? FileManager.default.attributesOfItem(atPath: url.path)
|
||||
if let attr = attr {
|
||||
print("EXISTING FILE SIZE", attr[.size])
|
||||
}
|
||||
|
||||
try FileManager.default.copyItem(at: url, to: dest)
|
||||
print("COPIED TO URL", dest)
|
||||
|
||||
let attr2 = try? FileManager.default.attributesOfItem(atPath: dest.path)
|
||||
if let attr2 = attr2 {
|
||||
print("COPIED FILE SIZE", attr2[.size])
|
||||
}
|
||||
|
||||
return PageScrapePayload(url: url.absoluteString, localUrl: dest)
|
||||
} catch {
|
||||
print("error copying file locally", error)
|
||||
|
|
|
|||
|
|
@ -121,9 +121,11 @@ public final class DataService: ObservableObject {
|
|||
let existingItem = try? self.backgroundContext.fetch(fetchRequest).first
|
||||
let linkedItem = existingItem ?? LinkedItem(entity: LinkedItem.entity(), insertInto: self.backgroundContext)
|
||||
|
||||
linkedItem.createdId = requestId
|
||||
linkedItem.id = existingItem?.unwrappedID ?? requestId
|
||||
linkedItem.title = normalizedURL
|
||||
linkedItem.pageURLString = normalizedURL
|
||||
linkedItem.state = existingItem != nil ? existingItem?.state : "PROCESSING"
|
||||
linkedItem.serverSyncStatus = Int64(ServerSyncStatus.needsCreation.rawValue)
|
||||
linkedItem.savedAt = currentTime
|
||||
linkedItem.createdAt = currentTime
|
||||
|
|
@ -136,8 +138,8 @@ public final class DataService: ObservableObject {
|
|||
linkedItem.author = nil
|
||||
linkedItem.publishDate = nil
|
||||
|
||||
if let currentViewer = self.currentViewer {
|
||||
linkedItem.slug = "\(currentViewer)/\(requestId)"
|
||||
if let currentViewer = self.currentViewer, let username = currentViewer.username {
|
||||
linkedItem.slug = "\(username)/\(requestId)"
|
||||
} else {
|
||||
// Technically this is invalid, but I don't think slug is used at all locally anymore
|
||||
linkedItem.slug = requestId
|
||||
|
|
@ -146,18 +148,15 @@ public final class DataService: ObservableObject {
|
|||
switch pageScrape.contentType {
|
||||
case let .pdf(localUrl):
|
||||
linkedItem.contentReader = "PDF"
|
||||
linkedItem.localPdfURL = localUrl.absoluteString
|
||||
linkedItem.title = PDFUtils.titleFromPdfFile(pageScrape.url)
|
||||
// let thumbnailUrl = PDFUtils.thumbnailUrl(localUrl: localUrl)
|
||||
// linkedItem.imageURLString = await PDFUtils.createThumbnailFor(inputUrl: localUrl, at: thumbnailUrl)
|
||||
|
||||
print("PERSISTING PDF", localUrl)
|
||||
linkedItem.localPDF = try PDFUtils.copyToLocal(url: localUrl)
|
||||
case let .html(html: html, title: title, iconURL: iconURL):
|
||||
linkedItem.contentReader = "WEB"
|
||||
linkedItem.originalHtml = html
|
||||
linkedItem.imageURLString = iconURL
|
||||
linkedItem.title = title ?? PDFUtils.titleFromPdfFile(pageScrape.url)
|
||||
case .none:
|
||||
print("SAVING URL", linkedItem.unwrappedPageURLString)
|
||||
linkedItem.contentReader = "WEB"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ public extension DataService {
|
|||
let input = InputObjects.UploadFileRequestInput(
|
||||
clientRequestId: OptionalArgument(id),
|
||||
contentType: "application/pdf",
|
||||
createPageEntry: OptionalArgument(false),
|
||||
createPageEntry: OptionalArgument(true),
|
||||
url: url
|
||||
)
|
||||
|
||||
|
|
@ -83,6 +83,12 @@ public extension DataService {
|
|||
request.httpMethod = "PUT"
|
||||
request.addValue("application/pdf", forHTTPHeaderField: "content-type")
|
||||
|
||||
print("UPLOADING PDF", localPdfURL)
|
||||
let attr = try? FileManager.default.attributesOfItem(atPath: localPdfURL.path)
|
||||
if let attr = attr {
|
||||
print("UPLOADING ATTR", attr[.size])
|
||||
}
|
||||
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
let task = networker.urlSession.uploadTask(with: request, fromFile: localPdfURL) { _, response, _ in
|
||||
if let httpResponse = response as? HTTPURLResponse, 200 ... 299 ~= httpResponse.statusCode {
|
||||
|
|
@ -95,23 +101,7 @@ public extension DataService {
|
|||
}
|
||||
}
|
||||
|
||||
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 = session.uploadTask(with: request, fromFile: localUrl)
|
||||
return task
|
||||
} else {
|
||||
// TODO: How should we handle this scenario?
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable:next line_length
|
||||
func saveFilePublisher(requestId: String, uploadFileId: String, url: String) async throws {
|
||||
func saveFilePublisher(requestId: String, uploadFileId: String, url: String) async throws -> String? {
|
||||
enum MutationResult {
|
||||
case saved(requestId: String, url: String)
|
||||
case error(errorCode: Enums.SaveErrorCode)
|
||||
|
|
@ -127,7 +117,13 @@ public extension DataService {
|
|||
let selection = Selection<MutationResult, Unions.SaveResult> {
|
||||
try $0.on(
|
||||
saveError: .init { .error(errorCode: (try? $0.errorCodes().first) ?? .unknown) },
|
||||
saveSuccess: .init { .saved(requestId: requestId, url: (try? $0.url()) ?? "") }
|
||||
saveSuccess: .init {
|
||||
if let requestId = try? $0.clientRequestId(), let url = try? $0.url() {
|
||||
return .saved(requestId: requestId, url: url)
|
||||
} else {
|
||||
return .error(errorCode: .unknown)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -148,8 +144,8 @@ public extension DataService {
|
|||
}
|
||||
|
||||
switch payload.data {
|
||||
case .saved:
|
||||
continuation.resume()
|
||||
case let .saved(requestId: requestId, url: _):
|
||||
continuation.resume(returning: requestId)
|
||||
case let .error(errorCode: errorCode):
|
||||
switch errorCode {
|
||||
case .unauthorized:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import Models
|
|||
import SwiftGraphQL
|
||||
|
||||
public extension DataService {
|
||||
func savePage(id: String, url: String, title: String, originalHtml: String) async throws {
|
||||
func savePage(id: String, url: String, title: String, originalHtml: String) async throws -> String? {
|
||||
enum MutationResult {
|
||||
case saved(requestId: String, url: String)
|
||||
case error(errorCode: Enums.SaveErrorCode)
|
||||
|
|
@ -20,7 +20,13 @@ public extension DataService {
|
|||
let selection = Selection<MutationResult, Unions.SaveResult> {
|
||||
try $0.on(
|
||||
saveError: .init { .error(errorCode: (try? $0.errorCodes().first) ?? .unknown) },
|
||||
saveSuccess: .init { .saved(requestId: id, url: (try? $0.url()) ?? "") }
|
||||
saveSuccess: .init {
|
||||
if let requestId = try? $0.clientRequestId(), let url = try? $0.url() {
|
||||
return .saved(requestId: requestId, url: url)
|
||||
} else {
|
||||
return .error(errorCode: .unknown)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -42,8 +48,8 @@ public extension DataService {
|
|||
return
|
||||
}
|
||||
switch payload.data {
|
||||
case .saved:
|
||||
continuation.resume()
|
||||
case let .saved(requestId: requestId, url: _):
|
||||
continuation.resume(returning: requestId)
|
||||
case let .error(errorCode: errorCode):
|
||||
switch errorCode {
|
||||
case .unauthorized:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import Models
|
|||
import SwiftGraphQL
|
||||
|
||||
public extension DataService {
|
||||
func saveURL(id: String, url: String) async throws {
|
||||
func saveURL(id: String, url: String) async throws -> String? {
|
||||
enum MutationResult {
|
||||
case saved(requestId: String, url: String)
|
||||
case error(errorCode: Enums.SaveErrorCode)
|
||||
|
|
@ -18,7 +18,13 @@ public extension DataService {
|
|||
let selection = Selection<MutationResult, Unions.SaveResult> {
|
||||
try $0.on(
|
||||
saveError: .init { .error(errorCode: (try? $0.errorCodes().first) ?? .unknown) },
|
||||
saveSuccess: .init { .saved(requestId: id, url: (try? $0.url()) ?? "") }
|
||||
saveSuccess: .init {
|
||||
if let requestId = try? $0.clientRequestId(), let url = try? $0.url() {
|
||||
return .saved(requestId: requestId, url: url)
|
||||
} else {
|
||||
return .error(errorCode: .unknown)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -41,8 +47,8 @@ public extension DataService {
|
|||
}
|
||||
|
||||
switch payload.data {
|
||||
case .saved:
|
||||
continuation.resume()
|
||||
case let .saved(requestId: requestId, url: _):
|
||||
continuation.resume(returning: requestId)
|
||||
case let .error(errorCode: errorCode):
|
||||
switch errorCode {
|
||||
case .unauthorized:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import CoreData
|
||||
import Foundation
|
||||
import Models
|
||||
import Utils
|
||||
|
||||
public extension DataService {
|
||||
internal func syncOfflineItemsWithServerIfNeeded() async throws {
|
||||
|
|
@ -35,27 +36,36 @@ public extension DataService {
|
|||
}
|
||||
}
|
||||
|
||||
private func updateLinkedItemStatus(id: String, status: ServerSyncStatus) async throws {
|
||||
try backgroundContext.performAndWait {
|
||||
private func updateLinkedItemStatus(id: String, newId: String?, status: ServerSyncStatus) async throws {
|
||||
backgroundContext.performAndWait {
|
||||
let fetchRequest: NSFetchRequest<Models.LinkedItem> = LinkedItem.fetchRequest()
|
||||
fetchRequest.predicate = NSPredicate(format: "id == %@", id)
|
||||
|
||||
guard let linkedItem = (try? backgroundContext.fetch(fetchRequest))?.first else { return }
|
||||
if let newId = newId {
|
||||
linkedItem.id = newId
|
||||
}
|
||||
linkedItem.serverSyncStatus = Int64(status.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
func syncPdf(id: String, localPdfURL: URL, url: String) async throws {
|
||||
func createPageFromPdf(id: String, localPdfURL: URL, url: String) async throws {
|
||||
do {
|
||||
try await updateLinkedItemStatus(id: id, newId: nil, status: .isSyncing)
|
||||
|
||||
let uploadRequest = try await uploadFileRequest(id: id, url: url)
|
||||
if let urlString = uploadRequest.urlString, let uploadUrl = URL(string: urlString) {
|
||||
let attr = try? FileManager.default.attributesOfItem(atPath: localPdfURL.path)
|
||||
if let attr = attr {
|
||||
print("ATTR", attr[.size])
|
||||
}
|
||||
|
||||
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 await updateLinkedItemStatus(id: id, newId: nil, status: .isNSync)
|
||||
try backgroundContext.performAndWait {
|
||||
try backgroundContext.save()
|
||||
}
|
||||
|
|
@ -67,13 +77,16 @@ public extension DataService {
|
|||
}
|
||||
}
|
||||
|
||||
func syncPage(id: String, originalHtml: String, title: String?, url: String) async throws {
|
||||
func createPage(id: String, originalHtml: String, title: String?, url: String) async throws -> String {
|
||||
do {
|
||||
try await savePage(id: id, url: url, title: title ?? url, originalHtml: originalHtml)
|
||||
try await updateLinkedItemStatus(id: id, status: .isNSync)
|
||||
try await updateLinkedItemStatus(id: id, newId: nil, status: .isSyncing)
|
||||
|
||||
let newId = try await savePage(id: id, url: url, title: title ?? url, originalHtml: originalHtml)
|
||||
try await updateLinkedItemStatus(id: id, newId: newId, status: .isNSync)
|
||||
try backgroundContext.performAndWait {
|
||||
try backgroundContext.save()
|
||||
}
|
||||
return newId ?? id
|
||||
} catch {
|
||||
backgroundContext.performAndWait {
|
||||
backgroundContext.rollback()
|
||||
|
|
@ -82,13 +95,16 @@ public extension DataService {
|
|||
}
|
||||
}
|
||||
|
||||
func syncUrl(id: String, url: String) async throws {
|
||||
func createPageFromUrl(id: String, url: String) async throws -> String {
|
||||
do {
|
||||
try await updateLinkedItemStatus(id: id, status: .isSyncing)
|
||||
try await saveURL(id: id, url: url)
|
||||
try await updateLinkedItemStatus(id: id, newId: nil, status: .isSyncing)
|
||||
|
||||
let newId = try await saveURL(id: id, url: url)
|
||||
try await updateLinkedItemStatus(id: id, newId: newId, status: .isNSync)
|
||||
try backgroundContext.performAndWait {
|
||||
try backgroundContext.save()
|
||||
}
|
||||
return newId ?? id
|
||||
} catch {
|
||||
backgroundContext.performAndWait {
|
||||
backgroundContext.rollback()
|
||||
|
|
@ -101,12 +117,10 @@ public extension DataService {
|
|||
switch item.contentReader {
|
||||
case "PDF":
|
||||
let id = item.unwrappedID
|
||||
let localPdfURL = item.localPdfURL
|
||||
let url = item.unwrappedPageURLString
|
||||
|
||||
if let pdfUrlStr = localPdfURL, let localPdfURL = URL(string: pdfUrlStr) {
|
||||
if let localPDF = item.localPDF, let localPdfURL = PDFUtils.localPdfURL(filename: localPDF) {
|
||||
Task {
|
||||
try await syncPdf(id: id, localPdfURL: localPdfURL, url: url)
|
||||
try await createPageFromPdf(id: id, localPdfURL: localPdfURL, url: url)
|
||||
}
|
||||
} else {
|
||||
// TODO: This is an invalid object, we should have a way of reflecting that with an error state
|
||||
|
|
@ -120,9 +134,9 @@ public extension DataService {
|
|||
|
||||
Task {
|
||||
if let originalHtml = originalHtml {
|
||||
try await syncPage(id: id, originalHtml: originalHtml, title: title, url: url)
|
||||
try await createPage(id: id, originalHtml: originalHtml, title: title, url: url)
|
||||
} else {
|
||||
try await syncUrl(id: id, url: url)
|
||||
try await createPageFromUrl(id: id, url: url)
|
||||
}
|
||||
}
|
||||
default:
|
||||
|
|
@ -186,23 +200,4 @@ public 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<Models.LinkedItem> = 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,15 @@ import CoreData
|
|||
import Foundation
|
||||
import Models
|
||||
import SwiftGraphQL
|
||||
import Utils
|
||||
|
||||
extension DataService {
|
||||
struct PendingLink {
|
||||
public extension DataService {
|
||||
internal struct PendingLink {
|
||||
let itemID: String
|
||||
let retryCount: Int
|
||||
}
|
||||
|
||||
public func prefetchPages(itemIDs: [String], username: String) async {
|
||||
func prefetchPages(itemIDs: [String], username: String) async {
|
||||
// TODO: make this concurrent
|
||||
// TODO: make a non-pending page option for BG tasks
|
||||
for itemID in itemIDs {
|
||||
|
|
@ -17,7 +18,7 @@ extension DataService {
|
|||
}
|
||||
}
|
||||
|
||||
func prefetchPage(pendingLink: PendingLink, username: String) async {
|
||||
internal func prefetchPage(pendingLink: PendingLink, username: String) async {
|
||||
let content = try? await articleContent(username: username, itemID: pendingLink.itemID, useCache: false)
|
||||
|
||||
if content?.contentStatus == .processing, pendingLink.retryCount < 7 {
|
||||
|
|
@ -40,7 +41,7 @@ extension DataService {
|
|||
}
|
||||
}
|
||||
|
||||
public func fetchArticleContent(
|
||||
func fetchArticleContent(
|
||||
itemID: String,
|
||||
username: String? = nil,
|
||||
requestCount: Int = 1
|
||||
|
|
@ -69,7 +70,7 @@ extension DataService {
|
|||
}
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
public func articleContent(
|
||||
func articleContent(
|
||||
username: String,
|
||||
itemID: String,
|
||||
useCache: Bool
|
||||
|
|
@ -101,6 +102,7 @@ extension DataService {
|
|||
createdAt: try $0.createdAt().value ?? Date(),
|
||||
savedAt: try $0.savedAt().value ?? Date(),
|
||||
updatedAt: try $0.updatedAt().value ?? Date(),
|
||||
state: try $0.state()?.rawValue ?? "SUCCEEDED",
|
||||
readingProgress: try $0.readingProgressPercent(),
|
||||
readingProgressAnchor: try $0.readingProgressAnchorIndex(),
|
||||
imageURLString: try $0.image(),
|
||||
|
|
@ -142,8 +144,8 @@ extension DataService {
|
|||
let path = appEnvironment.graphqlPath
|
||||
let headers = networker.defaultHeaders
|
||||
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
send(query, to: path, headers: headers) { [weak self] queryResult in
|
||||
let result: ArticleProps = try await withCheckedThrowingContinuation { continuation in
|
||||
send(query, to: path, headers: headers) { queryResult in
|
||||
guard let payload = try? queryResult.get() else {
|
||||
continuation.resume(throwing: ContentFetchError.network)
|
||||
return
|
||||
|
|
@ -158,133 +160,134 @@ extension DataService {
|
|||
continuation.resume(throwing: ContentFetchError.badData)
|
||||
return
|
||||
}
|
||||
|
||||
if status == .succeeded || result.item.isPDF {
|
||||
do {
|
||||
try self?.persistArticleContent(
|
||||
item: result.item,
|
||||
htmlContent: result.htmlContent,
|
||||
highlights: result.highlights
|
||||
)
|
||||
} catch {
|
||||
var message = "unknown error"
|
||||
let basicError = (error as? BasicError) ?? BasicError.message(messageText: "unknown error")
|
||||
if case let BasicError.message(messageText) = basicError {
|
||||
message = messageText
|
||||
}
|
||||
continuation.resume(throwing: ContentFetchError.unknown(description: message))
|
||||
}
|
||||
}
|
||||
|
||||
let articleContent = ArticleContent(
|
||||
htmlContent: result.htmlContent,
|
||||
highlightsJSONString: result.highlights.asJSONString,
|
||||
contentStatus: result.item.isPDF ? .succeeded : .make(from: result.contentStatus)
|
||||
)
|
||||
|
||||
continuation.resume(returning: articleContent)
|
||||
continuation.resume(returning: result)
|
||||
case .error:
|
||||
continuation.resume(throwing: ContentFetchError.badData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let articleContent = ArticleContent(
|
||||
title: result.item.title,
|
||||
htmlContent: result.htmlContent,
|
||||
highlightsJSONString: result.highlights.asJSONString,
|
||||
contentStatus: result.item.isPDF ? .succeeded : .make(from: result.contentStatus)
|
||||
)
|
||||
|
||||
if result.contentStatus == .succeeded || result.item.isPDF {
|
||||
do {
|
||||
try await persistArticleContent(
|
||||
item: result.item,
|
||||
htmlContent: result.htmlContent,
|
||||
highlights: result.highlights
|
||||
)
|
||||
} catch {
|
||||
var message = "unknown error"
|
||||
let basicError = (error as? BasicError) ?? BasicError.message(messageText: "unknown error")
|
||||
if case let BasicError.message(messageText) = basicError {
|
||||
message = messageText
|
||||
}
|
||||
throw ContentFetchError.unknown(description: message)
|
||||
}
|
||||
}
|
||||
|
||||
return articleContent
|
||||
}
|
||||
|
||||
func persistArticleContent(item: InternalLinkedItem, htmlContent: String, highlights: [InternalHighlight]) throws {
|
||||
Task {
|
||||
try await backgroundContext.perform { [weak self] in
|
||||
guard let self = self else { return }
|
||||
let fetchRequest: NSFetchRequest<Models.LinkedItem> = LinkedItem.fetchRequest()
|
||||
fetchRequest.predicate = NSPredicate(format: "id == %@", item.id)
|
||||
internal func persistArticleContent(item: InternalLinkedItem, htmlContent: String, highlights: [InternalHighlight]) async throws {
|
||||
try await backgroundContext.perform { [weak self] in
|
||||
guard let self = self else { return }
|
||||
let fetchRequest: NSFetchRequest<Models.LinkedItem> = LinkedItem.fetchRequest()
|
||||
fetchRequest.predicate = NSPredicate(format: "id == %@", item.id)
|
||||
|
||||
let existingItem = try? self.backgroundContext.fetch(fetchRequest).first
|
||||
let linkedItem = existingItem ?? LinkedItem(entity: LinkedItem.entity(), insertInto: self.backgroundContext)
|
||||
let existingItem = try? self.backgroundContext.fetch(fetchRequest).first
|
||||
let linkedItem = existingItem ?? LinkedItem(entity: LinkedItem.entity(), insertInto: self.backgroundContext)
|
||||
|
||||
let highlightObjects = highlights.map {
|
||||
$0.asManagedObject(context: self.backgroundContext)
|
||||
}
|
||||
linkedItem.addToHighlights(NSSet(array: highlightObjects))
|
||||
linkedItem.htmlContent = htmlContent
|
||||
linkedItem.id = item.id
|
||||
linkedItem.title = item.title
|
||||
linkedItem.createdAt = item.createdAt
|
||||
linkedItem.savedAt = item.savedAt
|
||||
linkedItem.readingProgress = item.readingProgress
|
||||
linkedItem.readingProgressAnchor = Int64(item.readingProgressAnchor)
|
||||
linkedItem.imageURLString = item.imageURLString
|
||||
linkedItem.onDeviceImageURLString = item.onDeviceImageURLString
|
||||
linkedItem.pageURLString = item.pageURLString
|
||||
linkedItem.descriptionText = item.descriptionText
|
||||
linkedItem.publisherURLString = item.publisherURLString
|
||||
linkedItem.author = item.author
|
||||
linkedItem.publishDate = item.publishDate
|
||||
linkedItem.slug = item.slug
|
||||
linkedItem.isArchived = item.isArchived
|
||||
linkedItem.contentReader = item.contentReader
|
||||
let highlightObjects = highlights.map {
|
||||
$0.asManagedObject(context: self.backgroundContext)
|
||||
}
|
||||
linkedItem.addToHighlights(NSSet(array: highlightObjects))
|
||||
linkedItem.htmlContent = htmlContent
|
||||
linkedItem.id = item.id
|
||||
linkedItem.state = item.state
|
||||
linkedItem.title = item.title
|
||||
linkedItem.createdAt = item.createdAt
|
||||
linkedItem.savedAt = item.savedAt
|
||||
linkedItem.readingProgress = item.readingProgress
|
||||
linkedItem.readingProgressAnchor = Int64(item.readingProgressAnchor)
|
||||
linkedItem.imageURLString = item.imageURLString
|
||||
linkedItem.onDeviceImageURLString = item.onDeviceImageURLString
|
||||
linkedItem.pageURLString = item.pageURLString
|
||||
linkedItem.descriptionText = item.descriptionText
|
||||
linkedItem.publisherURLString = item.publisherURLString
|
||||
linkedItem.author = item.author
|
||||
linkedItem.publishDate = item.publishDate
|
||||
linkedItem.slug = item.slug
|
||||
linkedItem.isArchived = item.isArchived
|
||||
linkedItem.contentReader = item.contentReader
|
||||
linkedItem.serverSyncStatus = Int64(ServerSyncStatus.isNSync.rawValue)
|
||||
}
|
||||
|
||||
if linkedItem.isPDF, linkedItem.localPdfURL == nil {
|
||||
do {
|
||||
try self.fetchPDFData(slug: linkedItem.unwrappedSlug, pageURLString: linkedItem.unwrappedPageURLString)
|
||||
} catch {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
if item.isPDF {
|
||||
try await fetchPDFData(slug: item.slug, pageURLString: item.pageURLString)
|
||||
}
|
||||
|
||||
do {
|
||||
try self.backgroundContext.save()
|
||||
logger.debug("ArticleContent saved succesfully")
|
||||
} catch {
|
||||
self.backgroundContext.rollback()
|
||||
logger.debug("Failed to save ArticleContent")
|
||||
throw error
|
||||
}
|
||||
try await backgroundContext.perform { [weak self] in
|
||||
do {
|
||||
try self?.backgroundContext.save()
|
||||
logger.debug("ArticleContent saved succesfully")
|
||||
} catch {
|
||||
self?.backgroundContext.rollback()
|
||||
logger.debug("Failed to save ArticleContent")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchPDFData(slug: String, pageURLString: String) throws {
|
||||
Task {
|
||||
guard let url = URL(string: pageURLString) else { return }
|
||||
let result: (Data, URLResponse)? = try? await URLSession.shared.data(from: url)
|
||||
guard let httpResponse = result?.1 as? HTTPURLResponse, 200 ..< 300 ~= httpResponse.statusCode else {
|
||||
throw BasicError.message(messageText: "pdfFetch failed. no response or bad status code.")
|
||||
}
|
||||
guard let data = result?.0 else {
|
||||
throw BasicError.message(messageText: "pdfFetch failed. no data received.")
|
||||
func fetchPDFData(slug: String, pageURLString: String) async throws -> URL? {
|
||||
guard let url = URL(string: pageURLString) else {
|
||||
throw BasicError.message(messageText: "No PDF URL found")
|
||||
}
|
||||
let result: (Data, URLResponse)? = try? await URLSession.shared.data(from: url)
|
||||
guard let httpResponse = result?.1 as? HTTPURLResponse, 200 ..< 300 ~= httpResponse.statusCode else {
|
||||
throw BasicError.message(messageText: "pdfFetch failed. no response or bad status code.")
|
||||
}
|
||||
guard let data = result?.0 else {
|
||||
throw BasicError.message(messageText: "pdfFetch failed. no data received.")
|
||||
}
|
||||
|
||||
var localPdfURL: URL?
|
||||
let tempPath = FileManager.default
|
||||
.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
||||
.appendingPathComponent(UUID().uuidString + ".pdf")
|
||||
|
||||
try await backgroundContext.perform { [weak self] in
|
||||
let fetchRequest: NSFetchRequest<Models.LinkedItem> = LinkedItem.fetchRequest()
|
||||
fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(LinkedItem.slug), slug)
|
||||
|
||||
let linkedItem = try? self?.backgroundContext.fetch(fetchRequest).first
|
||||
guard let linkedItem = linkedItem else {
|
||||
let errorMessage = "pdfFetch failed. could not find LinkedItem from fetch request"
|
||||
throw BasicError.message(messageText: errorMessage)
|
||||
}
|
||||
|
||||
try await backgroundContext.perform { [weak self] in
|
||||
let fetchRequest: NSFetchRequest<Models.LinkedItem> = LinkedItem.fetchRequest()
|
||||
fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(LinkedItem.slug), slug)
|
||||
|
||||
let linkedItem = try? self?.backgroundContext.fetch(fetchRequest).first
|
||||
guard let linkedItem = linkedItem else {
|
||||
let errorMessage = "pdfFetch failed. could not find LinkedItem from fetch request"
|
||||
throw BasicError.message(messageText: errorMessage)
|
||||
}
|
||||
|
||||
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 {
|
||||
self?.backgroundContext.rollback()
|
||||
logger.debug("PDF data saved succesfully")
|
||||
let errorMessage = "pdfFetch failed. core data save failed."
|
||||
throw BasicError.message(messageText: errorMessage)
|
||||
}
|
||||
do {
|
||||
try data.write(to: tempPath)
|
||||
let localPDF = try PDFUtils.moveToLocal(url: tempPath)
|
||||
localPdfURL = PDFUtils.localPdfURL(filename: localPDF)
|
||||
linkedItem.localPDF = localPDF
|
||||
try self?.backgroundContext.save()
|
||||
} catch {
|
||||
self?.backgroundContext.rollback()
|
||||
let errorMessage = "pdfFetch failed. core data save failed."
|
||||
throw BasicError.message(messageText: errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
return localPdfURL
|
||||
}
|
||||
|
||||
func cachedArticleContent(itemID: String) async -> ArticleContent? {
|
||||
internal func cachedArticleContent(itemID: String) async -> ArticleContent? {
|
||||
let linkedItemFetchRequest: NSFetchRequest<Models.LinkedItem> = LinkedItem.fetchRequest()
|
||||
linkedItemFetchRequest.predicate = NSPredicate(
|
||||
format: "id == %@", itemID
|
||||
|
|
@ -302,6 +305,7 @@ extension DataService {
|
|||
.filter { $0.serverSyncStatus != ServerSyncStatus.needsDeletion.rawValue }
|
||||
|
||||
return ArticleContent(
|
||||
title: linkedItem.unwrappedTitle,
|
||||
htmlContent: htmlContent,
|
||||
highlightsJSONString: highlights.map { InternalHighlight.make(from: $0) }.asJSONString,
|
||||
contentStatus: .succeeded
|
||||
|
|
@ -309,7 +313,7 @@ extension DataService {
|
|||
}
|
||||
}
|
||||
|
||||
public func syncUnsyncedArticleContent(itemID: String) async {
|
||||
func syncUnsyncedArticleContent(itemID: String) async {
|
||||
let linkedItemFetchRequest: NSFetchRequest<Models.LinkedItem> = LinkedItem.fetchRequest()
|
||||
linkedItemFetchRequest.predicate = NSPredicate(
|
||||
format: "id == %@", itemID
|
||||
|
|
@ -334,7 +338,7 @@ extension DataService {
|
|||
|
||||
if let id = id, let url = url, let title = title,
|
||||
let serverSyncStatus = serverSyncStatus,
|
||||
serverSyncStatus != ServerSyncStatus.isNSync.rawValue
|
||||
serverSyncStatus == ServerSyncStatus.needsCreation.rawValue
|
||||
{
|
||||
do {
|
||||
if let originalHtml = originalHtml {
|
||||
|
|
@ -342,12 +346,10 @@ extension DataService {
|
|||
} 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.
|
||||
print("Error syncUnsyncedArticleContent")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ public extension DataService {
|
|||
createdAt: try $0.createdAt().value ?? Date(),
|
||||
savedAt: try $0.savedAt().value ?? Date(),
|
||||
updatedAt: try $0.updatedAt().value ?? Date(),
|
||||
state: try $0.state()?.rawValue ?? "SUCCEEDED",
|
||||
readingProgress: try $0.readingProgressPercent(),
|
||||
readingProgressAnchor: try $0.readingProgressAnchorIndex(),
|
||||
imageURLString: try $0.image(),
|
||||
|
|
@ -164,6 +165,7 @@ private let libraryArticleSelection = Selection.Article {
|
|||
createdAt: try $0.createdAt().value ?? Date(),
|
||||
savedAt: try $0.savedAt().value ?? Date(),
|
||||
updatedAt: try $0.updatedAt().value ?? Date(),
|
||||
state: try $0.state()?.rawValue ?? "SUCCEEDED",
|
||||
readingProgress: try $0.readingProgressPercent(),
|
||||
readingProgressAnchor: try $0.readingProgressAnchorIndex(),
|
||||
imageURLString: try $0.image(),
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ struct InternalLinkedItem {
|
|||
let createdAt: Date
|
||||
let savedAt: Date
|
||||
let updatedAt: Date
|
||||
let state: String
|
||||
var readingProgress: Double
|
||||
var readingProgressAnchor: Int
|
||||
let imageURLString: String?
|
||||
|
|
@ -41,6 +42,7 @@ struct InternalLinkedItem {
|
|||
linkedItem.createdAt = createdAt
|
||||
linkedItem.savedAt = savedAt
|
||||
linkedItem.updatedAt = updatedAt
|
||||
linkedItem.state = state
|
||||
linkedItem.readingProgress = readingProgress
|
||||
linkedItem.readingProgressAnchor = Int64(readingProgressAnchor)
|
||||
linkedItem.imageURLString = imageURLString
|
||||
|
|
@ -98,6 +100,7 @@ extension JSONArticle {
|
|||
createdAt: createdAt,
|
||||
savedAt: savedAt,
|
||||
updatedAt: updatedAt,
|
||||
state: "SUCCEEDED",
|
||||
readingProgress: readingProgressPercent,
|
||||
readingProgressAnchor: readingProgressAnchorIndex,
|
||||
imageURLString: image,
|
||||
|
|
|
|||
|
|
@ -26,9 +26,13 @@ public func normalizeURL(_ dirtyURL: String) -> String {
|
|||
}
|
||||
|
||||
urlObject.queryItems = urlObject.queryItems?.filter { item in
|
||||
item.name.starts(with: "utm_")
|
||||
!item.name.starts(with: "utm_")
|
||||
}
|
||||
|
||||
urlObject.queryItems = urlObject.queryItems?.sorted(by: { first, second in
|
||||
first.name <= second.name
|
||||
})
|
||||
|
||||
if /* options.removeTrailingSlash */ true {
|
||||
urlObject.path = urlObject.path.replacingRegex(pattern: "/$", replaceWith: "")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,40 @@ import QuickLookThumbnailing
|
|||
import UIKit
|
||||
|
||||
public enum PDFUtils {
|
||||
public static func copyToLocal(url: URL) throws -> String {
|
||||
let subPath = UUID().uuidString + ".pdf"
|
||||
let dest = FileManager.default
|
||||
.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
.appendingPathComponent(subPath)
|
||||
|
||||
try FileManager.default.copyItem(at: url, to: dest)
|
||||
return subPath
|
||||
}
|
||||
|
||||
public static func moveToLocal(url: URL) throws -> String {
|
||||
let subPath = UUID().uuidString + ".pdf"
|
||||
let dest = FileManager.default
|
||||
.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
.appendingPathComponent(subPath)
|
||||
|
||||
try FileManager.default.moveItem(at: url, to: dest)
|
||||
return subPath
|
||||
}
|
||||
|
||||
public static func localPdfURL(filename: String) -> URL? {
|
||||
let url = FileManager.default
|
||||
.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
.appendingPathComponent(filename)
|
||||
return url
|
||||
}
|
||||
|
||||
public static func exists(filename: String?) -> Bool {
|
||||
if let filename = filename, let localPdfURL = localPdfURL(filename: filename) {
|
||||
return FileManager.default.fileExists(atPath: localPdfURL.absoluteString)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public static func titleFromPdfFile(_ urlStr: String) -> String {
|
||||
let url = URL(string: urlStr)
|
||||
if let url = url {
|
||||
|
|
|
|||
|
|
@ -161,6 +161,10 @@ public struct GridCard: View {
|
|||
}
|
||||
.onTapGesture { tapHandler() }
|
||||
}
|
||||
|
||||
if let status = item.serverSyncStatus, status != ServerSyncStatus.isNSync.rawValue {
|
||||
SyncStatusIcon(status: ServerSyncStatus(rawValue: Int(status)) ?? ServerSyncStatus.isNSync)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 0)
|
||||
.padding(.top, 0)
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ public struct FeedCard: View {
|
|||
}
|
||||
}
|
||||
|
||||
if item.sortedLabels.count > 0 {
|
||||
if item.hasLabels {
|
||||
// Category Labels
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,11 @@ public class ShareExtensionChildViewModel: ObservableObject {
|
|||
@Published public var title: String?
|
||||
@Published public var url: String?
|
||||
@Published public var iconURL: String?
|
||||
@Published public var requestId: String
|
||||
|
||||
public init() {}
|
||||
public init() {
|
||||
self.requestId = UUID().uuidString.lowercased()
|
||||
}
|
||||
}
|
||||
|
||||
public enum ShareExtensionStatus {
|
||||
|
|
@ -126,7 +129,7 @@ struct CheckmarkButtonView: View {
|
|||
public struct ShareExtensionChildView: View {
|
||||
let viewModel: ShareExtensionChildViewModel
|
||||
let onAppearAction: () -> Void
|
||||
let readNowButtonAction: () -> Void
|
||||
let readNowButtonAction: (String) -> Void
|
||||
let dismissButtonTappedAction: (ReminderTime?, Bool) -> Void
|
||||
|
||||
@State var reminderTime: ReminderTime?
|
||||
|
|
@ -135,7 +138,7 @@ public struct ShareExtensionChildView: View {
|
|||
public init(
|
||||
viewModel: ShareExtensionChildViewModel,
|
||||
onAppearAction: @escaping () -> Void,
|
||||
readNowButtonAction: @escaping () -> Void,
|
||||
readNowButtonAction: @escaping (String) -> Void,
|
||||
dismissButtonTappedAction: @escaping (ReminderTime?, Bool) -> Void
|
||||
) {
|
||||
self.viewModel = viewModel
|
||||
|
|
@ -178,8 +181,10 @@ public struct ShareExtensionChildView: View {
|
|||
|
||||
private var cloudIconColor: Color {
|
||||
switch viewModel.status {
|
||||
case .saved, .processing:
|
||||
case .saved:
|
||||
return .appGrayText
|
||||
case .processing:
|
||||
return .clear
|
||||
case .failed(error: _), .syncFailed(error: _):
|
||||
return .red
|
||||
case .synced:
|
||||
|
|
@ -187,13 +192,9 @@ public struct ShareExtensionChildView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func localImage(from: URL) -> Image? {
|
||||
do {
|
||||
if let data = try? Data(contentsOf: from), let img = UIImage(data: data) {
|
||||
return Image(uiImage: img)
|
||||
}
|
||||
} catch {
|
||||
return nil
|
||||
private func localImage(from url: URL) -> Image? {
|
||||
if let data = try? Data(contentsOf: url), let img = UIImage(data: data) {
|
||||
return Image(uiImage: img)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -283,13 +284,12 @@ public struct ShareExtensionChildView: View {
|
|||
Spacer()
|
||||
|
||||
HStack {
|
||||
if FeatureFlag.enableReadNow {
|
||||
Button(
|
||||
action: { readNowButtonAction() },
|
||||
label: { Text("Read Now").frame(maxWidth: .infinity) }
|
||||
)
|
||||
.buttonStyle(RoundedRectButtonStyle())
|
||||
}
|
||||
Button(
|
||||
action: { readNowButtonAction(self.viewModel.requestId) },
|
||||
label: { Text("Read Now").frame(maxWidth: .infinity) }
|
||||
)
|
||||
.buttonStyle(RoundedRectButtonStyle())
|
||||
|
||||
Button(
|
||||
action: {
|
||||
dismissButtonTappedAction(reminderTime, hideUntilReminded)
|
||||
|
|
|
|||
50
apple/OmnivoreKit/Sources/Views/SyncingIcon.swift
Normal file
50
apple/OmnivoreKit/Sources/Views/SyncingIcon.swift
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Jackson Harper on 6/5/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Models
|
||||
import SwiftUI
|
||||
import Utils
|
||||
|
||||
public struct SyncStatusIcon: View {
|
||||
let status: ServerSyncStatus
|
||||
|
||||
init(status: ServerSyncStatus) {
|
||||
self.status = status
|
||||
}
|
||||
|
||||
private var cloudIconName: String {
|
||||
switch status {
|
||||
// case .isNSync:
|
||||
// return "checkmark.icloud"
|
||||
case .isNSync:
|
||||
return "exclamationmark.icloud"
|
||||
case .isSyncing, .needsCreation, .needsDeletion, .needsUpdate:
|
||||
return "icloud"
|
||||
}
|
||||
}
|
||||
|
||||
private var cloudIconColor: Color {
|
||||
switch status {
|
||||
// case .isNSync:
|
||||
// return .blue
|
||||
case .isNSync:
|
||||
return .red
|
||||
case .isSyncing, .needsCreation, .needsDeletion, .needsUpdate:
|
||||
return .appGrayText
|
||||
}
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
Image(systemName: cloudIconName)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 12, height: 12, alignment: .trailing)
|
||||
.foregroundColor(cloudIconColor)
|
||||
.padding(EdgeInsets(top: 0, leading: 0, bottom: 8, trailing: 8))
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,20 @@ final class UtilsTests: XCTestCase {
|
|||
XCTAssertEqual("Hello", "Hello")
|
||||
}
|
||||
|
||||
func testNormalizeUrl() {
|
||||
// trailing slash removed
|
||||
XCTAssertEqual(normalizeURL("https://omnivore.app/"), "https://omnivore.app")
|
||||
|
||||
// utm_ removed
|
||||
XCTAssertEqual(normalizeURL("https://omnivore.app/?aa=a&bb=b&utm_track=track&cc=c"), "https://omnivore.app?aa=a&bb=b&cc=c")
|
||||
|
||||
// query params sorted
|
||||
XCTAssertEqual(normalizeURL("https://omnivore.app/?aa=a&cc=c&bb=b"), "https://omnivore.app?aa=a&bb=b&cc=c")
|
||||
XCTAssertEqual(normalizeURL("https://omnivore.app/?cc=c&bb=b&aa=a"), "https://omnivore.app?aa=a&bb=b&cc=c")
|
||||
}
|
||||
|
||||
static var allTests = [
|
||||
("testExample", testExample)
|
||||
("testExample", testExample),
|
||||
("testNormalizeUrl", testNormalizeUrl)
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ export const createPage = async (
|
|||
|
||||
return body._id as string
|
||||
} catch (e) {
|
||||
console.error('failed to create a page in elastic', e)
|
||||
console.error('failed to create a page in elastic', JSON.stringify(e))
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import { PubsubClient } from '../datalayer/pubsub'
|
||||
import { homePageURL } from '../env'
|
||||
import { Maybe, SavePageInput, SaveResult } from '../generated/graphql'
|
||||
import {
|
||||
Maybe,
|
||||
SaveErrorCode,
|
||||
SavePageInput,
|
||||
SaveResult,
|
||||
} from '../generated/graphql'
|
||||
import { DataModels } from '../resolvers/types'
|
||||
import { generateSlug, stringToHash, validatedDate } from '../utils/helpers'
|
||||
import { parsePreparedContent } from '../utils/parser'
|
||||
|
|
@ -102,25 +107,44 @@ export const savePage = async (
|
|||
state: ArticleSavingRequestStatus.Succeeded,
|
||||
})
|
||||
if (existingPage) {
|
||||
await updatePage(
|
||||
existingPage.id,
|
||||
{
|
||||
savedAt: new Date(),
|
||||
archivedAt: undefined,
|
||||
},
|
||||
ctx
|
||||
)
|
||||
if (
|
||||
!(await updatePage(
|
||||
existingPage.id,
|
||||
{
|
||||
savedAt: new Date(),
|
||||
archivedAt: undefined,
|
||||
},
|
||||
ctx
|
||||
))
|
||||
) {
|
||||
return {
|
||||
errorCodes: [SaveErrorCode.Unknown],
|
||||
message: 'Failed to update existing page',
|
||||
}
|
||||
}
|
||||
input.clientRequestId = existingPage.id
|
||||
} else if (shouldParseInBackend(input)) {
|
||||
await createPageSaveRequest(
|
||||
saver.userId,
|
||||
input.url,
|
||||
ctx.models,
|
||||
ctx.pubsub,
|
||||
input.clientRequestId
|
||||
)
|
||||
try {
|
||||
await createPageSaveRequest(
|
||||
saver.userId,
|
||||
input.url,
|
||||
ctx.models,
|
||||
ctx.pubsub,
|
||||
input.clientRequestId
|
||||
)
|
||||
} catch (e) {
|
||||
return {
|
||||
errorCodes: [SaveErrorCode.Unknown],
|
||||
message: 'Failed to create page save request',
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await createPage(articleToSave, ctx)
|
||||
if (!(await createPage(articleToSave, ctx))) {
|
||||
return {
|
||||
errorCodes: [SaveErrorCode.Unknown],
|
||||
message: 'Failed to create new page',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -267,15 +267,6 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
|||
))}
|
||||
</SpanBox>
|
||||
) : null}
|
||||
{props.isAppleAppEmbed && (
|
||||
<ArticleHeaderToolbar
|
||||
articleTitle={props.article.title}
|
||||
articleShareURL={props.highlightsBaseURL}
|
||||
setShowShareArticleModal={setShowShareModal}
|
||||
setShowHighlightsModal={props.setShowHighlightsModal}
|
||||
hasHighlights={props.article.highlights?.length > 0}
|
||||
/>
|
||||
)}
|
||||
</VStack>
|
||||
<Article
|
||||
highlightReady={highlightReady}
|
||||
|
|
|
|||
Loading…
Reference in a new issue