Add badge counts, fix logout and delete item navigation issues

This commit is contained in:
Jackson Harper
2023-12-13 12:03:46 +08:00
parent d553352ddd
commit a4976a7b62
16 changed files with 165 additions and 30 deletions

View File

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

View File

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

View File

@ -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<Models.LibraryItem> = LibraryItem.fetchRequest()
fetchRequest.predicate = internalFilter.predicate
if let count = try? dataService.backgroundContext.count(for: fetchRequest) {
UIApplication.shared.applicationIconBadgeNumber = count
}
}
}
}
}

View File

@ -136,6 +136,8 @@ import Views
updateFetchController(dataService: dataService, filterState: filterState)
}
}
BadgeCountHandler.updateBadgeCount(dataService: dataService)
}
func loadMoreItems(dataService: DataService, filterState: FetcherFilterState, isRefresh: Bool) async {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,6 +11,7 @@ import Views
let isMacApp = true
#endif
@MainActor
public final class RootViewModel: ObservableObject {
let services = Services()

View File

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

View File

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

View File

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

View File

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

View File

@ -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<MutationResult, Unions.SubscribeResult> {
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))
}
)
}

View File

@ -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<Models.Filter> = 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 {