From a4976a7b62bcb3645574e2c8210d747ed90fb4bd Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 13 Dec 2023 12:03:46 +0800 Subject: [PATCH] Add badge counts, fix logout and delete item navigation issues --- .../Share/ShareExtensionViewModel.swift | 1 + apple/OmnivoreKit/Sources/App/Services.swift | 2 + .../Home/Components/BadgeCountHandler.swift | 32 ++++++++--- .../Home/Components/LibraryItemFetcher.swift | 2 + .../App/Views/Home/HomeFeedViewModel.swift | 4 +- .../App/Views/LinkItemDetailView.swift | 2 +- .../App/Views/Profile/FiltersView.swift | 55 ++++++++++++++++++- .../App/Views/Profile/ProfileView.swift | 6 +- .../Views/Profile/SelectBadgeFilterView.swift | 33 ++++++++--- .../App/Views/RootView/RootViewModel.swift | 1 + .../Views/WebReader/WebReaderContainer.swift | 4 +- .../Services/AudioSession/Voices.swift | 5 +- .../Authentication/Authenticator.swift | 4 +- .../DataService/Mutations/ArchiveLink.swift | 2 +- .../Mutations/SubscribeToFeed.swift | 10 +++- .../InternalModels/InternalFilter.swift | 32 ++++++++++- 16 files changed, 165 insertions(+), 30 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift index a2759fea8..7f5ad4aff 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift @@ -5,6 +5,7 @@ import SwiftUI import Utils import Views +@MainActor public class ShareExtensionViewModel: ObservableObject { @Published public var status: ShareExtensionStatus = .processing @Published public var title: String = "" diff --git a/apple/OmnivoreKit/Sources/App/Services.swift b/apple/OmnivoreKit/Sources/App/Services.swift index 9015e4f20..9ad72a7bc 100644 --- a/apple/OmnivoreKit/Sources/App/Services.swift +++ b/apple/OmnivoreKit/Sources/App/Services.swift @@ -5,6 +5,7 @@ import OSLog import Services import Utils +@MainActor public final class Services { static let fetchTaskID = "app.omnivore.fetchLinkedItems" static let secondsToWaitBeforeNextBackgroundRefresh: TimeInterval = isDebug ? 0 : 3600 // 1 hour @@ -84,6 +85,7 @@ public final class Services { Task { do { let fetchedItemCount = try await services.dataService.fetchLinkedItemsBackgroundTask() + BadgeCountHandler.updateBadgeCount(dataService: services.dataService) task.setTaskCompleted(success: true) } catch { EventTracker.track( diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/Components/BadgeCountHandler.swift b/apple/OmnivoreKit/Sources/App/Views/Home/Components/BadgeCountHandler.swift index 2dd0e276c..8de7cd9a5 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/Components/BadgeCountHandler.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/Components/BadgeCountHandler.swift @@ -1,8 +1,26 @@ -// -// File.swift -// -// -// Created by Jackson Harper on 12/13/23. -// - +import CoreData import Foundation +import Models +import Services +import SwiftUI + +@MainActor +enum BadgeCountHandler { + @AppStorage("Filters::badgeFilter") public static var badgeFilter = "in:inbox" + + public static func updateBadgeCount(dataService: DataService) { + // if let badgeFilterId = badgeFilterId { + dataService.backgroundContext.performAndWait { + if let filter = Filter.lookup(byFilter: badgeFilter, inContext: dataService.backgroundContext), + let internalFilter = InternalFilter.make(from: [filter]).first + { + let fetchRequest: NSFetchRequest = LibraryItem.fetchRequest() + fetchRequest.predicate = internalFilter.predicate + + if let count = try? dataService.backgroundContext.count(for: fetchRequest) { + UIApplication.shared.applicationIconBadgeNumber = count + } + } + } + } +} diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryItemFetcher.swift b/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryItemFetcher.swift index 23fbd8f21..a03aa33ff 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryItemFetcher.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryItemFetcher.swift @@ -136,6 +136,8 @@ import Views updateFetchController(dataService: dataService, filterState: filterState) } } + + BadgeCountHandler.updateBadgeCount(dataService: dataService) } func loadMoreItems(dataService: DataService, filterState: FetcherFilterState, isRefresh: Bool) async { diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 6ad51d0f1..24eb86328 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -144,7 +144,7 @@ import Views filters = newFilters .filter { $0.folder == filterState.folder } .sorted(by: { $0.position < $1.position }) - + (folder == "inbox" ? [InternalFilter.DeletedFilter, InternalFilter.DownloadedFilter] : [InternalFilter.DownloadedFilter]) + + (folder == "inbox" ? [InternalFilter.UnreadFilter, InternalFilter.DeletedFilter, InternalFilter.DownloadedFilter] : [InternalFilter.DownloadedFilter]) if let newFilter = filters.first(where: { $0.name.lowercased() == appliedFilterName }), newFilter.id != appliedFilter?.id { appliedFilter = newFilter @@ -186,7 +186,7 @@ import Views func setLinkArchived(dataService: DataService, objectID: NSManagedObjectID, archived: Bool) { dataService.archiveLink(objectID: objectID, archived: archived) - snackbar(archived ? "Link archived" : "Link moved to Inbox") + snackbar(archived ? "Link archived" : "Link unarchived") } func removeLibraryItem(dataService: DataService, objectID: NSManagedObjectID) { diff --git a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift index d3b66c479..44b35f264 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift @@ -25,7 +25,7 @@ import Views func handleArchiveAction(dataService: DataService) { guard let objectID = item?.objectID ?? pdfItem?.objectID else { return } dataService.archiveLink(objectID: objectID, archived: !isItemArchived) - showInLibrarySnackbar(!isItemArchived ? "Link archived" : "Link moved to Inbox") + showInLibrarySnackbar(!isItemArchived ? "Link archived" : "Link unarchived") } func handleDeleteAction(dataService: DataService) { diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/FiltersView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/FiltersView.swift index 66214abf9..51272f997 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/FiltersView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/FiltersView.swift @@ -1,4 +1,3 @@ - import Models import Services import SwiftUI @@ -9,8 +8,16 @@ import Views @Published var isLoading = false @Published var isCreating = false @Published var networkError = false + + @Published var hasBadgePermission = false @Published var libraryFilters = [InternalFilter]() + @Published var badgeFilter = BadgeCountHandler.badgeFilter { + didSet { + BadgeCountHandler.badgeFilter = badgeFilter + } + } + @AppStorage("LibraryTabView::hideFollowingTab") var hideFollowingTab = false @AppStorage(UserDefaultKey.hideFeatureSection.rawValue) var hideFeatureSection = false @@ -25,6 +32,28 @@ import Views isLoading = false } + + func loadBadgePermission() { + UNUserNotificationCenter.current().getNotificationSettings { settings in + DispatchQueue.main.async { + if settings.badgeSetting == .enabled { + self.hasBadgePermission = true + } else { + self.hasBadgePermission = false + } + print("notification settings: ", settings.badgeSetting.rawValue) + print("got the notification settings") + } + } + } + + func requestBadgePermission() { + UNUserNotificationCenter.current().requestAuthorization(options: UNAuthorizationOptions.badge) { success, error in + DispatchQueue.main.async { + print("requested badge permission: ", success, error) + } + } + } } struct FiltersView: View { @@ -45,12 +74,15 @@ struct FiltersView: View { #endif } .navigationTitle(LocalText.filtersGeneric) - .task { await viewModel.loadFilters(dataService: dataService) } + .task { + viewModel.loadBadgePermission() + await viewModel.loadFilters(dataService: dataService) + } } private var innerBody: some View { List { - Section { + Section(header: Text("User Interface")) { Toggle("Hide following tab", isOn: $viewModel.hideFollowingTab) Toggle("Hide feature section", isOn: $viewModel.hideFeatureSection) } @@ -60,6 +92,23 @@ struct FiltersView: View { Text(filter.name) } } + + Section(header: Text("Application Badge")) { + Toggle("Display Badge Count", isOn: $viewModel.hasBadgePermission) + .onChange(of: viewModel.hasBadgePermission) { _ in + if viewModel.hasBadgePermission { + viewModel.requestBadgePermission() + } + } + + if viewModel.hasBadgePermission { + NavigationLink(destination: { + SelectBadgeFilterView(viewModel: viewModel) + }, label: { + Text(viewModel.badgeFilter) + }) + } + } } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift index b52dd980c..c489f8a14 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift @@ -25,8 +25,10 @@ import Views loadProfileCardData(name: name, username: username, profileImageURL: currentViewer.profileImageURL) } - if let viewer = try? await dataService.fetchViewer() { - loadProfileCardData(name: viewer.name, username: viewer.username, profileImageURL: viewer.profileImageURL) + if profileCardData.name.isEmpty { + if let viewer = try? await dataService.fetchViewer() { + loadProfileCardData(name: viewer.name, username: viewer.username, profileImageURL: viewer.profileImageURL) + } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/SelectBadgeFilterView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/SelectBadgeFilterView.swift index b79656683..c6ccaf34a 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/SelectBadgeFilterView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/SelectBadgeFilterView.swift @@ -1,17 +1,36 @@ import Foundation +import Models +import Services import SwiftUI -struct SelectBadgeFilterView: View { +@MainActor +public struct SelectBadgeFilterView: View { @ObservedObject var viewModel: FiltersViewModel - var body: some View { + public var body: some View { List { - ForEach(viewModel.libraryFilters) { filter in - HStack { - Text(filter.name) - Spacer() + Section(header: Text("Filter")) { + ForEach(viewModel.libraryFilters) { filter in + Button { + viewModel.badgeFilter = filter.filter + } label: { + HStack { + Text(filter.name) + Spacer() + if isSelected(filter) { + Image(systemName: "checkmark") + } + }.onTapGesture {} + }.contentShape(Rectangle()) } } - } + Section { + Text("Your selected filter will be used to display a badge value on the application icon.") + } + }.navigationTitle("Badge Filter") + } + + func isSelected(_ filter: InternalFilter) -> Bool { + viewModel.badgeFilter == filter.filter } } diff --git a/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift index a058c71b7..bef3729c5 100644 --- a/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift @@ -11,6 +11,7 @@ import Views let isMacApp = true #endif +@MainActor public final class RootViewModel: ObservableObject { let services = Services() diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index 1ea09ef87..a5d95f026 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -647,10 +647,10 @@ struct WebReaderContainerView: View { } func delete() { - removeLibraryItemAction(dataService: dataService, objectID: item.objectID) + pop() #if os(iOS) DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { - pop() + removeLibraryItemAction(dataService: dataService, objectID: item.objectID) } #endif } diff --git a/apple/OmnivoreKit/Sources/Services/AudioSession/Voices.swift b/apple/OmnivoreKit/Sources/Services/AudioSession/Voices.swift index 56750302f..c6a0da2a1 100644 --- a/apple/OmnivoreKit/Sources/Services/AudioSession/Voices.swift +++ b/apple/OmnivoreKit/Sources/Services/AudioSession/Voices.swift @@ -35,6 +35,7 @@ public enum VoiceCategory: String, CaseIterable { case itIT = "Italian (Italy)" case esES = "Spanish (Spain)" case jaJP = "Japanese (Japan)" + case nlNL = "Dutch (Netherlands)" case ptBR = "Portuguese (Brazil)" case taIN = "Tamil (India)" case taLK = "Tamil (Sri Lanka)" @@ -80,6 +81,7 @@ public enum Voices { VoiceLanguage(key: "it", name: "Italian", defaultVoice: "it-IT-BenignoNeural", categories: [.itIT]), VoiceLanguage(key: "ja", name: "Japanese", defaultVoice: "ja-JP-NanamiNeural", categories: [.jaJP]), VoiceLanguage(key: "es", name: "Spanish", defaultVoice: "es-ES-AlvaroNeural", categories: [.esES]), + VoiceLanguage(key: "nl", name: "Dutch", defaultVoice: "nl-NL-XiaochenNeural", categories: [.nlNL]), VoiceLanguage(key: "pt", name: "Portuguese", defaultVoice: "pt-BR-AntonioNeural", categories: [.ptBR]), VoiceLanguage(key: "ta", name: "Tamil", defaultVoice: "ta-IN-PallaviNeural", categories: [.taIN, .taLK, .taMY, .taSG]) ] @@ -113,7 +115,8 @@ public enum Voices { VoicePair(firstKey: "ta-LK-KumarNeural", secondKey: "ta-LK-SaranyaNeural", firstName: "Kumar", secondName: "Saranya", language: "ta-LK", category: .taLK), VoicePair(firstKey: "ta-MY-KaniNeural", secondKey: "ta-MY-SuryaNeural", firstName: "Kani", secondName: "Surya", language: "ta-MY", category: .taMY), VoicePair(firstKey: "ta-SG-AnbuNeural", secondKey: "ta-SG-VenbaNeural", firstName: "Anbu", secondName: "Venba", language: "ta-SG", category: .taSG), - VoicePair(firstKey: "it-IT-BenignoNeural", secondKey: "it-IT-IsabellaNeural", firstName: "Benigno", secondName: "Isabella", language: "it-IT", category: .itIT) + VoicePair(firstKey: "it-IT-BenignoNeural", secondKey: "it-IT-IsabellaNeural", firstName: "Benigno", secondName: "Isabella", language: "it-IT", category: .itIT), + VoicePair(firstKey: "nl-NL-MaartenNeural", secondKey: "nl-NL-FennaNeural", firstName: "Maarten", secondName: "Fenna", language: "nl-NL", category: .nlNL) ] public static let UltraPairs = [ diff --git a/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift b/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift index 7ac953c79..6ef7df88a 100644 --- a/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift +++ b/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift @@ -38,12 +38,14 @@ public final class Authenticator: ObservableObject { } public func logout(dataService: DataService, isAccountDeletion: Bool = false) { - dataService.resetLocalStorage() clearCreds() Authenticator.unregisterIntercomUser?() isLoggedIn = false showAppleRevokeTokenAlert = isAccountDeletion EventTracker.reset() + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) { + dataService.resetLocalStorage() + } } public func clearCreds() { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/ArchiveLink.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/ArchiveLink.swift index a2355cff8..a70c63ef9 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/ArchiveLink.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/ArchiveLink.swift @@ -15,7 +15,7 @@ extension DataService { // Send update to server self.syncLinkArchiveStatus(itemID: linkedItem.unwrappedID, archived: archived) - let message = archived ? "Link archived" : "Link moved to Inbox" + let message = archived ? "Link archived" : "Link unarchived" DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) { showInLibrarySnackbar(message) } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SubscribeToFeed.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SubscribeToFeed.swift index 4e4520700..e77845e82 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SubscribeToFeed.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SubscribeToFeed.swift @@ -5,16 +5,22 @@ import SwiftGraphQL public extension DataService { func subscribeToFeed(feedURL: String) async throws -> Bool { enum MutationResult { - case success(subscriptions: [Subscription]) + case success(subscriptionIds: [String]) case error(errorMessage: String) } + let subscriptionIdSelection = Selection.Subscription { + try $0.id() + } + let selection = Selection { try $0.on( subscribeError: .init { .error(errorMessage: try $0.errorCodes().first?.rawValue ?? "unknown error") }, - subscribeSuccess: .init { .success(subscriptions: try $0.subscriptions(selection: subscriptionSelection.list)) } + subscribeSuccess: .init { + .success(subscriptionIds: try $0.subscriptions(selection: subscriptionIdSelection.list)) + } ) } diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift index 855bd30e5..bd88280aa 100644 --- a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift +++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift @@ -39,6 +39,18 @@ public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable { ) } + public static var UnreadFilter: InternalFilter { + InternalFilter( + id: "unread", + name: "Unread", + folder: "inbox", + filter: "in:inbox is:unread", + visible: true, + position: -1, + defaultFilter: true + ) + } + public static var DefaultInboxFilters: [InternalFilter] { [ InternalFilter( @@ -177,8 +189,12 @@ public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable { case "Following": return NSCompoundPredicate(andPredicateWithSubpredicates: [folderPredicate, notInArchivePredicate, undeletedPredicate]) case "Inbox": - // non-archived items return NSCompoundPredicate(andPredicateWithSubpredicates: [folderPredicate, undeletedPredicate, notInArchivePredicate]) + case "Unread": + let isUnread = NSPredicate( + format: "readAt == nil" + ) + return NSCompoundPredicate(andPredicateWithSubpredicates: [folderPredicate, undeletedPredicate, notInArchivePredicate, isUnread]) case "Non-Feed Items": // non-archived or deleted items without the Newsletter label let nonNewsletterLabelPredicate = NSPredicate( @@ -325,6 +341,20 @@ public extension Filter { return filter } + + static func lookup(byFilter filter: String, inContext context: NSManagedObjectContext) -> Filter? { + let fetchRequest: NSFetchRequest = Filter.fetchRequest() + fetchRequest.predicate = NSPredicate( + format: "filter == %@", filter + ) + + var filter: Filter? + context.performAndWait { + filter = (try? context.fetch(fetchRequest))?.first + } + + return filter + } } extension Sequence where Element == InternalFilter {