From da9b77153aa97b6cd60fd0f1dbe3589bb372701e Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 17 May 2022 14:48:44 -0700 Subject: [PATCH] enable read now link from share extension --- .../App/Views/LinkItemDetailView.swift | 2 +- .../Sources/App/Views/RootView/RootView.swift | 19 +-- .../App/Views/RootView/RootViewModel.swift | 26 +---- .../Views/WebReader/WebReaderContainer.swift | 3 +- .../WebReader/WebReaderLoadingContainer.swift | 110 ++++++++++++++++++ .../Queries/LibraryItemsQuery.swift | 49 ++++++++ .../Sources/Utils/FeatureFlags.swift | 1 - .../Sources/Views/FullScreenWebAppView.swift | 60 ---------- .../Sources/Views/ShareExtensionView.swift | 39 ++----- 9 files changed, 187 insertions(+), 122 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift delete mode 100644 apple/OmnivoreKit/Sources/Views/FullScreenWebAppView.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift index e25217235..f11d72335 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift @@ -147,7 +147,7 @@ struct LinkItemDetailView: View { viewModel.trackReadEvent() } } else { - WebReaderContainerView(item: viewModel.item) + WebReaderContainerView(item: viewModel.item, isPresentedModally: false) .navigationBarHidden(hideNavBar) .task { hideNavBar = true diff --git a/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift b/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift index 596ff4d69..a2ea86ff9 100644 --- a/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift @@ -1,11 +1,12 @@ +import Models import Services import SwiftUI import Utils import Views -struct SafariWebLinkPath: Identifiable { +struct LinkRequest: Identifiable { let id: UUID - let path: String + let serverID: String } public struct RootView: View { @@ -51,14 +52,14 @@ struct InnerRootView: View { viewModel.triggerPushNotificationRequestIfNeeded() } #if os(iOS) - .fullScreenCover(item: $viewModel.webLinkPath, content: { safariLinkPath in + .fullScreenCover(item: $viewModel.linkRequest) { _ in NavigationView { - FullScreenWebAppView( - viewModel: viewModel.webAppWrapperViewModel(webLinkPath: safariLinkPath.path), - handleClose: { viewModel.webLinkPath = nil } + WebReaderLoadingContainer( + requestID: viewModel.linkRequest?.serverID ?? "", + handleClose: { viewModel.linkRequest = nil } ) } - }) + } #endif .snackBar(isShowing: $viewModel.showSnackbar, message: viewModel.snackbarMessage) // Schedule the dismissal every time we present the snackbar. @@ -96,8 +97,8 @@ struct InnerRootView: View { #if os(iOS) .onOpenURL { url in withoutAnimation { - if viewModel.webLinkPath != nil { - viewModel.webLinkPath = nil + if viewModel.linkRequest != nil { + viewModel.linkRequest = nil DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { Task { await viewModel.onOpenURL(url: url) } } diff --git a/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift index 51cdf52e5..503af6986 100644 --- a/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift @@ -15,7 +15,7 @@ public final class RootViewModel: ObservableObject { let services = Services() @Published public var showPushNotificationPrimer = false - @Published var webLinkPath: SafariWebLinkPath? + @Published var linkRequest: LinkRequest? @Published var snackbarMessage: String? @Published var showSnackbar = false @@ -59,23 +59,9 @@ public final class RootViewModel: ObservableObject { } @MainActor func onOpenURL(url: URL) async { - guard let linkRequestID = DeepLink.make(from: url)?.linkRequestID else { return } - - let username: String? = await { - if let cachedUsername = services.dataService.currentViewer?.username { - return cachedUsername - } - - if let viewerObjectID = try? await services.dataService.fetchViewer() { - let viewer = services.dataService.viewContext.object(with: viewerObjectID) as? Viewer - return viewer?.unwrappedUsername - } - - return nil - }() - - guard let username = username else { return } - webLinkPath = SafariWebLinkPath(id: UUID(), path: linkRequestPath(username: username, requestID: linkRequestID)) + if let linkRequestID = DeepLink.make(from: url)?.linkRequestID { + linkRequest = LinkRequest(id: UUID(), serverID: linkRequestID) + } } func triggerPushNotificationRequestIfNeeded() { @@ -107,10 +93,6 @@ public final class RootViewModel: ObservableObject { UNUserNotificationCenter.current().requestAuth() } #endif - - private func linkRequestPath(username: String, requestID: String) -> String { - "/app/\(username)/link-request/\(requestID)" - } } public struct IntercomProvider { diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index 754440c17..b401782ea 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -7,6 +7,7 @@ import WebKit #if os(iOS) struct WebReaderContainerView: View { let item: LinkedItem + let isPresentedModally: Bool @State private var showFontSizePopover = false @State private var showLabelsModal = false @@ -66,7 +67,7 @@ import WebKit Button( action: { self.presentationMode.wrappedValue.dismiss() }, label: { - Image(systemName: "chevron.backward") + Image(systemName: isPresentedModally ? "xmark" : "chevron.backward") .font(.appTitleTwo) .foregroundColor(.appGrayTextContrast) .padding(.horizontal) diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift new file mode 100644 index 000000000..3527aacb8 --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift @@ -0,0 +1,110 @@ +import Models +import Services +import SwiftUI +import Utils + +#if os(iOS) + @MainActor final class WebReaderLoadingContainerViewModel: ObservableObject { + @Published var item: LinkedItem? + @Published var errorMessage: String? + + func loadItem(dataService: DataService, requestID: String) async { + let username: String? = await { + if let cachedUsername = dataService.currentViewer?.username { + return cachedUsername + } + + if let viewerObjectID = try? await dataService.fetchViewer() { + let viewer = dataService.viewContext.object(with: viewerObjectID) as? Viewer + return viewer?.unwrappedUsername + } + + return nil + }() + + guard let username = username else { return } + + await fetchLinkedItem(dataService: dataService, requestID: requestID, username: username) + } + + private func fetchLinkedItem( + dataService: DataService, + requestID: String, + username: String, + requestCount: Int = 1 + ) async { + guard requestCount < 7 else { + errorMessage = "Unable to fetch item." + return + } + + if let objectID = try? await dataService.fetchLinkedItem(username: username, itemID: requestID) { + if let linkedItem = dataService.viewContext.object(with: objectID) as? LinkedItem { + item = linkedItem + } else { + errorMessage = "Unable to fetch item." + } + return + } + + // Retry on error + do { + let retryDelayInNanoSeconds = UInt64(requestCount * 2 * 1_000_000_000) + try await Task.sleep(nanoseconds: retryDelayInNanoSeconds) + await fetchLinkedItem( + dataService: dataService, + requestID: requestID, + username: username, + requestCount: requestCount + 1 + ) + } catch { + errorMessage = "Unable to fetch item." + } + } + + func trackReadEvent() { + guard let item = item else { return } + + EventTracker.track( + .linkRead( + linkID: item.unwrappedID, + slug: item.unwrappedSlug, + originalArticleURL: item.unwrappedPageURLString + ) + ) + } + } + + public struct WebReaderLoadingContainer: View { + let requestID: String + let handleClose: () -> Void + + @EnvironmentObject var dataService: DataService + @StateObject var viewModel = WebReaderLoadingContainerViewModel() + + public var body: some View { + if let item = viewModel.item { + WebReaderContainerView(item: item, isPresentedModally: true) + .navigationBarHidden(true) + .accentColor(.appGrayTextContrast) + .task { viewModel.trackReadEvent() } + } else if let errorMessage = viewModel.errorMessage { + Text(errorMessage) + } else { + ProgressView() + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button( + action: handleClose, + label: { + Image(systemName: "xmark") + .foregroundColor(.appGrayTextContrast) + } + ) + } + } + .task { await viewModel.loadItem(dataService: dataService, requestID: requestID) } + } + } + } +#endif diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LibraryItemsQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LibraryItemsQuery.swift index 3534ce291..286171204 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LibraryItemsQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LibraryItemsQuery.swift @@ -79,6 +79,55 @@ public extension DataService { } } } + + func fetchLinkedItem(username: String, itemID: String) async throws -> NSManagedObjectID { + struct ArticleProps { + let item: InternalLinkedItem + } + + enum QueryResult { + case success(result: InternalLinkedItem) + case error(error: String) + } + + let selection = Selection { + try $0.on( + articleError: .init { + QueryResult.error(error: try $0.errorCodes().description) + }, + articleSuccess: .init { + QueryResult.success(result: try $0.article(selection: articleSelection)) + } + ) + } + + 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) + } + + let path = appEnvironment.graphqlPath + let headers = networker.defaultHeaders + + return try await withCheckedThrowingContinuation { continuation in + send(query, to: path, headers: headers) { [weak self] queryResult in + guard let payload = try? queryResult.get() else { + continuation.resume(throwing: ContentFetchError.network) + return + } + switch payload.data { + case let .success(result: result): + if let context = self?.backgroundContext, let item = [result].persist(context: context)?.first { + continuation.resume(returning: item.objectID) + } else { + continuation.resume(throwing: BasicError.message(messageText: "CoreData error")) + } + case .error: + continuation.resume(throwing: BasicError.message(messageText: "LinkedItem fetch error")) + } + } + } + } } private let articleSelection = Selection.Article { diff --git a/apple/OmnivoreKit/Sources/Utils/FeatureFlags.swift b/apple/OmnivoreKit/Sources/Utils/FeatureFlags.swift index 9ce3b9eb1..7b5d0f704 100644 --- a/apple/OmnivoreKit/Sources/Utils/FeatureFlags.swift +++ b/apple/OmnivoreKit/Sources/Utils/FeatureFlags.swift @@ -9,7 +9,6 @@ import Foundation public enum FeatureFlag { public static let showAccountDeletion = false public static let enableSnoozeFromShareExtension = false - public static let enableReadNowFromShareExtension = false public static let enableRemindersFromShareExtension = false public static let enablePushNotifications = false public static let enableShareButton = false diff --git a/apple/OmnivoreKit/Sources/Views/FullScreenWebAppView.swift b/apple/OmnivoreKit/Sources/Views/FullScreenWebAppView.swift deleted file mode 100644 index 9b2beb3fb..000000000 --- a/apple/OmnivoreKit/Sources/Views/FullScreenWebAppView.swift +++ /dev/null @@ -1,60 +0,0 @@ -import SwiftUI - -#if os(iOS) - - public struct FullScreenWebAppView: View { - @State var showFontSizePopover = false - let viewModel: WebAppWrapperViewModel - let handleClose: () -> Void - - public init( - viewModel: WebAppWrapperViewModel, - handleClose: @escaping () -> Void - ) { - self.viewModel = viewModel - self.handleClose = handleClose - } - - public var body: some View { - WebAppWrapperView(viewModel: viewModel) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button( - action: handleClose, - label: { - Image(systemName: "xmark") - .foregroundColor(.appGrayTextContrast) - } - ) - } - } - .toolbar { - ToolbarItem(placement: .automatic) { - Button( - action: { showFontSizePopover = true }, - label: { - Image(systemName: "textformat.size") - .foregroundColor(.appGrayTextContrast) - } - ) - #if os(iOS) - .fittedPopover(isPresented: $showFontSizePopover) { - FontSizeAdjustmentPopoverView( - increaseFontAction: { viewModel.sendIncreaseFontSignal = true }, - decreaseFontAction: { viewModel.sendDecreaseFontSignal = true } - ) - } - #else - .popover(isPresented: $showFontSizePopover) { - FontSizeAdjustmentPopoverView( - increaseFontAction: { viewModel.sendIncreaseFontSignal = true }, - decreaseFontAction: { viewModel.sendDecreaseFontSignal = true } - ) - } - #endif - } - } - } - } - -#endif diff --git a/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift b/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift index 7f8cfed25..badcc27dc 100644 --- a/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift +++ b/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift @@ -112,21 +112,6 @@ public struct ShareExtensionChildView: View { @State var reminderTime: ReminderTime? @State var hideUntilReminded = false - private var savedStateView: some View { - HStack { - Spacer() - IconButtonView( - title: "Read Now", - systemIconName: "book", - action: { - readNowButtonAction() - } - ) - Spacer() - } - .padding(.horizontal, 8) - } - private func handleReminderTimeSelection(_ selectedTime: ReminderTime) { if selectedTime == reminderTime { reminderTime = nil @@ -156,20 +141,18 @@ public struct ShareExtensionChildView: View { Spacer() if case ShareExtensionStatus.successfullySaved = status { - if FeatureFlag.enableReadNowFromShareExtension { - savedStateView - } else { - HStack(spacing: 4) { - Text("Saved to Omnivore") - .font(.appTitleThree) - .foregroundColor(.appGrayText) - .padding(.trailing, 16) - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) - .lineLimit(nil) - } - .padding() + HStack { + Spacer() + IconButtonView( + title: "Read Now", + systemIconName: "book", + action: { + readNowButtonAction() + } + ) + Spacer() } + .padding(.horizontal, 8) } else if case let ShareExtensionStatus.failed(error) = status { HStack { Spacer()