diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents index 25579daa..bf87198e 100644 --- a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents +++ b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents @@ -155,13 +155,14 @@ - + + @@ -202,7 +203,7 @@ - + @@ -212,7 +213,7 @@ - + @@ -244,10 +245,10 @@ + - - + \ No newline at end of file diff --git a/CoreDataStack/Entity/Setting.swift b/CoreDataStack/Entity/Setting.swift index a4907f0a..671f9bab 100644 --- a/CoreDataStack/Entity/Setting.swift +++ b/CoreDataStack/Entity/Setting.swift @@ -8,11 +8,11 @@ import CoreData import Foundation -@objc(Setting) public final class Setting: NSManagedObject { @NSManaged public var appearance: String? @NSManaged public var triggerBy: String? @NSManaged public var domain: String? + @NSManaged public var userID: String? @NSManaged public private(set) var createdAt: Date @NSManaged public private(set) var updatedAt: Date @@ -40,6 +40,7 @@ public extension Setting { setting.appearance = property.appearance setting.triggerBy = property.triggerBy setting.domain = property.domain + setting.userID = property.userID return setting } @@ -61,11 +62,13 @@ public extension Setting { public let appearance: String public let triggerBy: String public let domain: String + public let userID: String - public init(appearance: String, triggerBy: String, domain: String) { + public init(appearance: String, triggerBy: String, domain: String, userID: String) { self.appearance = appearance self.triggerBy = triggerBy self.domain = domain + self.userID = userID } } } @@ -77,8 +80,11 @@ extension Setting: Managed { } extension Setting { - public static func predicate(domain: String) -> NSPredicate { - return NSPredicate(format: "%K == %@", #keyPath(Setting.domain), domain) + public static func predicate(domain: String, userID: String) -> NSPredicate { + return NSPredicate(format: "%K == %@ AND %K == %@", + #keyPath(Setting.domain), domain, + #keyPath(Setting.userID), userID + ) } } diff --git a/CoreDataStack/Entity/Subscription.swift b/CoreDataStack/Entity/Subscription.swift index 7d7a7457..8ced945d 100644 --- a/CoreDataStack/Entity/Subscription.swift +++ b/CoreDataStack/Entity/Subscription.swift @@ -9,7 +9,6 @@ import Foundation import CoreData -@objc(Subscription) public final class Subscription: NSManagedObject { @NSManaged public var id: String @NSManaged public var endpoint: String @@ -95,8 +94,8 @@ extension Subscription: Managed { extension Subscription { - public static func predicate(id: String) -> NSPredicate { - return NSPredicate(format: "%K == %@", #keyPath(Subscription.id), id) + public static func predicate(type: String) -> NSPredicate { + return NSPredicate(format: "%K == %@", #keyPath(Subscription.type), type) } } diff --git a/CoreDataStack/Entity/SubscriptionAlerts.swift b/CoreDataStack/Entity/SubscriptionAlerts.swift index d1169104..f5abf495 100644 --- a/CoreDataStack/Entity/SubscriptionAlerts.swift +++ b/CoreDataStack/Entity/SubscriptionAlerts.swift @@ -20,7 +20,7 @@ public final class SubscriptionAlerts: NSManagedObject { @NSManaged public private(set) var updatedAt: Date // MARK: - relationships - @NSManaged public var pushSubscription: Subscription? + @NSManaged public var subscription: Subscription? } public extension SubscriptionAlerts { diff --git a/Mastodon/Scene/Settings/SettingsViewController.swift b/Mastodon/Scene/Settings/SettingsViewController.swift index a9b1f0b8..4615f92a 100644 --- a/Mastodon/Scene/Settings/SettingsViewController.swift +++ b/Mastodon/Scene/Settings/SettingsViewController.swift @@ -46,7 +46,7 @@ class SettingsViewController: UIViewController, NeedsDependency { UIAction(title: noOne, image: UIImage(systemName: "nosign"), attributes: []) { [weak self] action in self?.updateTrigger(by: noOne) }, - ].reversed() + ] ) return menu } @@ -344,10 +344,15 @@ extension SettingsViewController: UITableViewDelegate { // Update setting into core data extension SettingsViewController { func updateTrigger(by who: String) { + guard self.viewModel.triggerBy != who else { return } guard let setting = self.viewModel.setting.value else { return } - _ = context.managedObjectContext.performChanges { - setting.update(triggerBy: who) + setting.update(triggerBy: who) + // trigger to call `subscription` API with POST method + // confirm the local data is correct even if request failed + // The asynchronous execution is to solve the problem of dropped frames for animations. + DispatchQueue.main.async { [weak self] in + self?.viewModel.setting.value = setting } } @@ -356,34 +361,35 @@ extension SettingsViewController { guard let settings = self.viewModel.setting.value else { return } guard let triggerBy = settings.triggerBy else { return } - guard let alerts = settings.subscription?.first(where: { (s) -> Bool in + if let alerts = settings.subscription?.first(where: { (s) -> Bool in return s.type == settings.triggerBy - })?.alert else { - return + })?.alert { + var alertValues = [Bool?]() + alertValues.append(alerts.favourite?.boolValue) + alertValues.append(alerts.follow?.boolValue) + alertValues.append(alerts.reblog?.boolValue) + alertValues.append(alerts.mention?.boolValue) + + // need to update `alerts` to make update API with correct parameter + switch title { + case L10n.Scene.Settings.Section.Notifications.favorites: + alertValues[0] = isOn + alerts.favourite = NSNumber(booleanLiteral: isOn) + case L10n.Scene.Settings.Section.Notifications.follows: + alertValues[1] = isOn + alerts.follow = NSNumber(booleanLiteral: isOn) + case L10n.Scene.Settings.Section.Notifications.boosts: + alertValues[2] = isOn + alerts.reblog = NSNumber(booleanLiteral: isOn) + case L10n.Scene.Settings.Section.Notifications.mentions: + alertValues[3] = isOn + alerts.mention = NSNumber(booleanLiteral: isOn) + default: break + } + self.viewModel.updateSubscriptionSubject.send((triggerBy: triggerBy, values: alertValues)) + } else if let alertValues = self.viewModel.notificationDefaultValue[triggerBy] { + self.viewModel.updateSubscriptionSubject.send((triggerBy: triggerBy, values: alertValues)) } - var alertValues = [Bool?]() - alertValues.append(alerts.favourite?.boolValue) - alertValues.append(alerts.follow?.boolValue) - alertValues.append(alerts.reblog?.boolValue) - alertValues.append(alerts.mention?.boolValue) - - // need to update `alerts` to make update API with correct parameter - switch title { - case L10n.Scene.Settings.Section.Notifications.favorites: - alertValues[0] = isOn - alerts.favourite = NSNumber(booleanLiteral: isOn) - case L10n.Scene.Settings.Section.Notifications.follows: - alertValues[1] = isOn - alerts.follow = NSNumber(booleanLiteral: isOn) - case L10n.Scene.Settings.Section.Notifications.boosts: - alertValues[2] = isOn - alerts.reblog = NSNumber(booleanLiteral: isOn) - case L10n.Scene.Settings.Section.Notifications.mentions: - alertValues[3] = isOn - alerts.mention = NSNumber(booleanLiteral: isOn) - default: break - } - self.viewModel.updateSubscriptionSubject.send((triggerBy: triggerBy, values: alertValues)) } } @@ -435,7 +441,7 @@ extension SettingsViewController { guard let setting: Setting? = { let domain = box.domain let request = Setting.sortedFetchRequest - request.predicate = Setting.predicate(domain: domain) + request.predicate = Setting.predicate(domain: domain, userID: box.userID) request.fetchLimit = 1 request.returnsObjectsAsFaults = false do { diff --git a/Mastodon/Scene/Settings/SettingsViewModel.swift b/Mastodon/Scene/Settings/SettingsViewModel.swift index b61334f4..470617ae 100644 --- a/Mastodon/Scene/Settings/SettingsViewModel.swift +++ b/Mastodon/Scene/Settings/SettingsViewModel.swift @@ -29,7 +29,7 @@ class SettingsViewModel: NSObject, NeedsDependency { if let box = self.context.authenticationService.activeMastodonAuthenticationBox.value { let domain = box.domain - fetchRequest.predicate = Setting.predicate(domain: domain) + fetchRequest.predicate = Setting.predicate(domain: domain, userID: box.userID) } fetchRequest.fetchLimit = 1 @@ -78,6 +78,9 @@ class SettingsViewModel: NSObject, NeedsDependency { return Mastodon.API.privacyURL(domain: box.domain) }() + /// to store who trigger the notification. + var triggerBy: String? + struct Input { } @@ -121,12 +124,14 @@ class SettingsViewModel: NSObject, NeedsDependency { follow: values[1], reblog: values[2], mention: values[3], - poll: nil) + poll: nil + ) self.context.apiService.changeSubscription( domain: domain, mastodonAuthenticationBox: activeMastodonAuthenticationBox, query: query, - triggerBy: triggerBy + triggerBy: triggerBy, + userID: activeMastodonAuthenticationBox.userID ) .sink { (_) in } receiveValue: { (_) in @@ -164,7 +169,8 @@ class SettingsViewModel: NSObject, NeedsDependency { domain: domain, mastodonAuthenticationBox: activeMastodonAuthenticationBox, query: query, - triggerBy: triggerBy + triggerBy: triggerBy, + userID: activeMastodonAuthenticationBox.userID ) .sink { (_) in } receiveValue: { (_) in @@ -178,13 +184,6 @@ class SettingsViewModel: NSObject, NeedsDependency { // request subsription data for updating or initialization requestSubscription() - - do { - try fetchResultsController.performFetch() - setting.value = fetchResultsController.fetchedObjects?.first - } catch { - assertionFailure(error.localizedDescription) - } return nil } @@ -213,12 +212,12 @@ class SettingsViewModel: NSObject, NeedsDependency { } else if let triggerBy = settings?.triggerBy, let values = self.notificationDefaultValue[triggerBy] { switches = values - self.createSubscriptionSubject.send((triggerBy: triggerBy, values: values)) } else { // fallback a default value let anyone = L10n.Scene.Settings.Section.Notifications.Trigger.anyone switches = self.notificationDefaultValue[anyone] } + let notifications = [L10n.Scene.Settings.Section.Notifications.favorites, L10n.Scene.Settings.Section.Notifications.follows, L10n.Scene.Settings.Section.Notifications.boosts, @@ -273,31 +272,61 @@ class SettingsViewModel: NSObject, NeedsDependency { } private func requestSubscription() { - // request subscription of notifications - typealias SubscriptionResponse = Mastodon.Response.Content - viewDidLoad.flatMap { [weak self] (_) -> AnyPublisher in - guard let self = self, - let activeMastodonAuthenticationBox = - self.context.authenticationService.activeMastodonAuthenticationBox.value else { - return Empty().eraseToAnyPublisher() + setting.sink { [weak self] (settings) in + guard let self = self else { return } + guard settings != nil else { return } + guard self.triggerBy != settings?.triggerBy else { return } + self.triggerBy = settings?.triggerBy + + var switches: [Bool?]? + var who: String? + if let alerts = settings?.subscription?.first(where: { (s) -> Bool in + return s.type == settings?.triggerBy + })?.alert { + var items = [Bool?]() + items.append(alerts.favourite?.boolValue) + items.append(alerts.follow?.boolValue) + items.append(alerts.reblog?.boolValue) + items.append(alerts.mention?.boolValue) + switches = items + who = settings?.triggerBy + } else if let triggerBy = settings?.triggerBy, + let values = self.notificationDefaultValue[triggerBy] { + switches = values + who = triggerBy + } else { + // fallback a default value + let anyone = L10n.Scene.Settings.Section.Notifications.Trigger.anyone + switches = self.notificationDefaultValue[anyone] + who = anyone } - let domain = activeMastodonAuthenticationBox.domain - return self.context.apiService.subscription( - domain: domain, - mastodonAuthenticationBox: activeMastodonAuthenticationBox) - } - .sink { [weak self] competion in - if case .failure(_) = competion { - // create a subscription when doesn't has one - let anyone = L10n.Scene.Settings.Section.Notifications.Trigger.anyone - if let values = self?.notificationDefaultValue[anyone] { - self?.createSubscriptionSubject.send((triggerBy: anyone, values: values)) - } + // should create a subscription whenever change trigger + if let values = switches, let triggerBy = who { + self.createSubscriptionSubject.send((triggerBy: triggerBy, values: values)) } - } receiveValue: { (subscription) in } .store(in: &disposeBag) + + guard let activeMastodonAuthenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { + return + } + let domain = activeMastodonAuthenticationBox.domain + let userId = activeMastodonAuthenticationBox.userID + + do { + try fetchResultsController.performFetch() + if nil == fetchResultsController.fetchedObjects?.first { + let anyone = L10n.Scene.Settings.Section.Notifications.Trigger.anyone + setting.value = self.context.apiService.createSettingIfNeed(domain: domain, + userId: userId, + triggerBy: anyone) + } else { + setting.value = fetchResultsController.fetchedObjects?.first + } + } catch { + assertionFailure(error.localizedDescription) + } } deinit { diff --git a/Mastodon/Service/APIService/APIService+Subscriptions.swift b/Mastodon/Service/APIService/APIService+Subscriptions.swift index d67284c7..337ab26d 100644 --- a/Mastodon/Service/APIService/APIService+Subscriptions.swift +++ b/Mastodon/Service/APIService/APIService+Subscriptions.swift @@ -5,44 +5,71 @@ // Created by ihugo on 2021/4/9. // +import Combine +import CoreData +import CoreDataStack import Foundation import MastodonSDK -import Combine extension APIService { func subscription( domain: String, + userID: String, mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox ) -> AnyPublisher, Error> { let authorization = mastodonAuthenticationBox.userAuthorization - + let findSettings: Setting? = { + let request = Setting.sortedFetchRequest + request.predicate = Setting.predicate(domain: domain, userID: userID) + request.fetchLimit = 1 + request.returnsObjectsAsFaults = false + do { + return try self.backgroundManagedObjectContext.fetch(request).first + } catch { + assertionFailure(error.localizedDescription) + return nil + } + }() + let triggerBy = findSettings?.triggerBy ?? "anyone" + let setting = self.createSettingIfNeed( + domain: domain, + userId: userID, + triggerBy: triggerBy + ) return Mastodon.API.Subscriptions.subscription( session: session, domain: domain, - authorization: authorization) - .flatMap { response -> AnyPublisher, Error> in - return self.backgroundManagedObjectContext.performChanges { - _ = APIService.CoreData.createOrMergeSubscription( - into: self.backgroundManagedObjectContext, - entity: response.value, - domain: domain) - } - .setFailureType(to: Error.self) - .map { _ in return response } - .eraseToAnyPublisher() - }.eraseToAnyPublisher() + authorization: authorization + ) + .flatMap { response -> AnyPublisher, Error> in + return self.backgroundManagedObjectContext.performChanges { + _ = APIService.CoreData.createOrMergeSubscription( + into: self.backgroundManagedObjectContext, + entity: response.value, + domain: domain, + triggerBy: triggerBy, + setting: setting) + } + .setFailureType(to: Error.self) + .map { _ in return response } + .eraseToAnyPublisher() + }.eraseToAnyPublisher() } func changeSubscription( domain: String, mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox, query: Mastodon.API.Subscriptions.CreateSubscriptionQuery, - triggerBy: String + triggerBy: String, + userID: String ) -> AnyPublisher, Error> { let authorization = mastodonAuthenticationBox.userAuthorization + let setting = self.createSettingIfNeed(domain: domain, + userId: userID, + triggerBy: triggerBy) return Mastodon.API.Subscriptions.createSubscription( session: session, domain: domain, @@ -55,7 +82,9 @@ extension APIService { into: self.backgroundManagedObjectContext, entity: response.value, domain: domain, - triggerBy: triggerBy) + triggerBy: triggerBy, + setting: setting + ) } .setFailureType(to: Error.self) .map { _ in return response } @@ -67,10 +96,15 @@ extension APIService { domain: String, mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox, query: Mastodon.API.Subscriptions.UpdateSubscriptionQuery, - triggerBy: String + triggerBy: String, + userID: String ) -> AnyPublisher, Error> { let authorization = mastodonAuthenticationBox.userAuthorization + let setting = self.createSettingIfNeed(domain: domain, + userId: userID, + triggerBy: triggerBy) + return Mastodon.API.Subscriptions.updateSubscription( session: session, domain: domain, @@ -83,12 +117,47 @@ extension APIService { into: self.backgroundManagedObjectContext, entity: response.value, domain: domain, - triggerBy: triggerBy) + triggerBy: triggerBy, + setting: setting + ) } .setFailureType(to: Error.self) .map { _ in return response } .eraseToAnyPublisher() }.eraseToAnyPublisher() } + + func createSettingIfNeed(domain: String, userId: String, triggerBy: String) -> Setting { + // create setting entity if possible + let oldSetting: Setting? = { + let request = Setting.sortedFetchRequest + request.predicate = Setting.predicate(domain: domain, userID: userId) + request.fetchLimit = 1 + request.returnsObjectsAsFaults = false + do { + return try backgroundManagedObjectContext.fetch(request).first + } catch { + assertionFailure(error.localizedDescription) + return nil + } + }() + var setting: Setting! + if let oldSetting = oldSetting { + setting = oldSetting + } else { + let property = Setting.Property( + appearance: "automatic", + triggerBy: triggerBy, + domain: domain, + userID: userId) + (setting, _) = APIService.CoreData.createOrMergeSetting( + into: backgroundManagedObjectContext, + domain: domain, + userID: userId, + property: property + ) + } + return setting + } } diff --git a/Mastodon/Service/APIService/CoreData/APIService+CoreData+Subscriptions.swift b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Subscriptions.swift index 8dc18973..f5a4022e 100644 --- a/Mastodon/Service/APIService/CoreData/APIService+CoreData+Subscriptions.swift +++ b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Subscriptions.swift @@ -16,11 +16,12 @@ extension APIService.CoreData { static func createOrMergeSetting( into managedObjectContext: NSManagedObjectContext, domain: String, + userID: String, property: Setting.Property ) -> (Subscription: Setting, isCreated: Bool) { let oldSetting: Setting? = { let request = Setting.sortedFetchRequest - request.predicate = Setting.predicate(domain: property.domain) + request.predicate = Setting.predicate(domain: property.domain, userID: userID) request.fetchLimit = 1 request.returnsObjectsAsFaults = false do { @@ -45,38 +46,12 @@ extension APIService.CoreData { into managedObjectContext: NSManagedObjectContext, entity: Mastodon.Entity.Subscription, domain: String, - triggerBy: String? = nil + triggerBy: String, + setting: Setting ) -> (Subscription: Subscription, isCreated: Bool) { - // create setting entity if possible - let oldSetting: Setting? = { - let request = Setting.sortedFetchRequest - request.predicate = Setting.predicate(domain: domain) - request.fetchLimit = 1 - request.returnsObjectsAsFaults = false - do { - return try managedObjectContext.fetch(request).first - } catch { - assertionFailure(error.localizedDescription) - return nil - } - }() - var setting: Setting! - if let oldSetting = oldSetting { - setting = oldSetting - } else { - let property = Setting.Property( - appearance: "automatic", - triggerBy: "anyone", - domain: domain) - (setting, _) = createOrMergeSetting( - into: managedObjectContext, - domain: domain, - property: property) - } - let oldSubscription: Subscription? = { let request = Subscription.sortedFetchRequest - request.predicate = Subscription.predicate(id: entity.id) + request.predicate = Subscription.predicate(type: triggerBy) request.fetchLimit = 1 request.returnsObjectsAsFaults = false do { @@ -91,7 +66,8 @@ extension APIService.CoreData { endpoint: entity.endpoint, id: entity.id, serverKey: entity.serverKey, - type: triggerBy ?? setting.triggerBy ?? "") + type: triggerBy + ) let alertEntity = entity.alerts let alert = SubscriptionAlerts.Property( favourite: alertEntity.favouriteNumber, @@ -105,7 +81,8 @@ extension APIService.CoreData { if nil == oldSubscription.alert { oldSubscription.alert = SubscriptionAlerts.insert( into: managedObjectContext, - property: alert) + property: alert + ) } else { oldSubscription.alert?.updateIfNeed(property: alert) }