Add badge counts, fix logout and delete item navigation issues
This commit is contained in:
@ -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 = ""
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -136,6 +136,8 @@ import Views
|
||||
updateFetchController(dataService: dataService, filterState: filterState)
|
||||
}
|
||||
}
|
||||
|
||||
BadgeCountHandler.updateBadgeCount(dataService: dataService)
|
||||
}
|
||||
|
||||
func loadMoreItems(dataService: DataService, filterState: FetcherFilterState, isRefresh: Bool) async {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import Views
|
||||
let isMacApp = true
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
public final class RootViewModel: ObservableObject {
|
||||
let services = Services()
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 = [
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user