enable read now link from share extension
This commit is contained in:
@ -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
|
||||
|
||||
@ -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) }
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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()
|
||||
|
||||
Reference in New Issue
Block a user