From cd8347d0e077f0154a98d7727790ce3d54710878 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 8 Mar 2024 12:43:54 +0800 Subject: [PATCH 1/9] Properly observe model items so library position can be updated --- .../xcshareddata/swiftpm/Package.resolved | 30 ++------ apple/OmnivoreKit/Package.swift | 5 +- .../LibraryItemListNavigationLink.swift | 6 +- .../App/Views/Home/HomeFeedViewIOS.swift | 47 +----------- .../Sources/Views/FeedItem/GridCard.swift | 10 +-- .../Views/FeedItem/LibraryItemCard.swift | 73 ++----------------- 6 files changed, 24 insertions(+), 147 deletions(-) diff --git a/apple/Omnivore.xcworkspace/xcshareddata/swiftpm/Package.resolved b/apple/Omnivore.xcworkspace/xcshareddata/swiftpm/Package.resolved index b740628b5..d5a6b0de1 100644 --- a/apple/Omnivore.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/apple/Omnivore.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/nathantannar4/Engine", "state" : { - "revision" : "31949c114698e4fd43fd76290913bca415fa87bc", - "version" : "1.1.0" + "revision" : "e9867eb6df013abc65c3437d295e594077469a13", + "version" : "1.5.1" } }, { @@ -180,24 +180,6 @@ "version" : "1.0.2" } }, - { - "identity" : "swift-async-algorithms", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-async-algorithms", - "state" : { - "revision" : "da4e36f86544cdf733a40d59b3a2267e3a7bbf36", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections.git", - "state" : { - "revision" : "d029d9d39c87bed85b1c50adee7c41795261a192", - "version" : "1.0.6" - } - }, { "identity" : "swift-graphql", "kind" : "remoteSourceControl", @@ -257,8 +239,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/nathantannar4/Transmission", "state" : { - "revision" : "9517912f8f528c777f86f7896b5c35d7e43fa916", - "version" : "1.0.1" + "revision" : "3dac53ae4bddc7ab99e6374622a9c5eefbe50eed", + "version" : "1.1.4" } }, { @@ -266,8 +248,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/nathantannar4/Turbocharger", "state" : { - "revision" : "b4201ba0bc094facf6cabe3b36fd3763b51ccfc8", - "version" : "1.0.1" + "revision" : "095344c0cac57873e1552f30d3561ab1bec5ae35", + "version" : "1.1.4" } }, { diff --git a/apple/OmnivoreKit/Package.swift b/apple/OmnivoreKit/Package.swift index 4347f0f6a..93c47d10a 100644 --- a/apple/OmnivoreKit/Package.swift +++ b/apple/OmnivoreKit/Package.swift @@ -71,8 +71,9 @@ var dependencies: [Package.Dependency] { .package(url: "https://github.com/google/GoogleSignIn-iOS", from: "6.2.2"), .package(url: "https://github.com/gonzalezreal/swift-markdown-ui", from: "2.0.0"), .package(url: "https://github.com/PostHog/posthog-ios.git", from: "2.0.0"), - .package(url: "https://github.com/nathantannar4/Transmission", from: "1.0.1"), - .package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0") + .package(url: "https://github.com/nathantannar4/Engine", exact: "1.5.1"), + .package(url: "https://github.com/nathantannar4/Turbocharger", exact: "1.1.4"), + .package(url: "https://github.com/nathantannar4/Transmission", from: "1.1.4") ] // Comment out following line for macOS build deps.append(.package(url: "https://github.com/PSPDFKit/PSPDFKit-SP", from: "13.1.0")) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryItemListNavigationLink.swift b/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryItemListNavigationLink.swift index cb3b384df..63db31397 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryItemListNavigationLink.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryItemListNavigationLink.swift @@ -14,7 +14,7 @@ struct MacFeedCardNavigationLink: View { var body: some View { ZStack { - LibraryItemCard(item: LibraryItemData.make(from: item), viewer: dataService.currentViewer) + LibraryItemCard(item: item, viewer: dataService.currentViewer) NavigationLink(destination: LinkItemDetailView( linkedItemObjectID: item.objectID, isPDF: item.isPDF @@ -36,7 +36,7 @@ struct LibraryItemListNavigationLink: View { Button(action: { viewModel.presentItem(item: item) }, label: { - LibraryItemCard(item: LibraryItemData.make(from: item), viewer: dataService.currentViewer) + LibraryItemCard(item: item, viewer: dataService.currentViewer) }) } } @@ -54,7 +54,7 @@ struct LibraryItemGridCardNavigationLink: View { Button(action: { viewModel.presentItem(item: item) }, label: { - GridCard(item: LibraryItemData.make(from: item)) + GridCard(item: item) }) .buttonStyle(.plain) .aspectRatio(1.0, contentMode: .fill) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 92438d1ac..2fa9ec946 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -715,15 +715,6 @@ struct AnimatingCellHeight: AnimatableModifier { } } - var redactedItems: some View { - ForEach(Array(fakeLibraryItems(dataService: dataService).enumerated()), id: \.1.id) { _, item in - let horizontalInset = CGFloat(UIDevice.isIPad ? 20 : 10) - LibraryItemCard(item: item, viewer: dataService.currentViewer) - .listRowSeparatorTint(Color.thBorderColor) - .listRowInsets(.init(top: 0, leading: horizontalInset, bottom: 10, trailing: horizontalInset)) - }.redacted(reason: .placeholder) - } - var listItems: some View { ForEach(Array(viewModel.fetcher.items.enumerated()), id: \.1.unwrappedID) { idx, item in let horizontalInset = CGFloat(UIDevice.isIPad ? 20 : 10) @@ -814,9 +805,7 @@ struct AnimatingCellHeight: AnimatableModifier { } } - if viewModel.showLoadingBar == .redacted { - redactedItems - } else if viewModel.showLoadingBar == .simple { + if viewModel.showLoadingBar == .redacted || viewModel.showLoadingBar == .simple { VStack { ProgressView() } @@ -1001,14 +990,7 @@ struct AnimatingCellHeight: AnimatableModifier { ScrollView { LazyVGrid(columns: [GridItem(.adaptive(minimum: 325, maximum: 400), spacing: 16)], alignment: .center, spacing: 30) { - if viewModel.showLoadingBar == .redacted { - ForEach(fakeLibraryItems(dataService: dataService), id: \.id) { item in - GridCard(item: item) - .aspectRatio(1.0, contentMode: .fill) - .background(Color.systemBackground) - .cornerRadius(6) - }.redacted(reason: .placeholder) - } else if viewModel.showLoadingBar == .simple { + if viewModel.showLoadingBar == .redacted || viewModel.showLoadingBar == .simple { VStack { ProgressView() } @@ -1125,31 +1107,6 @@ struct LinkDestination: View { } } -func fakeLibraryItems(dataService _: DataService) -> [LibraryItemData] { - Array( - repeatElement(0, count: 20) - .map { _ in - LibraryItemData( - id: UUID().uuidString, - title: "fake title that is kind of long so it looks better", - pageURLString: "", - isArchived: false, - author: "fake author", - deepLink: nil, - hasLabels: false, - noteText: nil, - readingProgress: 10, - wordsCount: 10, - isPDF: false, - highlights: nil, - sortedLabels: [], - imageURL: nil, - publisherDisplayName: "fake publisher", - descriptionText: "This is a fake description" - ) - }) -} - struct BottomView: View { @ObservedObject var viewModel: HomeFeedViewModel @EnvironmentObject var dataService: DataService diff --git a/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift b/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift index 69631117c..4229cc18d 100644 --- a/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift +++ b/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift @@ -11,10 +11,10 @@ public enum GridCardAction { } public struct GridCard: View { - let item: LibraryItemData + let item: Models.LibraryItem public init( - item: LibraryItemData + item: Models.LibraryItem ) { self.item = item } @@ -67,7 +67,7 @@ public struct GridCard: View { var fallbackImage: some View { GeometryReader { geo in HStack { - Text(item.title) + Text(item.title ?? "") .font(fallbackFont) .frame(alignment: .center) .multilineTextAlignment(.leading) @@ -232,7 +232,7 @@ public struct GridCard: View { .dynamicTypeSize(.xSmall ... .medium) .padding(.horizontal, 15) - Text(item.title) + Text(item.title ?? "") .lineLimit(2) .font(.appHeadline) .foregroundColor(.appGrayTextContrast) @@ -246,7 +246,7 @@ public struct GridCard: View { // Link description and image HStack(alignment: .top) { - Text(item.descriptionText ?? item.title) + Text(item.descriptionText ?? item.title ?? "") .font(.appSubheadline) .foregroundColor(.appGrayTextContrast) .lineLimit(2) diff --git a/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift b/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift index 0a963f7da..261e45dc5 100644 --- a/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift +++ b/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift @@ -32,11 +32,11 @@ enum FlairLabels: String { } public extension View { - func draggableItem(item: LibraryItemData) -> some View { + func draggableItem(item: Models.LibraryItem) -> some View { #if os(iOS) if #available(iOS 16.0, *), let url = item.deepLink { return AnyView(self.draggable(url) { - Label(item.title, systemImage: "link") + Label(item.title ?? "", systemImage: "link") }) } #endif @@ -44,75 +44,12 @@ public extension View { } } -public struct LibraryItemData { - public var id: String - public let title: String - public let pageURLString: String - public var isArchived: Bool - public let author: String? - public let deepLink: URL? - public let hasLabels: Bool - public let noteText: String? - public let readingProgress: Double - public let wordsCount: Int64 - public let isPDF: Bool - public let highlights: NSSet? - public let sortedLabels: [LinkedItemLabel] - public let imageURL: URL? - public let publisherDisplayName: String? - public let descriptionText: String? - - public init(id: String, title: String, pageURLString: String, isArchived: Bool, author: String?, - deepLink: URL?, hasLabels: Bool, noteText: String?, - readingProgress: Double, wordsCount: Int64, isPDF: Bool, highlights: NSSet?, - sortedLabels: [LinkedItemLabel], imageURL: URL?, publisherDisplayName: String?, descriptionText: String?) - { - self.id = id - self.title = title - self.pageURLString = pageURLString - self.isArchived = isArchived - self.author = author - self.deepLink = deepLink - self.hasLabels = hasLabels - self.noteText = noteText - self.readingProgress = readingProgress - self.wordsCount = wordsCount - self.isPDF = isPDF - self.highlights = highlights - self.sortedLabels = sortedLabels - self.imageURL = imageURL - self.publisherDisplayName = publisherDisplayName - self.descriptionText = descriptionText - } - - public static func make(from item: Models.LibraryItem) -> LibraryItemData { - LibraryItemData( - id: item.unwrappedID, - title: item.unwrappedTitle, - pageURLString: item.unwrappedPageURLString, - isArchived: item.isArchived, - author: item.author, - deepLink: item.deepLink, - hasLabels: item.hasLabels, - noteText: item.noteText, - readingProgress: item.readingProgress, - wordsCount: item.wordsCount, - isPDF: item.isPDF, - highlights: item.highlights, - sortedLabels: item.sortedLabels, - imageURL: item.imageURL, - publisherDisplayName: item.publisherDisplayName, - descriptionText: item.descriptionText - ) - } -} - public struct LibraryItemCard: View { let viewer: Viewer? - var item: LibraryItemData + @ObservedObject var item: Models.LibraryItem @State var noteLineLimit: Int? = 3 - public init(item: LibraryItemData, viewer: Viewer?) { + public init(item: Models.LibraryItem, viewer: Viewer?) { self.item = item self.viewer = viewer } @@ -362,7 +299,7 @@ public struct LibraryItemCard: View { readInfo .dynamicTypeSize(.xSmall ... .medium) - Text(item.title) + Text(item.title ?? "") .font(.body).fontWeight(.semibold) .lineSpacing(1.25) .foregroundColor(.appGrayTextContrast) From 5383e8133bde2661d63c6cf451d5ec8e30f335e5 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 8 Mar 2024 12:49:41 +0800 Subject: [PATCH 2/9] Dont show footer during loading --- .../Sources/App/Views/Home/HomeFeedViewIOS.swift | 6 ++++-- apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 2fa9ec946..978ac9722 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -832,7 +832,9 @@ struct AnimatingCellHeight: AnimatableModifier { }, header: { filtersHeader }) - BottomView(viewModel: viewModel) + if viewModel.showLoadingBar == .none { + BottomView(viewModel: viewModel) + } } .padding(0) .listStyle(.plain) @@ -1038,7 +1040,7 @@ struct AnimatingCellHeight: AnimatableModifier { } } - if viewModel.fetcher.items.isEmpty { + if viewModel.fetcher.items.isEmpty || viewModel.showLoadingBar == .redacted || viewModel.showLoadingBar == .simple { EmptyState(viewModel: viewModel) } else { HStack { diff --git a/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift b/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift index 4229cc18d..15a79b8e5 100644 --- a/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift +++ b/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift @@ -11,7 +11,7 @@ public enum GridCardAction { } public struct GridCard: View { - let item: Models.LibraryItem + @ObservedObject var item: Models.LibraryItem public init( item: Models.LibraryItem From 2ce4fa3f48f359dc1518b8476a3896efc0995aa2 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 8 Mar 2024 13:20:22 +0800 Subject: [PATCH 3/9] Fix issues with search view hierarchy --- .../Sources/App/Views/Home/HomeFeedViewIOS.swift | 3 ++- .../Sources/App/Views/Home/HomeFeedViewModel.swift | 7 ++++++- .../Sources/App/Views/Home/LibrarySearchView.swift | 3 ++- .../Views/WebReader/WebReaderLoadingContainer.swift | 13 ++++++------- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 978ac9722..b4cd25501 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -493,7 +493,8 @@ struct AnimatingCellHeight: AnimatableModifier { options: PresentationLinkTransition.SlideTransitionOptions(edge: .trailing, options: PresentationLinkTransition.Options( - modalPresentationCapturesStatusBarAppearance: true + modalPresentationCapturesStatusBarAppearance: true, + preferredPresentationBackgroundColor: ThemeManager.currentBgColor ))), isPresented: $viewModel.presentWebContainer, destination: { diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index af5d6d68c..27b42b75b 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -73,7 +73,12 @@ enum LoadingBarStyle { self.linkIsActive = true } } - + + func pushLinkedRequest(request: LinkRequest) { + self.linkRequest = request + self.presentWebContainer = true + } + private var filterState: FetcherFilterState? { if let appliedFilter = appliedFilter { return FetcherFilterState( diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/LibrarySearchView.swift b/apple/OmnivoreKit/Sources/App/Views/Home/LibrarySearchView.swift index d7e768e0a..b75cff002 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/LibrarySearchView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/LibrarySearchView.swift @@ -101,7 +101,8 @@ Spacer() Image(systemName: "chevron.right") }.onTapGesture { - viewModel.linkRequest = LinkRequest(id: UUID(), serverID: item.id) + homeFeedViewModel.pushLinkedRequest(request: LinkRequest(id: UUID(), serverID: item.id)) + dismiss() } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift index 88b14a80d..bdc018795 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift @@ -10,6 +10,11 @@ import Views @Published var errorMessage: String? func loadItem(dataService: DataService, username: String, requestID: String) async { + if let cached = Models.LibraryItem.lookup(byID: requestID, inContext: dataService.viewContext) { + item = cached + return + } + guard let objectID = try? await dataService.loadItemContentUsingRequestID(username: username, requestID: requestID) else { @@ -57,8 +62,6 @@ public struct WebReaderLoadingContainer: View { PDFWrapperView(pdfURL: pdfURL) } #endif - } else if item.state == "CONTENT_NOT_FETCHED" { - ProgressView() } else { WebReaderContainerView(item: item) #if os(iOS) @@ -72,11 +75,7 @@ public struct WebReaderLoadingContainer: View { } else { ProgressView() .task { - if let username = dataService.currentViewer?.username { - await viewModel.loadItem(dataService: dataService, username: username, requestID: requestID) - } else { - viewModel.errorMessage = "You are not logged in." - } + await viewModel.loadItem(dataService: dataService, username: "me", requestID: requestID) } } } From bd4e40598497897341252f3f8de344b07b5f7795 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 8 Mar 2024 13:26:49 +0800 Subject: [PATCH 4/9] Dont show results indicator while loading on iPad --- .../Sources/App/Views/Home/HomeFeedViewIOS.swift | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index b4cd25501..7baa71f50 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -150,13 +150,15 @@ struct EmptyState: View { return AnyView(Group { Spacer() - VStack(alignment: .center, spacing: 20) { - Text("No results found for this query") - .font(Font.system(size: 18, weight: .bold)) + if viewModel.showLoadingBar == .none { + VStack(alignment: .center, spacing: 20) { + Text("No results found for this query") + .font(Font.system(size: 18, weight: .bold)) + } + .frame(minHeight: 400) + .frame(maxWidth: .infinity) + .padding() } - .frame(minHeight: 400) - .frame(maxWidth: .infinity) - .padding() Spacer() }) From 6bc100942f079417842efd484cd5a7c8411d36e4 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 8 Mar 2024 16:40:49 +0800 Subject: [PATCH 5/9] Send libraryItemId and thumbnail with push notifications --- packages/api/src/jobs/trigger_rule.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/api/src/jobs/trigger_rule.ts b/packages/api/src/jobs/trigger_rule.ts index 3e73d4661..354fd3d82 100644 --- a/packages/api/src/jobs/trigger_rule.ts +++ b/packages/api/src/jobs/trigger_rule.ts @@ -70,6 +70,8 @@ const sendNotification = async (obj: RuleActionObj) => { const message = { title: item.author || item.siteName || 'Omnivore', body: item.title, + libraryItemId: item.id, + thumbnail: item.thumbnail, } return sendPushNotifications(obj.userId, message, 'rule') From 7d77291f1165bd7f128b6d8c503c19d146d725aa Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 8 Mar 2024 17:15:59 +0800 Subject: [PATCH 6/9] Push configuration on iOS --- .../PushNotificationSettingsView.swift | 169 +++++++++++++++--- .../App/Views/Profile/SubscriptionsView.swift | 21 ++- .../Mutations/SetRuleMutation.swift | 47 +++++ apple/Sources/PushNotificationConfig.swift | 2 +- packages/api/src/jobs/trigger_rule.ts | 4 +- 5 files changed, 212 insertions(+), 31 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/PushNotificationSettingsView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/PushNotificationSettingsView.swift index df3be30d7..68082a4af 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/PushNotificationSettingsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/PushNotificationSettingsView.swift @@ -4,47 +4,120 @@ import SwiftUI import Utils import Views + import Transmission - @MainActor final class PushNotificationSettingsViewModel: ObservableObject { - @Published var isLoading = false - @Published var emails = [NewsletterEmail]() - @Published var desiredNotificationsEnabled = false - @AppStorage(UserDefaultKey.notificationsEnabled.rawValue) var notificationsEnabled = false +@MainActor final class PushNotificationSettingsViewModel: ObservableObject { + @Published var isLoading = false + @Published var isLoadingRules = true - func checkPushNotificationsStatus() { - UNUserNotificationCenter.current().getNotificationSettings { settings in - DispatchQueue.main.async { - self.desiredNotificationsEnabled = settings.alertSetting == UNNotificationSetting.enabled - } + @Published var emails = [NewsletterEmail]() + @Published var desiredNotificationsEnabled = false + @Published var allSubscriptionsNotificationRule: Rule? + @Published var hasSubscriptionsNotifyRule = false + @Published var showOperationToast = false + @Published var operationStatus: OperationStatus = .none + @Published var operationMessage: String? + + let subscriptionRuleName = "system.autoNotify.subscriptions" + + @AppStorage(UserDefaultKey.notificationsEnabled.rawValue) var notificationsEnabled = false + + func checkPushNotificationsStatus() { + UNUserNotificationCenter.current().getNotificationSettings { settings in + DispatchQueue.main.async { + self.desiredNotificationsEnabled = settings.alertSetting == UNNotificationSetting.enabled } } + } - func tryUpdateToDesired(dataService: DataService) { - UserDefaults.standard.set(desiredNotificationsEnabled, forKey: UserDefaultKey.notificationsEnabled.rawValue) + func tryUpdateToDesired(dataService: DataService) { + UserDefaults.standard.set(desiredNotificationsEnabled, forKey: UserDefaultKey.notificationsEnabled.rawValue) - if desiredNotificationsEnabled { - UNUserNotificationCenter.current().requestAuthorization(options: [.alert]) { granted, _ in - DispatchQueue.main.async { - self.desiredNotificationsEnabled = granted - Task { - if let savedToken = UserDefaults.standard.string(forKey: UserDefaultKey.firebasePushToken.rawValue) { - _ = try? await dataService.syncDeviceToken( - deviceTokenOperation: DeviceTokenOperation.addToken(token: savedToken)) - } - NotificationCenter.default.post(name: Notification.Name("ReconfigurePushNotifications"), object: nil) + if desiredNotificationsEnabled { + UNUserNotificationCenter.current().requestAuthorization(options: [.alert]) { granted, _ in + DispatchQueue.main.async { + self.desiredNotificationsEnabled = granted + Task { + if let savedToken = UserDefaults.standard.string(forKey: UserDefaultKey.firebasePushToken.rawValue) { + _ = try? await dataService.syncDeviceToken( + deviceTokenOperation: DeviceTokenOperation.addToken(token: savedToken)) } + NotificationCenter.default.post(name: Notification.Name("ReconfigurePushNotifications"), object: nil) } } - } else { - if let tokenID = UserDefaults.standard.string(forKey: UserDefaultKey.deviceTokenID.rawValue) { - Task { - try? await Services().dataService.syncDeviceToken(deviceTokenOperation: .deleteToken(tokenID: tokenID)) - } + } + } else { + if let tokenID = UserDefaults.standard.string(forKey: UserDefaultKey.deviceTokenID.rawValue) { + Task { + try? await Services().dataService.syncDeviceToken(deviceTokenOperation: .deleteToken(tokenID: tokenID)) } } } } + func loadRule(dataService: DataService) async { + do { + let rule = try await dataService.rules().filter { $0.name == subscriptionRuleName }.first + setAllSubscriptionRule(rule: rule) + } catch { + print("error fetching", error) + setAllSubscriptionRule(rule: nil) + } + } + + func setAllSubscriptionRule(rule: Rule?) { + allSubscriptionsNotificationRule = rule + hasSubscriptionsNotifyRule = allSubscriptionsNotificationRule != nil + isLoadingRules = false + } + + func createSubscriptionNotificationRule(dataService: DataService) { + if allSubscriptionsNotificationRule != nil { + return + } + + operationMessage = "Creating notification rule..." + operationStatus = .isPerforming + showOperationToast = true + + Task { + do { + let rule = try await dataService.createNotificationRule( + name: subscriptionRuleName, + filter: "in:all has:subscription" + ) + setAllSubscriptionRule(rule: rule) + operationMessage = "Rule created" + operationStatus = .success + } catch { + print("error creating notification rule: ", error) + operationMessage = "Failed to create notification rule" + operationStatus = .failure + } + } + } + + func deleteSubscriptionNotificationRule(dataService: DataService) { + operationMessage = "Creating label rule..." + operationStatus = .isPerforming + showOperationToast = true + + Task { + do { + if let allSubscriptionsNotificationRule = allSubscriptionsNotificationRule { + _ = try await dataService.deleteRule(ruleID: allSubscriptionsNotificationRule.id) + setAllSubscriptionRule(rule: nil) + operationMessage = "Notification rule deleted" + operationStatus = .success + } + } catch { + operationMessage = "Failed to create label rule" + operationStatus = .failure + } + } + } +} + struct PushNotificationSettingsView: View { @EnvironmentObject var dataService: DataService @Environment(\.dismiss) private var dismiss @@ -54,6 +127,12 @@ var body: some View { Group { + WindowLink(level: .alert, transition: .move(edge: .bottom), isPresented: $viewModel.showOperationToast) { + OperationToast(operationMessage: $viewModel.operationMessage, showOperationToast: $viewModel.showOperationToast, operationStatus: $viewModel.operationStatus) + } label: { + EmptyView() + }.buttonStyle(.plain) + #if os(iOS) Form { innerBody @@ -68,7 +147,10 @@ .onReceive(NotificationCenter.default.publisher(for: Notification.Name("ScrollToTop"))) { _ in dismiss() } - .task { viewModel.checkPushNotificationsStatus() } + .task { + viewModel.checkPushNotificationsStatus() + await viewModel.loadRule(dataService: dataService) + } } private var notificationsText: some View { @@ -84,6 +166,14 @@ .accentColor(.blue) } + private var rulesSection: some View { + if viewModel.isLoadingRules { + AnyView(EmptyView()) + } else { + AnyView(Toggle("Notify me when new items arrive from my subscriptions", isOn: $viewModel.hasSubscriptionsNotifyRule)) + } + } + private var innerBody: some View { Group { Section { @@ -96,12 +186,35 @@ notificationsText } + Section { + rulesSection + } + Section { NavigationLink("Devices") { PushNotificationDevicesView() } } } + .onChange(of: viewModel.hasSubscriptionsNotifyRule) { newValue in + print("has notification rule: \(newValue)") + if viewModel.isLoadingRules { + return + } + + if newValue { + viewModel.createSubscriptionNotificationRule(dataService: dataService) + } else { + viewModel.deleteSubscriptionNotificationRule(dataService: dataService) + } + } + .onChange(of: viewModel.operationStatus) { newValue in + if newValue == .success || newValue == .failure { + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(2000)) { + viewModel.showOperationToast = false + } + } + } .navigationTitle(LocalText.pushNotificationsGeneric) } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/SubscriptionsView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/SubscriptionsView.swift index 0f6507bfc..197cbee59 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/SubscriptionsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/SubscriptionsView.swift @@ -273,7 +273,7 @@ struct SubscriptionsView: View { await viewModel.cancelSubscription(dataService: dataService, subscription: subscription) } } - ) + ).background(Color.systemBackground) } label: { SubscriptionCell(subscription: subscription) } @@ -526,6 +526,25 @@ struct SubscriptionSettingsView: View { } } } +// +// var notificationRuleRow: some View { +// HStack { +// Text("Add Labels") +// Spacer() +// if isLoadingRule || viewModel.rules != nil { +// Button(action: { showLabelsSelector = true }, label: { +// if let ruleLabels = ruleLabels { +// let labelNames = ruleLabels.map(\.unwrappedName) +// Text("[\(labelNames.joined(separator: ","))]") +// } else { +// Text("Create Rule") +// } +// }).tint(Color.blue) +// } else { +// ProgressView() +// } +// } +// } var body: some View { VStack { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SetRuleMutation.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SetRuleMutation.swift index 48dfc16f5..a59fd43e8 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SetRuleMutation.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SetRuleMutation.swift @@ -89,4 +89,51 @@ public extension DataService { } } } + + func createNotificationRule(name: String, filter: String) async throws -> Rule { + enum MutationResult { + case result(rule: Rule) + case error(errorMessage: String) + } + + let selection = Selection { + try $0.on( + setRuleError: .init { .error(errorMessage: try $0.errorCodes().first?.rawValue ?? "Unknown Error") }, + setRuleSuccess: .init { .result(rule: try $0.rule(selection: ruleSelection)) } + ) + } + + let mutation = Selection.Mutation { + try $0.setRule( + input: InputObjects.SetRuleInput( + actions: [InputObjects.RuleActionInput(params: [], type: .sendNotification)], + enabled: true, + eventTypes: [.pageCreated], + filter: filter, + id: OptionalArgument(nil), + name: name + ), + selection: selection + ) + } + + let path = appEnvironment.graphqlPath + let headers = networker.defaultHeaders + + return try await withCheckedThrowingContinuation { continuation in + send(mutation, to: path, headers: headers) { queryResult in + guard let payload = try? queryResult.get() else { + continuation.resume(throwing: BasicError.message(messageText: "network error")) + return + } + + switch payload.data { + case let .result(rule: rule): + continuation.resume(returning: rule) + case let .error(errorMessage: errorMessage): + continuation.resume(throwing: BasicError.message(messageText: errorMessage)) + } + } + } + } } diff --git a/apple/Sources/PushNotificationConfig.swift b/apple/Sources/PushNotificationConfig.swift index 71ec6cbf0..d6588b779 100644 --- a/apple/Sources/PushNotificationConfig.swift +++ b/apple/Sources/PushNotificationConfig.swift @@ -55,7 +55,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate { let userInfo = notification.request.content.userInfo UIApplication.shared.applicationIconBadgeNumber = 0 - print(userInfo) // extract data sent along with PN + print("push data", userInfo) // extract data sent along with PN completionHandler([[.banner, .sound]]) } diff --git a/packages/api/src/jobs/trigger_rule.ts b/packages/api/src/jobs/trigger_rule.ts index 354fd3d82..813a67526 100644 --- a/packages/api/src/jobs/trigger_rule.ts +++ b/packages/api/src/jobs/trigger_rule.ts @@ -70,8 +70,10 @@ const sendNotification = async (obj: RuleActionObj) => { const message = { title: item.author || item.siteName || 'Omnivore', body: item.title, + image: item.thumbnail, + } + const data = { libraryItemId: item.id, - thumbnail: item.thumbnail, } return sendPushNotifications(obj.userId, message, 'rule') From 28aa9a468d582cd1e287a0d47657cb4f5d77a1df Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 8 Mar 2024 17:23:38 +0800 Subject: [PATCH 7/9] Reduce toast display time --- .../App/Views/Profile/PushNotificationSettingsView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/PushNotificationSettingsView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/PushNotificationSettingsView.swift index 68082a4af..aaf50a9cd 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/PushNotificationSettingsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/PushNotificationSettingsView.swift @@ -210,7 +210,7 @@ } .onChange(of: viewModel.operationStatus) { newValue in if newValue == .success || newValue == .failure { - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(2000)) { + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1000)) { viewModel.showOperationToast = false } } From e093738dcc7d14b3b4391ab0a2945fd166ad92e9 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 8 Mar 2024 17:24:02 +0800 Subject: [PATCH 8/9] Send libraryItemId with push notifications --- packages/api/src/jobs/trigger_rule.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/src/jobs/trigger_rule.ts b/packages/api/src/jobs/trigger_rule.ts index 813a67526..2ada36242 100644 --- a/packages/api/src/jobs/trigger_rule.ts +++ b/packages/api/src/jobs/trigger_rule.ts @@ -76,7 +76,7 @@ const sendNotification = async (obj: RuleActionObj) => { libraryItemId: item.id, } - return sendPushNotifications(obj.userId, message, 'rule') + return sendPushNotifications(obj.userId, message, 'rule', data) } const getRuleAction = (actionType: RuleActionType): RuleActionFunc => { From 50a7315185a7a11582adb48da2cbea32e29b931e Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 8 Mar 2024 17:41:49 +0800 Subject: [PATCH 9/9] Fix confirmation modal --- .../components/patterns/ConfirmationModal.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/web/components/patterns/ConfirmationModal.tsx b/packages/web/components/patterns/ConfirmationModal.tsx index a82a9677f..25618fdbe 100644 --- a/packages/web/components/patterns/ConfirmationModal.tsx +++ b/packages/web/components/patterns/ConfirmationModal.tsx @@ -19,8 +19,18 @@ type ConfirmationModalProps = { } export function ConfirmationModal(props: ConfirmationModalProps): JSX.Element { + const safeOnOpenChange = useCallback( + (open: boolean) => { + props.onOpenChange(open) + setTimeout(() => { + document.body.style.removeProperty('pointer-events') + }, 200) + }, + [props] + ) + return ( - + @@ -46,11 +56,15 @@ export function ConfirmationModal(props: ConfirmationModalProps): JSX.Element {