enable read now link from share extension

This commit is contained in:
Satindar Dhillon
2022-05-17 14:48:44 -07:00
parent 033965f0b4
commit da9b77153a
9 changed files with 187 additions and 122 deletions

View File

@ -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

View File

@ -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) }
}

View File

@ -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 {

View File

@ -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)

View File

@ -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

View File

@ -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<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))
}
)
}
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 {

View File

@ -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

View File

@ -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

View File

@ -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()