Merge pull request #712 from omnivore-app/fix/ios-pdf-handling

Better PDF upload handling on iOS
This commit is contained in:
Jackson Harper 2022-05-27 08:43:07 -07:00 committed by GitHub
commit 987336f438
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 17013 additions and 16839 deletions

View file

@ -59,11 +59,17 @@ final class ShareExtensionViewModel: ObservableObject {
return
}
let backgroundTask = UIApplication.shared.beginBackgroundTask(withName: requestId)
let saveLinkPublisher: AnyPublisher<Void, SaveArticleError> = {
if pageScrapePayload.contentType == .pdf {
return services.dataService.uploadPDFPublisher(pageScrapePayload: pageScrapePayload, requestId: requestId)
} else if pageScrapePayload.html != nil {
return services.dataService.savePagePublisher(pageScrapePayload: pageScrapePayload, requestId: requestId)
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)
}
@ -74,8 +80,10 @@ 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)
} receiveValue: { [weak self] _ in
self?.status = .success
UIApplication.shared.endBackgroundTask(backgroundTask)
}
.store(in: &subscriptions)

View file

@ -9,28 +9,34 @@ 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 enum ContentType {
case html
case pdf
public struct HTMLPayload {
let url: String
let title: String?
let html: String
}
public enum ContentType {
case none
case html(html: String, title: String?)
case pdf(data: Data)
}
public let title: String?
public let html: String?
public let url: String
public let contentType: ContentType
init(url: String, title: String?, html: String?, contentType: String?) {
init(url: String) {
self.url = url
self.title = title
self.html = html
self.contentType = .none
}
// If the content type was specified and we know its PDF, use that
// otherwise fallback to using file extensions.
if let contentType = contentType, contentType.contains("pdf") {
self.contentType = .pdf
} else {
self.contentType = url.hasSuffix(".pdf") ? .pdf : .html
}
init(url: String, pdfData: Data) {
self.url = url
self.contentType = .pdf(data: pdfData)
}
init(url: String, title: String?, html: String) {
self.url = url
self.contentType = .html(html: html, title: title)
}
}
@ -52,10 +58,25 @@ public enum PageScraper {
}
var pageScrapePayload: PageScrapePayload?
let PDFKey = UTType.pdf.identifier
let publicFileKey = UTType.fileURL.identifier
let propertyListKey = UTType.propertyList.identifier
let group = DispatchGroup()
for attachment in attachments where attachment.hasItemConformingToTypeIdentifier(PDFKey) {
group.enter()
attachment.loadItem(
forTypeIdentifier: PDFKey,
options: nil
) { item, _ in
if let payload = PageScrapePayload.make(item: item) {
pageScrapePayload = payload
}
group.leave()
}
}
for attachment in attachments where attachment.hasItemConformingToTypeIdentifier(propertyListKey) {
group.enter()
attachment.loadItem(
@ -214,17 +235,55 @@ public enum PageScraper {
private extension PageScrapePayload {
static func make(url: URL?) -> PageScrapePayload? {
guard let url = url else { return nil }
return PageScrapePayload(url: url.absoluteString, title: nil, html: nil, contentType: nil)
return PageScrapePayload(url: url.absoluteString)
}
static func make(item: NSSecureCoding?) -> PageScrapePayload? {
let dictionary = item as? NSDictionary
let results = dictionary?[NSExtensionJavaScriptPreprocessingResultsKey] as? NSDictionary
if let dictionary = item as? NSDictionary {
return makeFromDictionary(dictionary)
}
if let url = item as? NSURL {
return makeFromURL(url as URL)
}
return nil
}
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)
}
// 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
}
return PageScrapePayload(url: url.absoluteString)
}
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 title = results?["title"] as? String
let contentType = results?["contentType"] as? String
return PageScrapePayload(url: url, title: title, html: html, contentType: contentType)
// If we were not able to capture any HTML, treat this as a URL and
// see if the backend can do better.
if html == nil || html!.isEmpty {
return PageScrapePayload(url: url)
}
// If its a PDF that we opened through Safari we don't have access to the
// file content, so pass the URL to the backend and let it download it.
if contentType == "application/pdf" {
return PageScrapePayload(url: url)
}
if let html = html {
return PageScrapePayload(url: url, title: title, html: html)
}
return PageScrapePayload(url: url)
}
}

File diff suppressed because it is too large Load diff

View file

@ -24,16 +24,16 @@ extension DataService {
let selection = Selection<MutationResult, Unions.ArchiveLinkResult> {
try $0.on(
archiveLinkError: .init { .error(errorCode: try $0.errorCodes().first ?? .badRequest) },
archiveLinkSuccess: .init { .success(linkId: try $0.linkId()) }
archiveLinkSuccess: .init { .success(linkId: try $0.linkId()) },
archiveLinkError: .init { .error(errorCode: try $0.errorCodes().first ?? .badRequest) }
)
}
let mutation = Selection.Mutation {
try $0.setLinkArchived(
input: InputObjects.ArchiveLinkInput(
archived: archived,
linkId: itemID
linkId: itemID,
archived: archived
),
selection: selection
)

View file

@ -40,22 +40,22 @@ extension DataService {
let selection = Selection<MutationResult, Unions.CreateHighlightResult> {
try $0.on(
createHighlightError: .init { .error(errorCode: try $0.errorCodes().first ?? .badData) },
createHighlightSuccess: .init {
.saved(highlight: try $0.highlight(selection: highlightSelection))
}
},
createHighlightError: .init { .error(errorCode: try $0.errorCodes().first ?? .badData) }
)
}
let mutation = Selection.Mutation {
try $0.createHighlight(
input: InputObjects.CreateHighlightInput(
annotation: OptionalArgument(highlight.annotation),
articleId: articleId,
id: highlight.id,
shortId: highlight.shortId,
articleId: articleId,
patch: highlight.patch,
quote: highlight.quote,
shortId: highlight.shortId
annotation: OptionalArgument(highlight.annotation)
),
selection: selection
)

View file

@ -34,17 +34,17 @@ extension DataService {
let selection = Selection<MutationResult, Unions.CreateLabelResult> {
try $0.on(
createLabelError: .init { .error(errorCode: try $0.errorCodes().first ?? .badRequest) },
createLabelSuccess: .init { .saved(label: try $0.label(selection: feedItemLabelSelection)) }
createLabelSuccess: .init { .saved(label: try $0.label(selection: feedItemLabelSelection)) },
createLabelError: .init { .error(errorCode: try $0.errorCodes().first ?? .badRequest) }
)
}
let mutation = Selection.Mutation {
try $0.createLabel(
input: InputObjects.CreateLabelInput(
name: label.name,
color: label.color,
description: OptionalArgument(label.labelDescription),
name: label.name
description: OptionalArgument(label.labelDescription)
),
selection: selection
)

View file

@ -12,9 +12,6 @@ public extension DataService {
let selection = Selection<MutationResult, Unions.CreateNewsletterEmailResult> {
try $0.on(
createNewsletterEmailError: .init {
.error(errorCode: try $0.errorCodes().first ?? .badRequest)
},
createNewsletterEmailSuccess: .init {
.saved(newsletterEmail: try $0.newsletterEmail(selection: Selection.NewsletterEmail {
InternalNewsletterEmail(
@ -23,6 +20,9 @@ public extension DataService {
confirmationCode: try $0.confirmationCode()
)
}))
},
createNewsletterEmailError: .init {
.error(errorCode: try $0.errorCodes().first ?? .badRequest)
}
)
}

View file

@ -37,21 +37,21 @@ public extension DataService {
let selection = Selection<MutationResult, Unions.CreateReminderResult> {
try $0.on(
createReminderError: .init { .error(errorCode: try $0.errorCodes().first ?? .badRequest) },
createReminderSuccess: .init {
.complete(id: try $0.reminder(selection: Selection.Reminder { try $0.id() }))
}
},
createReminderError: .init { .error(errorCode: try $0.errorCodes().first ?? .badRequest) }
)
}
let mutation = Selection.Mutation {
try $0.createReminder(
input: InputObjects.CreateReminderInput(
archiveUntil: true,
clientRequestId: OptionalArgument(reminderItemId.clientRequestId),
linkId: OptionalArgument(reminderItemId.linkId),
remindAt: DateTime(from: remindAt),
sendNotification: true
clientRequestId: OptionalArgument(reminderItemId.clientRequestId),
archiveUntil: true,
sendNotification: true,
remindAt: DateTime(from: remindAt)
),
selection: selection
)

View file

@ -30,10 +30,10 @@ public extension DataService {
let selection = Selection<MutationResult, Unions.DeleteHighlightResult> {
try $0.on(
deleteHighlightError: .init { .error(errorCode: try $0.errorCodes().first ?? .unauthorized) },
deleteHighlightSuccess: .init {
.saved(id: try $0.highlight(selection: Selection.Highlight { try $0.id() }))
}
},
deleteHighlightError: .init { .error(errorCode: try $0.errorCodes().first ?? .unauthorized) }
)
}

View file

@ -12,8 +12,8 @@ public extension DataService {
let selection = Selection<MutationResult, Unions.UnsubscribeResult> {
try $0.on(
unsubscribeError: .init { .error(errorMessage: (try $0.errorCodes().first ?? .unauthorized).rawValue) },
unsubscribeSuccess: .init { .success(id: try $0.subscription(selection: Selection.Subscription { try $0.id() })) }
unsubscribeSuccess: .init { .success(id: try $0.subscription(selection: Selection.Subscription { try $0.id() })) },
unsubscribeError: .init { .error(errorMessage: (try $0.errorCodes().first ?? .unauthorized).rawValue) }
)
}

View file

@ -34,10 +34,10 @@ public extension DataService {
let selection = Selection<MutationResult, Unions.SetDeviceTokenResult> {
try $0.on(
setDeviceTokenError: .init { .error(errorCode: try $0.errorCodes().first ?? .badRequest) },
setDeviceTokenSuccess: .init {
.saved(id: try $0.deviceToken(selection: Selection.DeviceToken { try $0.id() }))
}
},
setDeviceTokenError: .init { .error(errorCode: try $0.errorCodes().first ?? .badRequest) }
)
}

View file

@ -46,25 +46,25 @@ extension DataService {
let selection = Selection<MutationResult, Unions.MergeHighlightResult> {
try $0.on(
mergeHighlightError: .init { .error(errorCode: try $0.errorCodes().first ?? .badData) },
mergeHighlightSuccess: .init {
.saved(highlight: try $0.highlight(selection: highlightSelection))
}
},
mergeHighlightError: .init { .error(errorCode: try $0.errorCodes().first ?? .badData) }
)
}
let mutation = Selection.Mutation {
try $0.mergeHighlight(
input: InputObjects.MergeHighlightInput(
annotation: .absent(),
articleId: articleId,
id: highlight.id,
overlapHighlightIdList: overlapHighlightIdList,
patch: highlight.patch,
prefix: .absent(),
quote: highlight.quote,
shortId: highlight.shortId,
suffix: .absent()
articleId: articleId,
patch: highlight.patch,
quote: highlight.quote,
prefix: .absent(),
suffix: .absent(),
annotation: .absent(),
overlapHighlightIdList: overlapHighlightIdList
),
selection: selection
)

View file

@ -23,10 +23,10 @@ extension DataService {
let selection = Selection<MutationResult, Unions.DeleteLabelResult> {
try $0.on(
deleteLabelError: .init { .error(errorCode: try $0.errorCodes().first ?? .badRequest) },
deleteLabelSuccess: .init {
.success(labelID: try $0.label(selection: Selection.Label { try $0.id() }))
}
},
deleteLabelError: .init { .error(errorCode: try $0.errorCodes().first ?? .badRequest) }
)
}

View file

@ -24,14 +24,14 @@ extension DataService {
let selection = Selection<MutationResult, Unions.SetBookmarkArticleResult> {
try $0.on(
setBookmarkArticleError: .init { .error(errorCode: try $0.errorCodes().first ?? .notFound) },
setBookmarkArticleSuccess: .init {
.success(
linkId: try $0.bookmarkedArticle(selection: Selection.Article {
try $0.id()
})
)
}
},
setBookmarkArticleError: .init { .error(errorCode: try $0.errorCodes().first ?? .notFound) }
)
}

View file

@ -29,7 +29,6 @@ public extension Networker {
let selection = Selection<QueryResult, Unions.ArticleSavingRequestResult> {
try $0.on(
articleSavingRequestError: .init { .error(errorCode: (try? $0.errorCodes().first) ?? .notFound) },
articleSavingRequestSuccess: .init {
.saved(
status: try $0.articleSavingRequest(
@ -41,7 +40,8 @@ public extension Networker {
}
)
)
}
},
articleSavingRequestError: .init { .error(errorCode: (try? $0.errorCodes().first) ?? .notFound) }
)
}
@ -92,23 +92,25 @@ public extension DataService {
}
let preparedDocument: InputObjects.PreparedDocumentInput? = {
guard let html = pageScrapePayload.html, let title = pageScrapePayload.title else { return nil }
return InputObjects.PreparedDocumentInput(
document: html,
pageInfo: InputObjects.PageInfoInput(title: OptionalArgument(title))
)
if case let .html(html, title) = pageScrapePayload.contentType {
return InputObjects.PreparedDocumentInput(
document: html,
pageInfo: InputObjects.PageInfoInput(title: OptionalArgument(title))
)
}
return nil
}()
let input = InputObjects.CreateArticleInput(
url: pageScrapePayload.url,
preparedDocument: OptionalArgument(preparedDocument),
uploadFileId: uploadFileId != nil ? .present(uploadFileId!) : .null(),
url: pageScrapePayload.url
uploadFileId: uploadFileId != nil ? .present(uploadFileId!) : .null()
)
let selection = Selection<MutationResult, Unions.CreateArticleResult> {
try $0.on(
createArticleError: .init { .error(errorCode: (try? $0.errorCodes().first) ?? .unableToParse) },
createArticleSuccess: .init { .saved(created: try $0.created()) }
createArticleSuccess: .init { .saved(created: try $0.created()) },
createArticleError: .init { .error(errorCode: (try? $0.errorCodes().first) ?? .unableToParse) }
)
}
@ -161,7 +163,6 @@ public extension DataService {
let selection = Selection<MutationResult, Unions.CreateArticleSavingRequestResult> {
try $0.on(
createArticleSavingRequestError: .init { .error(errorCode: (try? $0.errorCodes().first) ?? .badData) },
createArticleSavingRequestSuccess: .init {
.saved(
status: try $0.articleSavingRequest(
@ -173,7 +174,8 @@ public extension DataService {
}
)
)
}
},
createArticleSavingRequestError: .init { .error(errorCode: (try? $0.errorCodes().first) ?? .badData) }
)
}

View file

@ -6,10 +6,11 @@ import SwiftGraphQL
public extension DataService {
func uploadPDFPublisher(
pageScrapePayload: PageScrapePayload,
data: Data,
requestId: String
) -> AnyPublisher<Void, SaveArticleError> {
uploadFileRequestPublisher(pageScrapePayload: pageScrapePayload)
.flatMap { self.uploadFilePublisher(fileUploadConfig: $0, fileURLString: pageScrapePayload.url) }
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)
@ -25,20 +26,21 @@ private struct UploadFileRequestPayload {
private extension DataService {
// swiftlint:disable:next line_length
func uploadFileRequestPublisher(pageScrapePayload: PageScrapePayload) -> AnyPublisher<UploadFileRequestPayload, SaveArticleError> {
func uploadFileRequestPublisher(pageScrapePayload: PageScrapePayload, requestId: String?) -> AnyPublisher<UploadFileRequestPayload, SaveArticleError> {
enum MutationResult {
case success(payload: UploadFileRequestPayload)
case error(errorCode: Enums.UploadFileRequestErrorCode?)
}
let input = InputObjects.UploadFileRequestInput(
url: pageScrapePayload.url,
contentType: "application/pdf",
url: pageScrapePayload.url
createPageEntry: OptionalArgument(true),
clientRequestId: OptionalArgument(requestId)
)
let selection = Selection<MutationResult, Unions.UploadFileRequestResult> {
try $0.on(
uploadFileRequestError: .init { .error(errorCode: try? $0.errorCodes().first) },
uploadFileRequestSuccess: .init {
.success(
payload: UploadFileRequestPayload(
@ -47,7 +49,8 @@ private extension DataService {
urlString: try $0.uploadSignedUrl()
)
)
}
},
uploadFileRequestError: .init { .error(errorCode: try? $0.errorCodes().first) }
)
}
@ -88,16 +91,14 @@ private extension DataService {
}
// swiftlint:disable:next line_length
func uploadFilePublisher(fileUploadConfig: UploadFileRequestPayload, fileURLString: String) -> AnyPublisher<String, SaveArticleError> {
let pdfData = URL(string: fileURLString).flatMap { try? Data(contentsOf: $0) }
func uploadFilePublisher(fileUploadConfig: UploadFileRequestPayload, data: Data) -> AnyPublisher<String, SaveArticleError> {
let url = fileUploadConfig.urlString.flatMap { URL(string: $0) }
guard let url = url, let pdfData = pdfData else { return Future { $0(.failure(.badData)) }.eraseToAnyPublisher() }
guard let url = url else { return Future { $0(.failure(.badData)) }.eraseToAnyPublisher() }
var request = URLRequest(url: url)
request.httpMethod = "PUT"
request.addValue("application/pdf", forHTTPHeaderField: "content-type")
request.httpBody = pdfData
request.httpBody = data
return networker.urlSession.dataTaskPublisher(for: request)
.tryMap { data, response -> String in
@ -132,16 +133,16 @@ private extension DataService {
}
let input = InputObjects.SaveFileInput(
clientRequestId: requestId,
url: pageScrapePayload.url,
source: "ios-file",
uploadFileId: uploadFileId,
url: pageScrapePayload.url
clientRequestId: requestId,
uploadFileId: uploadFileId
)
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 { .saved(requestId: requestId, url: (try? $0.url()) ?? "") },
saveError: .init { .error(errorCode: (try? $0.errorCodes().first) ?? .unknown) }
)
}

View file

@ -5,24 +5,23 @@ import SwiftGraphQL
public extension DataService {
// swiftlint:disable:next line_length
func savePagePublisher(pageScrapePayload: PageScrapePayload, requestId: String) -> AnyPublisher<Void, SaveArticleError> {
func savePagePublisher(pageScrapePayload: PageScrapePayload, html: String, title: String?, requestId: String) -> AnyPublisher<Void, SaveArticleError> {
enum MutationResult {
case saved(requestId: String, url: String)
case error(errorCode: Enums.SaveErrorCode)
}
let input = InputObjects.SavePageInput(
clientRequestId: requestId,
originalContent: pageScrapePayload.html ?? "",
source: "ios-page",
title: OptionalArgument(pageScrapePayload.title),
url: pageScrapePayload.url
url: requestId,
source: html,
clientRequestId: "ios-page",
title: OptionalArgument(title),
originalContent: pageScrapePayload.url
)
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 { .saved(requestId: requestId, url: (try? $0.url()) ?? "") }, saveError: .init { .error(errorCode: (try? $0.errorCodes().first) ?? .unknown) }
)
}

View file

@ -12,15 +12,15 @@ public extension DataService {
}
let input = InputObjects.SaveUrlInput(
clientRequestId: requestId,
url: pageScrapePayload.url,
source: "ios-url",
url: pageScrapePayload.url
clientRequestId: requestId
)
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 { .saved(requestId: requestId, url: (try? $0.url()) ?? "") },
saveError: .init { .error(errorCode: (try? $0.errorCodes().first) ?? .unknown) }
)
}

View file

@ -32,16 +32,16 @@ extension DataService {
let selection = Selection<MutationResult, Unions.SetLabelsResult> {
try $0.on(
setLabelsError: .init { .error(errorCode: try $0.errorCodes().first ?? .badRequest) },
setLabelsSuccess: .init { .saved(feedItem: try $0.labels(selection: feedItemLabelSelection.list)) }
setLabelsSuccess: .init { .saved(feedItem: try $0.labels(selection: feedItemLabelSelection.list)) },
setLabelsError: .init { .error(errorCode: try $0.errorCodes().first ?? .badRequest) }
)
}
let mutation = Selection.Mutation {
try $0.setLabels(
input: InputObjects.SetLabelsInput(
labelIds: labelIDs,
pageId: itemID
pageId: itemID,
labelIds: labelIDs
),
selection: selection
)

View file

@ -33,12 +33,12 @@ extension DataService {
let selection = Selection<MutationResult, Unions.SaveArticleReadingProgressResult> {
try $0.on(
saveArticleReadingProgressError: .init { .error(errorCode: try $0.errorCodes().first ?? .badData) },
saveArticleReadingProgressSuccess: .init {
.saved(
readingProgress: try $0.updatedArticle(selection: Selection.Article { try $0.readingProgressPercent() })
)
}
},
saveArticleReadingProgressError: .init { .error(errorCode: try $0.errorCodes().first ?? .badData) }
)
}
@ -46,8 +46,8 @@ extension DataService {
try $0.saveArticleReadingProgress(
input: InputObjects.SaveArticleReadingProgressInput(
id: itemID,
readingProgressAnchorIndex: anchorIndex,
readingProgressPercent: readingProgress
readingProgressPercent: readingProgress,
readingProgressAnchorIndex: anchorIndex
),
selection: selection
)

View file

@ -31,18 +31,18 @@ extension DataService {
let selection = Selection<MutationResult, Unions.UpdateHighlightResult> {
try $0.on(
updateHighlightError: .init { .error(errorCode: try $0.errorCodes().first ?? .badData) },
updateHighlightSuccess: .init {
.saved(highlight: try $0.highlight(selection: highlightSelection))
}
},
updateHighlightError: .init { .error(errorCode: try $0.errorCodes().first ?? .badData) }
)
}
let mutation = Selection.Mutation {
try $0.updateHighlight(
input: InputObjects.UpdateHighlightInput(
annotation: OptionalArgument(annotation),
highlightId: highlightID,
annotation: OptionalArgument(annotation),
sharedAt: OptionalArgument(nil)
),
selection: selection

View file

@ -90,7 +90,7 @@ extension DataService {
case error(error: String)
}
let articleSelection = Selection.Article {
let articleContentSelection = Selection.Article {
ArticleProps(
item: InternalLinkedItem(
id: try $0.id(),
@ -121,18 +121,17 @@ extension DataService {
let selection = Selection<QueryResult, Unions.ArticleResult> {
try $0.on(
articleSuccess: .init {
QueryResult.success(result: try $0.article(selection: articleContentSelection))
},
articleError: .init {
QueryResult.error(error: try $0.errorCodes().description)
},
articleSuccess: .init {
QueryResult.success(result: try $0.article(selection: articleSelection))
}
)
}
let query = Selection.Query {
// backend has a hack that allows us to pass in itemID in place of slug
try $0.article(slug: itemID, username: username, selection: selection)
try $0.article(username: username, slug: itemID, selection: selection)
}
let path = appEnvironment.graphqlPath

View file

@ -12,11 +12,11 @@ public extension DataService {
let selection = Selection<QueryResult, Unions.LabelsResult> {
try $0.on(
labelsError: .init {
QueryResult.error(error: try $0.errorCodes().description)
},
labelsSuccess: .init {
QueryResult.success(result: try $0.labels(selection: feedItemLabelSelection.list))
},
labelsError: .init {
QueryResult.error(error: try $0.errorCodes().description)
}
)
}

View file

@ -24,9 +24,6 @@ public extension DataService {
let selection = Selection<QueryResult, Unions.ArticlesResult> {
try $0.on(
articlesError: .init {
QueryResult.error(error: try $0.errorCodes().description)
},
articlesSuccess: .init {
QueryResult.success(
result: InternalHomeFeedData(
@ -36,22 +33,25 @@ public extension DataService {
})
)
)
},
articlesError: .init {
QueryResult.error(error: try $0.errorCodes().description)
}
)
}
let query = Selection.Query {
try $0.articles(
after: OptionalArgument(cursor),
first: OptionalArgument(limit),
includePending: OptionalArgument(true),
query: OptionalArgument(searchQuery),
sharedOnly: .present(false),
sort: OptionalArgument(
InputObjects.SortParams(
by: .updatedTime, order: .present(.descending)
order: .present(.descending), by: .updatedTime
)
),
after: OptionalArgument(cursor),
first: OptionalArgument(limit),
query: OptionalArgument(searchQuery),
includePending: OptionalArgument(true),
selection: selection
)
}
@ -90,20 +90,44 @@ public extension DataService {
case error(error: String)
}
let articleSelection = Selection.Article {
InternalLinkedItem(
id: try $0.id(),
title: try $0.title(),
createdAt: try $0.createdAt().value ?? Date(),
savedAt: try $0.savedAt().value ?? Date(),
readingProgress: try $0.readingProgressPercent(),
readingProgressAnchor: try $0.readingProgressAnchorIndex(),
imageURLString: try $0.image(),
onDeviceImageURLString: nil,
documentDirectoryPath: nil,
pageURLString: try $0.url(),
descriptionText: try $0.description(),
publisherURLString: try $0.originalArticleUrl(),
siteName: try $0.siteName(),
author: try $0.author(),
publishDate: try $0.publishedAt()?.value,
slug: try $0.slug(),
isArchived: try $0.isArchived(),
contentReader: try $0.contentReader().rawValue,
labels: try $0.labels(selection: feedItemLabelSelection.list.nullable) ?? []
)
}
let selection = Selection<QueryResult, Unions.ArticleResult> {
try $0.on(
articleError: .init {
QueryResult.error(error: try $0.errorCodes().description)
},
articleSuccess: .init {
QueryResult.success(result: try $0.article(selection: articleSelection))
},
articleError: .init {
QueryResult.error(error: try $0.errorCodes().description)
}
)
}
let query = Selection.Query {
// backend has a hack that allows us to pass in itemID in place of slug
try $0.article(slug: itemID, username: username, selection: selection)
try $0.article(username: username, slug: itemID, selection: selection)
}
let path = appEnvironment.graphqlPath
@ -130,7 +154,7 @@ public extension DataService {
}
}
private let articleSelection = Selection.Article {
private let libraryArticleSelection = Selection.Article {
InternalLinkedItem(
id: try $0.id(),
title: try $0.title(),
@ -155,5 +179,5 @@ private let articleSelection = Selection.Article {
}
private let articleEdgeSelection = Selection.ArticleEdge {
try $0.node(selection: articleSelection)
try $0.node(selection: libraryArticleSelection)
}

View file

@ -20,9 +20,6 @@ extension DataService {
let selection = Selection<QueryResult, Unions.SearchResult> {
try $0.on(
searchError: .init {
QueryResult.error(error: try $0.errorCodes().description)
},
searchSuccess: .init {
QueryResult.success(
result: LinkedItemIDFetchResult(
@ -32,6 +29,9 @@ extension DataService {
})
)
)
},
searchError: .init {
QueryResult.error(error: try $0.errorCodes().description)
}
)
}

View file

@ -20,11 +20,11 @@ public extension DataService {
let selection = Selection<QueryResult, Unions.NewsletterEmailsResult> {
try $0.on(
newsletterEmailsError: .init {
QueryResult.error(error: try $0.errorCodes().description)
},
newsletterEmailsSuccess: .init {
QueryResult.success(result: try $0.newsletterEmails(selection: newsletterEmailSelection.list))
},
newsletterEmailsError: .init {
QueryResult.error(error: try $0.errorCodes().description)
}
)
}

View file

@ -26,11 +26,11 @@ public extension DataService {
let selection = Selection<QueryResult, Unions.SubscriptionsResult> {
try $0.on(
subscriptionsError: .init {
QueryResult.error(error: try $0.errorCodes().description)
},
subscriptionsSuccess: .init {
QueryResult.success(result: try $0.subscriptions(selection: subsciptionSelection.list))
},
subscriptionsError: .init {
QueryResult.error(error: try $0.errorCodes().description)
}
)
}

View file

@ -2057,7 +2057,9 @@ export enum UploadFileRequestErrorCode {
}
export type UploadFileRequestInput = {
clientRequestId?: InputMaybe<Scalars['String']>;
contentType: Scalars['String'];
createPageEntry?: InputMaybe<Scalars['Boolean']>;
url: Scalars['String'];
};

View file

@ -1581,7 +1581,9 @@ enum UploadFileRequestErrorCode {
}
input UploadFileRequestInput {
clientRequestId: String
contentType: String!
createPageEntry: Boolean
url: String!
}

View file

@ -51,6 +51,7 @@ import {
stringToHash,
userDataToUser,
validatedDate,
titleForFilePath,
} from '../../utils/helpers'
import {
ParsedContentPuppeteer,
@ -166,6 +167,7 @@ export const createArticleResolver = authorized<
.join('.')
).replace(/_/gi, ' ')
let title: string | undefined
let parsedContent: Readability.ParseResult | null = null
let canonicalUrl
let userArticleUrl: string | null = null
@ -219,6 +221,7 @@ export const createArticleResolver = authorized<
userArticleUrl = uploadFileDetails.fileUrl
canonicalUrl = uploadFile.url
pageType = PageType.File
title = titleForFilePath(uploadFile.url)
} else if (
source !== 'puppeteer-parse' &&
FORCE_PUPPETEER_URLS.some((regex) => regex.test(url))
@ -252,6 +255,7 @@ export const createArticleResolver = authorized<
content: parsedContent?.content || '',
description: parsedContent?.excerpt || '',
title:
title ||
parsedContent?.title ||
preparedDocument?.pageInfo.title ||
croppedPathname,
@ -333,7 +337,6 @@ export const createArticleResolver = authorized<
} else {
// update existing page's state from processing to succeeded
articleToSave.archivedAt = archive ? saveTime : undefined
articleToSave.url = uploadFileUrlOverride || articleToSave.url
const updated = await updatePage(pageId, articleToSave, {
...ctx,
uid,

View file

@ -16,7 +16,7 @@ import path from 'path'
import normalizeUrl from 'normalize-url'
import { analytics } from '../../utils/analytics'
import { env } from '../../env'
import { createPage } from '../../elastic/pages'
import { createPage, getPageByParam, updatePage } from '../../elastic/pages'
import { PageType } from '../../elastic/types'
import { generateSlug } from '../../utils/helpers'
@ -82,28 +82,44 @@ export const uploadFileRequestResolver: ResolverFn<
input.contentType
)
const pageId = await createPage(
{
id: '',
url: input.url,
if (input.createPageEntry) {
const page = await getPageByParam({
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 { errorCodes: [UploadFileRequestErrorCode.FailedCreate] }
url: input.url,
})
if (page) {
await updatePage(
page.id,
{
savedAt: new Date(),
archivedAt: null,
},
ctx
)
} else {
const pageId = await createPage(
{
url: input.url,
id: input.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 { errorCodes: [UploadFileRequestErrorCode.FailedCreate] }
}
}
}
return { id: uploadFileData.id, uploadSignedUrl }

View file

@ -416,7 +416,10 @@ const schema = gql`
input UploadFileRequestInput {
url: String!
contentType: String!
createPageEntry: Boolean
clientRequestId: String
}
enum UploadFileRequestErrorCode {
UNAUTHORIZED
BAD_INPUT

View file

@ -83,13 +83,14 @@ export const createPageSaveRequest = async (
priority = priority || (await getPriorityByRateLimit(userId))
// look for existing page
url = normalizeUrl(url, {
const normalizedUrl = normalizeUrl(url, {
stripHash: true,
stripWWW: false,
})
let page = await getPageByParam({
userId,
url,
url: normalizedUrl,
})
if (page) {
console.log('Page already exists', page)

View file

@ -2,17 +2,9 @@ import Knex from 'knex'
import { PubsubClient } from '../datalayer/pubsub'
import { UserData } from '../datalayer/user/model'
import { homePageURL } from '../env'
import {
PageType,
SaveErrorCode,
SaveFileInput,
SaveResult,
} from '../generated/graphql'
import { SaveErrorCode, SaveFileInput, SaveResult } from '../generated/graphql'
import { DataModels } from '../resolvers/types'
import { generateSlug } from '../utils/helpers'
import { getStorageFileDetails, makeStorageFilePublic } from '../utils/uploads'
import { createPage, getPageByParam, updatePage } from '../elastic/pages'
import { ArticleSavingRequestStatus } from '../elastic/types'
import { getStorageFileDetails } from '../utils/uploads'
type SaveContext = {
pubsub: PubsubClient

View file

@ -18,6 +18,8 @@ import { Merge } from '../util'
import { CreateArticlesSuccessPartial } from '../resolvers'
import { ArticleSavingRequestStatus, Page } from '../elastic/types'
import { updatePage } from '../elastic/pages'
import path from 'path'
import normalizeUrl from 'normalize-url'
interface InputObject {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -230,3 +232,13 @@ export const validatedDate = (
return undefined
}
}
export const titleForFilePath = (url: string): string => {
try {
const title = decodeURI(path.basename(new URL(url).pathname, '.pdf'))
return title
} catch (e) {
console.log(e)
}
return url
}

View file

@ -94,7 +94,7 @@ const uploadToSignedUrl = async ({ id, uploadSignedUrl }, contentType, contentOb
})
};
const getUploadIdAndSignedUrl = async (userId, url) => {
const getUploadIdAndSignedUrl = async (userId, url, articleSavingRequestId) => {
const auth = await signToken({ uid: userId }, process.env.JWT_SECRET);
const data = JSON.stringify({
query: `mutation UploadFileRequest($input: UploadFileRequestInput!) {
@ -112,6 +112,7 @@ const getUploadIdAndSignedUrl = async (userId, url) => {
input: {
url,
contentType: 'application/pdf',
clientRequestId: articleSavingRequestId,
}
}
});
@ -126,10 +127,10 @@ const getUploadIdAndSignedUrl = async (userId, url) => {
return response.data.data.uploadFileRequest;
};
const uploadPdf = async (url, userId) => {
const uploadPdf = async (url, userId, articleSavingRequestId) => {
validateUrlString(url);
const uploadResult = await getUploadIdAndSignedUrl(userId, url);
const uploadResult = await getUploadIdAndSignedUrl(userId, url, articleSavingRequestId);
await uploadToSignedUrl(uploadResult, 'application/pdf', url);
return uploadResult.id;
};
@ -282,7 +283,7 @@ async function fetchContent(req, res) {
try {
if (contentType === 'application/pdf') {
const uploadedFileId = await uploadPdf(finalUrl, userId);
const uploadedFileId = await uploadPdf(finalUrl, userId, articleSavingRequestId);
const l = await saveUploadedPdf(userId, finalUrl, uploadedFileId, articleSavingRequestId);
} else {
if (!content || !title) {