From 033965f0b4940d46bf5258d26e65f40757245312 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 17 May 2022 11:32:07 -0700 Subject: [PATCH 1/3] remove homeViewModel references where they're no longer needed --- .../Components/FeedCardNavigationLink.swift | 4 ++-- .../Sources/App/Views/LinkItemDetailView.swift | 16 ++++++---------- .../Views/WebReader/WebReaderContainer.swift | 17 +++++++++-------- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift index 0ff39e4e4..895216be8 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift @@ -11,7 +11,7 @@ struct FeedCardNavigationLink: View { @ObservedObject var viewModel: HomeFeedViewModel var body: some View { - let destination = LinkItemDetailView(viewModel: LinkItemDetailViewModel(item: item, homeFeedViewModel: viewModel)) + let destination = LinkItemDetailView(viewModel: LinkItemDetailViewModel(item: item)) #if os(iOS) let modifiedDestination = destination .navigationTitle("") @@ -50,7 +50,7 @@ struct GridCardNavigationLink: View { @ObservedObject var viewModel: HomeFeedViewModel var body: some View { - let destination = LinkItemDetailView(viewModel: LinkItemDetailViewModel(item: item, homeFeedViewModel: viewModel)) + let destination = LinkItemDetailView(viewModel: LinkItemDetailViewModel(item: item)) #if os(iOS) let modifiedDestination = destination .navigationTitle("") diff --git a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift index 1b5124b36..e25217235 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift @@ -10,27 +10,23 @@ enum PDFProvider { } @MainActor final class LinkItemDetailViewModel: ObservableObject { - let homeFeedViewModel: HomeFeedViewModel @Published var item: LinkedItem @Published var webAppWrapperViewModel: WebAppWrapperViewModel? var subscriptions = Set() - init(item: LinkedItem, homeFeedViewModel: HomeFeedViewModel) { + init(item: LinkedItem) { self.item = item - self.homeFeedViewModel = homeFeedViewModel } func handleArchiveAction(dataService: DataService) { - homeFeedViewModel.setLinkArchived( - dataService: dataService, - objectID: item.objectID, - archived: !item.isArchived - ) + dataService.archiveLink(objectID: item.objectID, archived: !item.isArchived) + Snackbar.show(message: !item.isArchived ? "Link archived" : "Link moved to Inbox") } func handleDeleteAction(dataService: DataService) { - homeFeedViewModel.removeLink(dataService: dataService, objectID: item.objectID) + Snackbar.show(message: "Link removed") + dataService.removeLink(objectID: item.objectID) } func updateItemReadStatus(dataService: DataService) { @@ -151,7 +147,7 @@ struct LinkItemDetailView: View { viewModel.trackReadEvent() } } else { - WebReaderContainerView(item: viewModel.item, homeFeedViewModel: viewModel.homeFeedViewModel) + WebReaderContainerView(item: viewModel.item) .navigationBarHidden(hideNavBar) .task { hideNavBar = true diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index 4aa28e430..754440c17 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -7,9 +7,9 @@ import WebKit #if os(iOS) struct WebReaderContainerView: View { let item: LinkedItem - let homeFeedViewModel: HomeFeedViewModel @State private var showFontSizePopover = false + @State private var showLabelsModal = false @State var showHighlightAnnotationModal = false @State var safariWebLink: SafariWebLink? @State private var navBarVisibilityRatio = 1.0 @@ -87,16 +87,13 @@ import WebKit content: { Group { Button( - action: { homeFeedViewModel.itemUnderLabelEdit = item }, + action: { showLabelsModal = true }, label: { Label("Edit Labels", systemImage: "tag") } ) Button( action: { - homeFeedViewModel.setLinkArchived( - dataService: dataService, - objectID: item.objectID, - archived: !item.isArchived - ) + dataService.archiveLink(objectID: item.objectID, archived: !item.isArchived) + Snackbar.show(message: !item.isArchived ? "Link archived" : "Link moved to Inbox") }, label: { Label( @@ -130,10 +127,14 @@ import WebKit } .alert("Are you sure?", isPresented: $showDeleteConfirmation) { Button("Remove Link", role: .destructive) { - homeFeedViewModel.removeLink(dataService: dataService, objectID: item.objectID) + Snackbar.show(message: "Link removed") + dataService.removeLink(objectID: item.objectID) } Button("Cancel", role: .cancel, action: {}) } + .sheet(isPresented: $showLabelsModal) { + ApplyLabelsView(mode: .item(item), onSave: { _ in showLabelsModal = false }) + } } var body: some View { From da9b77153aa97b6cd60fd0f1dbe3589bb372701e Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 17 May 2022 14:48:44 -0700 Subject: [PATCH 2/3] 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() From 17e807afe333b256f3d655b06a02a3f0fa676ab3 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 17 May 2022 14:56:07 -0700 Subject: [PATCH 3/3] set stack style navigation for full screen reader --- .../Sources/App/Views/WebReader/WebReaderLoadingContainer.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift index 3527aacb8..efd41ef1a 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift @@ -86,6 +86,7 @@ import Utils if let item = viewModel.item { WebReaderContainerView(item: item, isPresentedModally: true) .navigationBarHidden(true) + .navigationViewStyle(.stack) .accentColor(.appGrayTextContrast) .task { viewModel.trackReadEvent() } } else if let errorMessage = viewModel.errorMessage {