diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents
index 0d0170282..69c30e990 100644
--- a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents
+++ b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents
@@ -172,14 +172,12 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
@@ -221,24 +219,27 @@
-
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
-
+
@@ -263,10 +264,10 @@
-
+
-
-
+
+
-
+
\ No newline at end of file
diff --git a/CoreDataStack/CoreDataStack.swift b/CoreDataStack/CoreDataStack.swift
index 1d13ee5ee..766bcf4de 100644
--- a/CoreDataStack/CoreDataStack.swift
+++ b/CoreDataStack/CoreDataStack.swift
@@ -18,7 +18,7 @@ public final class CoreDataStack {
}
public convenience init(databaseName: String = "shared") {
- let storeURL = URL.storeURL(for: "group.org.joinmastodon.mastodon-temp", databaseName: databaseName)
+ let storeURL = URL.storeURL(for: AppSharedName.groupID, databaseName: databaseName)
let storeDescription = NSPersistentStoreDescription(url: storeURL)
self.init(persistentStoreDescriptions: [storeDescription])
}
diff --git a/CoreDataStack/Entity/Setting.swift b/CoreDataStack/Entity/Setting.swift
index 671f9bab3..6fac8c351 100644
--- a/CoreDataStack/Entity/Setting.swift
+++ b/CoreDataStack/Entity/Setting.swift
@@ -9,66 +9,61 @@ import CoreData
import Foundation
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 var appearanceRaw: 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
- // relationships
- @NSManaged public var subscription: Set?
+ // one-to-many relationships
+ @NSManaged public var subscriptions: Set?
}
-public extension Setting {
- override func awakeFromInsert() {
- super.awakeFromInsert()
- setPrimitiveValue(Date(), forKey: #keyPath(Setting.createdAt))
- }
+extension Setting {
- func didUpdate(at networkDate: Date) {
- self.updatedAt = networkDate
+ public override func awakeFromInsert() {
+ super.awakeFromInsert()
+ let now = Date()
+ setPrimitiveValue(now, forKey: #keyPath(Setting.createdAt))
+ setPrimitiveValue(now, forKey: #keyPath(Setting.updatedAt))
}
@discardableResult
- static func insert(
+ public static func insert(
into context: NSManagedObjectContext,
property: Property
) -> Setting {
let setting: Setting = context.insertObject()
- setting.appearance = property.appearance
- setting.triggerBy = property.triggerBy
+ setting.appearanceRaw = property.appearanceRaw
setting.domain = property.domain
setting.userID = property.userID
return setting
}
- func update(appearance: String?) {
- guard appearance != self.appearance else { return }
- self.appearance = appearance
+ public func update(appearanceRaw: String) {
+ guard appearanceRaw != self.appearanceRaw else { return }
+ self.appearanceRaw = appearanceRaw
didUpdate(at: Date())
}
- func update(triggerBy: String?) {
- guard triggerBy != self.triggerBy else { return }
- self.triggerBy = triggerBy
- didUpdate(at: Date())
+ public func didUpdate(at networkDate: Date) {
+ self.updatedAt = networkDate
}
+
}
-public extension Setting {
- struct Property {
- public let appearance: String
- public let triggerBy: String
+extension Setting {
+ public struct Property {
public let domain: String
public let userID: String
+ public let appearanceRaw: String
- public init(appearance: String, triggerBy: String, domain: String, userID: String) {
- self.appearance = appearance
- self.triggerBy = triggerBy
+ public init(domain: String, userID: String, appearanceRaw: String) {
self.domain = domain
self.userID = userID
+ self.appearanceRaw = appearanceRaw
}
}
}
diff --git a/CoreDataStack/Entity/Subscription.swift b/CoreDataStack/Entity/Subscription.swift
index 8ced945d9..6cb1902a1 100644
--- a/CoreDataStack/Entity/Subscription.swift
+++ b/CoreDataStack/Entity/Subscription.swift
@@ -10,30 +10,35 @@ import Foundation
import CoreData
public final class Subscription: NSManagedObject {
- @NSManaged public var id: String
- @NSManaged public var endpoint: String
- @NSManaged public var serverKey: String
- /// four types:
- /// - anyone
- /// - a follower
- /// - anyone I follow
- /// - no one
- @NSManaged public var type: String
+ @NSManaged public var id: String?
+ @NSManaged public var endpoint: String?
+ @NSManaged public var policyRaw: String
+ @NSManaged public var serverKey: String?
+ @NSManaged public var userToken: String?
@NSManaged public private(set) var createdAt: Date
@NSManaged public private(set) var updatedAt: Date
+ @NSManaged public private(set) var activedAt: Date
+
+ // MARK: one-to-one relationships
+ @NSManaged public var alert: SubscriptionAlerts
- // MARK: - relationships
- @NSManaged public var alert: SubscriptionAlerts?
- // MARK: holder
+ // MARK: many-to-one relationships
@NSManaged public var setting: Setting?
}
public extension Subscription {
override func awakeFromInsert() {
super.awakeFromInsert()
- setPrimitiveValue(Date(), forKey: #keyPath(Subscription.createdAt))
+ let now = Date()
+ setPrimitiveValue(now, forKey: #keyPath(Subscription.createdAt))
+ setPrimitiveValue(now, forKey: #keyPath(Subscription.updatedAt))
+ setPrimitiveValue(now, forKey: #keyPath(Subscription.activedAt))
+ }
+
+ func update(activedAt: Date) {
+ self.activedAt = activedAt
}
func didUpdate(at networkDate: Date) {
@@ -43,45 +48,22 @@ public extension Subscription {
@discardableResult
static func insert(
into context: NSManagedObjectContext,
- property: Property
+ property: Property,
+ setting: Setting
) -> Subscription {
- let setting: Subscription = context.insertObject()
- setting.id = property.id
- setting.endpoint = property.endpoint
- setting.serverKey = property.serverKey
- setting.type = property.type
-
- return setting
+ let subscription: Subscription = context.insertObject()
+ subscription.policyRaw = property.policyRaw
+ subscription.setting = setting
+ return subscription
}
}
public extension Subscription {
struct Property {
- public let endpoint: String
- public let id: String
- public let serverKey: String
- public let type: String
+ public let policyRaw: String
- public init(endpoint: String, id: String, serverKey: String, type: String) {
- self.endpoint = endpoint
- self.id = id
- self.serverKey = serverKey
- self.type = type
- }
- }
-
- func updateIfNeed(property: Property) {
- if self.endpoint != property.endpoint {
- self.endpoint = property.endpoint
- }
- if self.id != property.id {
- self.id = property.id
- }
- if self.serverKey != property.serverKey {
- self.serverKey = property.serverKey
- }
- if self.type != property.type {
- self.type = property.type
+ public init(policyRaw: String) {
+ self.policyRaw = policyRaw
}
}
}
@@ -94,8 +76,8 @@ extension Subscription: Managed {
extension Subscription {
- public static func predicate(type: String) -> NSPredicate {
- return NSPredicate(format: "%K == %@", #keyPath(Subscription.type), type)
+ public static func predicate(policyRaw: String) -> NSPredicate {
+ return NSPredicate(format: "%K == %@", #keyPath(Subscription.policyRaw), policyRaw)
}
}
diff --git a/CoreDataStack/Entity/SubscriptionAlerts.swift b/CoreDataStack/Entity/SubscriptionAlerts.swift
index f5abf4955..613d1caf7 100644
--- a/CoreDataStack/Entity/SubscriptionAlerts.swift
+++ b/CoreDataStack/Entity/SubscriptionAlerts.swift
@@ -10,117 +10,165 @@ import Foundation
import CoreData
public final class SubscriptionAlerts: NSManagedObject {
- @NSManaged public var follow: NSNumber?
- @NSManaged public var favourite: NSNumber?
- @NSManaged public var reblog: NSNumber?
- @NSManaged public var mention: NSNumber?
- @NSManaged public var poll: NSNumber?
+ @NSManaged public var favouriteRaw: NSNumber?
+ @NSManaged public var followRaw: NSNumber?
+ @NSManaged public var followRequestRaw: NSNumber?
+ @NSManaged public var mentionRaw: NSNumber?
+ @NSManaged public var pollRaw: NSNumber?
+ @NSManaged public var reblogRaw: NSNumber?
@NSManaged public private(set) var createdAt: Date
@NSManaged public private(set) var updatedAt: Date
- // MARK: - relationships
- @NSManaged public var subscription: Subscription?
+ // MARK: one-to-one relationships
+ @NSManaged public var subscription: Subscription
}
-public extension SubscriptionAlerts {
- override func awakeFromInsert() {
- super.awakeFromInsert()
- setPrimitiveValue(Date(), forKey: #keyPath(SubscriptionAlerts.createdAt))
- }
+extension SubscriptionAlerts {
- func didUpdate(at networkDate: Date) {
- self.updatedAt = networkDate
+ public override func awakeFromInsert() {
+ super.awakeFromInsert()
+ let now = Date()
+ setPrimitiveValue(now, forKey: #keyPath(SubscriptionAlerts.createdAt))
+ setPrimitiveValue(now, forKey: #keyPath(SubscriptionAlerts.updatedAt))
}
@discardableResult
- static func insert(
+ public static func insert(
into context: NSManagedObjectContext,
- property: Property
+ property: Property,
+ subscription: Subscription
) -> SubscriptionAlerts {
let alerts: SubscriptionAlerts = context.insertObject()
- alerts.favourite = property.favourite
- alerts.follow = property.follow
- alerts.mention = property.mention
- alerts.poll = property.poll
- alerts.reblog = property.reblog
+
+ alerts.favouriteRaw = property.favouriteRaw
+ alerts.followRaw = property.followRaw
+ alerts.followRequestRaw = property.followRequestRaw
+ alerts.mentionRaw = property.mentionRaw
+ alerts.pollRaw = property.pollRaw
+ alerts.reblogRaw = property.reblogRaw
+
+ alerts.subscription = subscription
+
return alerts
}
- func update(favourite: NSNumber?) {
+ public func update(favourite: Bool?) {
guard self.favourite != favourite else { return }
self.favourite = favourite
didUpdate(at: Date())
}
- func update(follow: NSNumber?) {
+ public func update(follow: Bool?) {
guard self.follow != follow else { return }
self.follow = follow
didUpdate(at: Date())
}
- func update(mention: NSNumber?) {
+ public func update(followRequest: Bool?) {
+ guard self.followRequest != followRequest else { return }
+ self.followRequest = followRequest
+
+ didUpdate(at: Date())
+ }
+
+ public func update(mention: Bool?) {
guard self.mention != mention else { return }
self.mention = mention
didUpdate(at: Date())
}
- func update(poll: NSNumber?) {
+ public func update(poll: Bool?) {
guard self.poll != poll else { return }
self.poll = poll
didUpdate(at: Date())
}
- func update(reblog: NSNumber?) {
+ public func update(reblog: Bool?) {
guard self.reblog != reblog else { return }
self.reblog = reblog
didUpdate(at: Date())
}
+
+ public func didUpdate(at networkDate: Date) {
+ self.updatedAt = networkDate
+ }
+
}
-public extension SubscriptionAlerts {
- struct Property {
- public let favourite: NSNumber?
- public let follow: NSNumber?
- public let mention: NSNumber?
- public let poll: NSNumber?
- public let reblog: NSNumber?
+extension SubscriptionAlerts {
+
+ private func boolean(from number: NSNumber?) -> Bool? {
+ return number.flatMap { $0.intValue == 1 }
+ }
+
+ private func number(from boolean: Bool?) -> NSNumber? {
+ return boolean.flatMap { NSNumber(integerLiteral: $0 ? 1 : 0) }
+ }
+
+ public var favourite: Bool? {
+ get { boolean(from: favouriteRaw) }
+ set { favouriteRaw = number(from: newValue) }
+ }
+
+ public var follow: Bool? {
+ get { boolean(from: followRaw) }
+ set { followRaw = number(from: newValue) }
+ }
+
+ public var followRequest: Bool? {
+ get { boolean(from: followRequestRaw) }
+ set { followRequestRaw = number(from: newValue) }
+ }
+
+ public var mention: Bool? {
+ get { boolean(from: mentionRaw) }
+ set { mentionRaw = number(from: newValue) }
+ }
+
+ public var poll: Bool? {
+ get { boolean(from: pollRaw) }
+ set { pollRaw = number(from: newValue) }
+ }
+
+ public var reblog: Bool? {
+ get { boolean(from: reblogRaw) }
+ set { reblogRaw = number(from: newValue) }
+ }
+
+}
- public init(favourite: NSNumber?, follow: NSNumber?, mention: NSNumber?, poll: NSNumber?, reblog: NSNumber?) {
- self.favourite = favourite
- self.follow = follow
- self.mention = mention
- self.poll = poll
- self.reblog = reblog
+extension SubscriptionAlerts {
+ public struct Property {
+ public let favouriteRaw: NSNumber?
+ public let followRaw: NSNumber?
+ public let followRequestRaw: NSNumber?
+ public let mentionRaw: NSNumber?
+ public let pollRaw: NSNumber?
+ public let reblogRaw: NSNumber?
+
+ public init(
+ favourite: Bool?,
+ follow: Bool?,
+ followRequest: Bool?,
+ mention: Bool?,
+ poll: Bool?,
+ reblog: Bool?
+ ) {
+ self.favouriteRaw = favourite.flatMap { NSNumber(value: $0 ? 1 : 0) }
+ self.followRaw = follow.flatMap { NSNumber(value: $0 ? 1 : 0) }
+ self.followRequestRaw = followRequest.flatMap { NSNumber(value: $0 ? 1 : 0) }
+ self.mentionRaw = mention.flatMap { NSNumber(value: $0 ? 1 : 0) }
+ self.pollRaw = poll.flatMap { NSNumber(value: $0 ? 1 : 0) }
+ self.reblogRaw = reblog.flatMap { NSNumber(value: $0 ? 1 : 0) }
}
}
- func updateIfNeed(property: Property) {
- if self.follow != property.follow {
- self.follow = property.follow
- }
-
- if self.favourite != property.favourite {
- self.favourite = property.favourite
- }
-
- if self.reblog != property.reblog {
- self.reblog = property.reblog
- }
-
- if self.mention != property.mention {
- self.mention = property.mention
- }
-
- if self.poll != property.poll {
- self.poll = property.poll
- }
- }
}
extension SubscriptionAlerts: Managed {
diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj
index 30202e558..b1bc63e6d 100644
--- a/Mastodon.xcodeproj/project.pbxproj
+++ b/Mastodon.xcodeproj/project.pbxproj
@@ -138,7 +138,6 @@
5B90C460262599800002E742 /* SettingsAppearanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C45A262599800002E742 /* SettingsAppearanceTableViewCell.swift */; };
5B90C461262599800002E742 /* SettingsLinkTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C45B262599800002E742 /* SettingsLinkTableViewCell.swift */; };
5B90C462262599800002E742 /* SettingsSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C45C262599800002E742 /* SettingsSectionHeader.swift */; };
- 5B90C463262599800002E742 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C45D262599800002E742 /* SettingsViewController.swift */; };
5B90C46E26259B2C0002E742 /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C46C26259B2C0002E742 /* Subscription.swift */; };
5B90C46F26259B2C0002E742 /* Setting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C46D26259B2C0002E742 /* Setting.swift */; };
5B90C47F26259BA90002E742 /* SubscriptionAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C47E26259BA90002E742 /* SubscriptionAlerts.swift */; };
@@ -250,10 +249,24 @@
DB6B35182601FA3400DC1E11 /* MastodonAttachmentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */; };
DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift */; };
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */; };
+ DB6D1B24263684C600ACB481 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D1B23263684C600ACB481 /* UserDefaults.swift */; };
+ DB6D1B2B2636852000ACB481 /* AppSharedName.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D1B2A2636852000ACB481 /* AppSharedName.swift */; };
+ DB6D1B312636853100ACB481 /* AppSharedName.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D1B2A2636852000ACB481 /* AppSharedName.swift */; };
+ DB6D1B3D2636857500ACB481 /* AppearancePreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D1B3C2636857500ACB481 /* AppearancePreference.swift */; };
+ DB6D1B44263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D1B43263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift */; };
DB6D9F232635195E008423CD /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F222635195E008423CD /* String.swift */; };
DB6D9F3526351B7A008423CD /* NotificationService+Decrypt.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */; };
DB6D9F3B26352019008423CD /* AppSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1E05E0263180F500201847 /* AppSecret.swift */; };
DB6D9F42263527CE008423CD /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB6D9F41263527CE008423CD /* AlamofireImage */; };
+ DB6D9F4926353FD7008423CD /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F4826353FD6008423CD /* Subscription.swift */; };
+ DB6D9F502635761F008423CD /* SubscriptionAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F4F2635761F008423CD /* SubscriptionAlerts.swift */; };
+ DB6D9F57263577D2008423CD /* APIService+CoreData+Setting.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F56263577D2008423CD /* APIService+CoreData+Setting.swift */; };
+ DB6D9F6326357848008423CD /* SettingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F6226357848008423CD /* SettingService.swift */; };
+ DB6D9F6F2635807F008423CD /* Setting.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F6E2635807F008423CD /* Setting.swift */; };
+ DB6D9F76263587C7008423CD /* SettingFetchedResultController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F75263587C7008423CD /* SettingFetchedResultController.swift */; };
+ DB6D9F7D26358ED4008423CD /* SettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F7C26358ED4008423CD /* SettingsSection.swift */; };
+ DB6D9F8426358EEC008423CD /* SettingsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F8326358EEC008423CD /* SettingsItem.swift */; };
+ DB6D9F9726367249008423CD /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F9626367249008423CD /* SettingsViewController.swift */; };
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */; };
DB71FD3625F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */; };
DB71FD3C25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD3B25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift */; };
@@ -584,7 +597,6 @@
5B90C45A262599800002E742 /* SettingsAppearanceTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsAppearanceTableViewCell.swift; sourceTree = ""; };
5B90C45B262599800002E742 /* SettingsLinkTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsLinkTableViewCell.swift; sourceTree = ""; };
5B90C45C262599800002E742 /* SettingsSectionHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsSectionHeader.swift; sourceTree = ""; };
- 5B90C45D262599800002E742 /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; };
5B90C46C26259B2C0002E742 /* Subscription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Subscription.swift; sourceTree = ""; };
5B90C46D26259B2C0002E742 /* Setting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Setting.swift; sourceTree = ""; };
5B90C47E26259BA90002E742 /* SubscriptionAlerts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionAlerts.swift; sourceTree = ""; };
@@ -703,8 +715,21 @@
DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAttachmentService.swift; sourceTree = ""; };
DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentCollectionViewCell.swift; sourceTree = ""; };
DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error.swift"; sourceTree = ""; };
+ DB6D1B23263684C600ACB481 /* UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaults.swift; sourceTree = ""; };
+ DB6D1B2A2636852000ACB481 /* AppSharedName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSharedName.swift; sourceTree = ""; };
+ DB6D1B3C2636857500ACB481 /* AppearancePreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePreference.swift; sourceTree = ""; };
+ DB6D1B43263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+API+Subscriptions+Policy.swift"; sourceTree = ""; };
DB6D9F222635195E008423CD /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; };
DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationService+Decrypt.swift"; sourceTree = ""; };
+ DB6D9F4826353FD6008423CD /* Subscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Subscription.swift; sourceTree = ""; };
+ DB6D9F4F2635761F008423CD /* SubscriptionAlerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionAlerts.swift; sourceTree = ""; };
+ DB6D9F56263577D2008423CD /* APIService+CoreData+Setting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Setting.swift"; sourceTree = ""; };
+ DB6D9F6226357848008423CD /* SettingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingService.swift; sourceTree = ""; };
+ DB6D9F6E2635807F008423CD /* Setting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Setting.swift; sourceTree = ""; };
+ DB6D9F75263587C7008423CD /* SettingFetchedResultController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingFetchedResultController.swift; sourceTree = ""; };
+ DB6D9F7C26358ED4008423CD /* SettingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSection.swift; sourceTree = ""; };
+ DB6D9F8326358EEC008423CD /* SettingsItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsItem.swift; sourceTree = ""; };
+ DB6D9F9626367249008423CD /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; };
DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarStackContainerButton.swift; sourceTree = ""; };
DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+PersistMemo.swift"; sourceTree = ""; };
DB71FD3B25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+PersistCache.swift"; sourceTree = ""; };
@@ -1109,6 +1134,7 @@
DB71FD4B25F8C80E00512AE1 /* StatusPrefetchingService.swift */,
DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */,
DB4924E126312AB200E9DB22 /* NotificationService.swift */,
+ DB6D9F6226357848008423CD /* SettingService.swift */,
);
path = Service;
sourceTree = "";
@@ -1176,6 +1202,7 @@
2D198648261C0B8500F0B013 /* SearchResultSection.swift */,
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */,
DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */,
+ DB6D9F7C26358ED4008423CD /* SettingsSection.swift */,
);
path = Section;
sourceTree = "";
@@ -1232,6 +1259,7 @@
DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */,
DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */,
DB44768A260B3F2100B66B82 /* CustomEmojiPickerItem.swift */,
+ DB6D9F8326358EEC008423CD /* SettingsItem.swift */,
);
path = Item;
sourceTree = "";
@@ -1285,8 +1313,8 @@
isa = PBXGroup;
children = (
5B90C457262599800002E742 /* View */,
+ DB6D9F9626367249008423CD /* SettingsViewController.swift */,
5B90C456262599800002E742 /* SettingsViewModel.swift */,
- 5B90C45D262599800002E742 /* SettingsViewController.swift */,
);
path = Settings;
sourceTree = "";
@@ -1350,6 +1378,9 @@
DB084B5625CBC56C00F898ED /* Status.swift */,
DB45FAE225CA7181005A8AC7 /* MastodonUser.swift */,
DB9D6C3725E508BE0051B173 /* Attachment.swift */,
+ DB6D9F6E2635807F008423CD /* Setting.swift */,
+ DB6D9F4826353FD6008423CD /* Subscription.swift */,
+ DB6D9F4F2635761F008423CD /* SubscriptionAlerts.swift */,
);
path = CoreDataStack;
sourceTree = "";
@@ -1377,6 +1408,7 @@
DB427DD525BAA00100D1B89D /* AppDelegate.swift */,
DB427DD725BAA00100D1B89D /* SceneDelegate.swift */,
DB1E05E0263180F500201847 /* AppSecret.swift */,
+ DB6D1B2A2636852000ACB481 /* AppSharedName.swift */,
DB427DDB25BAA00100D1B89D /* Main.storyboard */,
DB427DE025BAA00100D1B89D /* LaunchScreen.storyboard */,
DB68A05C25E9055900CFDF14 /* Settings.bundle */,
@@ -1511,6 +1543,7 @@
DB45FADC25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift */,
DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */,
2D79E700261EA5550011E398 /* APIService+CoreData+Tag.swift */,
+ DB6D9F56263577D2008423CD /* APIService+CoreData+Setting.swift */,
5B90C48A26259C120002E742 /* APIService+CoreData+Subscriptions.swift */,
);
path = CoreData;
@@ -1530,6 +1563,7 @@
isa = PBXGroup;
children = (
DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */,
+ DB6D1B3C2636857500ACB481 /* AppearancePreference.swift */,
);
path = Preference;
sourceTree = "";
@@ -1573,6 +1607,7 @@
5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */,
2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */,
2DB72C8B262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift */,
+ DB6D1B43263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift */,
);
path = MastodonSDK;
sourceTree = "";
@@ -1787,6 +1822,7 @@
0F20223826146553000C64BF /* Array.swift */,
DBCC3B2F261440A50045B23D /* UITabBarController.swift */,
DBCC3B35261440BA0045B23D /* UINavigationController.swift */,
+ DB6D1B23263684C600ACB481 /* UserDefaults.swift */,
);
path = Extension;
sourceTree = "";
@@ -1994,6 +2030,7 @@
isa = PBXGroup;
children = (
DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */,
+ DB6D9F75263587C7008423CD /* SettingFetchedResultController.swift */,
);
path = FetchedResultsController;
sourceTree = "";
@@ -2555,6 +2592,7 @@
DB55D33025FB630A0002F825 /* TwitterTextEditor+String.swift in Sources */,
2D38F1EB25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift in Sources */,
0F202201261326E6000C64BF /* HashtagTimelineViewModel.swift in Sources */,
+ DB6D9F9726367249008423CD /* SettingsViewController.swift in Sources */,
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */,
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */,
DBE3CE0D261D767100430CC6 /* FavoriteViewController+StatusProvider.swift in Sources */,
@@ -2566,8 +2604,8 @@
5DDDF1A92617489F00311060 /* Mastodon+Entity+History.swift in Sources */,
DB59F11825EFA35B001F1DAB /* StripProgressView.swift in Sources */,
DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */,
- 5B90C463262599800002E742 /* SettingsViewController.swift in Sources */,
DB73B490261F030A002E9E9F /* SafariActivity.swift in Sources */,
+ DB6D1B44263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift in Sources */,
DB9A48962603685D008B817C /* MastodonAttachmentService+UploadState.swift in Sources */,
2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */,
DB66728C25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift in Sources */,
@@ -2599,6 +2637,7 @@
5B90C45F262599800002E742 /* SettingsToggleTableViewCell.swift in Sources */,
2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */,
DBAE3F682615DD60004B8251 /* UserProvider.swift in Sources */,
+ DB6D9F76263587C7008423CD /* SettingFetchedResultController.swift in Sources */,
DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */,
5D0393902612D259007FE196 /* WebViewController.swift in Sources */,
DB4481CC25EE2AFE00BEFB67 /* PollItem.swift in Sources */,
@@ -2624,9 +2663,11 @@
DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */,
2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */,
2D084B8D26258EA3003AA3AF /* NotificationViewModel+diffable.swift in Sources */,
+ DB6D1B24263684C600ACB481 /* UserDefaults.swift in Sources */,
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */,
DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */,
+ DB6D9F7D26358ED4008423CD /* SettingsSection.swift in Sources */,
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */,
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */,
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */,
@@ -2645,6 +2686,7 @@
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */,
DB68A04A25E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift in Sources */,
0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */,
+ DB6D9F8426358EEC008423CD /* SettingsItem.swift in Sources */,
2D364F7825E66D8300204FDC /* MastodonResendEmailViewModel.swift in Sources */,
DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */,
DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */,
@@ -2664,6 +2706,7 @@
2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */,
DB35FC252612FD7A006193C9 /* ProfileFieldView.swift in Sources */,
DB938F0326240EA300E5B6C1 /* CachedThreadViewModel.swift in Sources */,
+ DB6D9F6326357848008423CD /* SettingService.swift in Sources */,
2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */,
2D24E12D2626FD2E00A59D4F /* NotificationViewModel+LoadOldestState.swift in Sources */,
2DB72C8C262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift in Sources */,
@@ -2678,11 +2721,13 @@
DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */,
DBAE3F9E2616E308004B8251 /* APIService+Mute.swift in Sources */,
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */,
+ DB6D9F57263577D2008423CD /* APIService+CoreData+Setting.swift in Sources */,
DB9D6C0E25E4F9780051B173 /* MosaicImageViewContainer.swift in Sources */,
DB71FD3625F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift in Sources */,
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */,
DB98338725C945ED00AD9700 /* Strings.swift in Sources */,
2D7867192625B77500211898 /* NotificationItem.swift in Sources */,
+ DB6D1B2B2636852000ACB481 /* AppSharedName.swift in Sources */,
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */,
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */,
2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */,
@@ -2701,6 +2746,7 @@
2D76317D25C14DF500929FB9 /* PublicTimelineViewController+StatusProvider.swift in Sources */,
0F20223326145E51000C64BF /* HashtagTimelineViewModel+LoadMiddleState.swift in Sources */,
2D206B8025F5F45E00143C56 /* UIImage.swift in Sources */,
+ DB6D9F502635761F008423CD /* SubscriptionAlerts.swift in Sources */,
0F20220726134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift in Sources */,
2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */,
DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */,
@@ -2734,17 +2780,20 @@
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */,
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */,
DB4924E226312AB200E9DB22 /* NotificationService.swift in Sources */,
+ DB6D9F6F2635807F008423CD /* Setting.swift in Sources */,
DBB525412611ED54002F1F29 /* ProfileHeaderViewController.swift in Sources */,
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */,
0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */,
DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */,
DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */,
+ DB6D1B3D2636857500ACB481 /* AppearancePreference.swift in Sources */,
DB938F3326243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift in Sources */,
2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */,
DB938F25262438D600E5B6C1 /* ThreadViewController+StatusProvider.swift in Sources */,
DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */,
0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */,
5DF1057925F88A1D00D6C0D4 /* PlayerContainerView.swift in Sources */,
+ DB6D9F4926353FD7008423CD /* Subscription.swift in Sources */,
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */,
2D6DE40026141DF600A63F6A /* SearchViewModel.swift in Sources */,
DB51D172262832380062B7A1 /* BlurHashDecode.swift in Sources */,
@@ -2794,6 +2843,7 @@
2D152A9225C2980C009AA50C /* UIFont.swift in Sources */,
DB4481B325EE16D000BEFB67 /* PollOption.swift in Sources */,
DB89BA4425C1165F008580ED /* Managed.swift in Sources */,
+ DB6D1B312636853100ACB481 /* AppSharedName.swift in Sources */,
2D6125472625436B00299647 /* Notification.swift in Sources */,
DB89BA4325C1165F008580ED /* NetworkUpdatable.swift in Sources */,
DB8AF56825C13E2A002E6C99 /* HomeTimelineIndex.swift in Sources */,
diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
index 41711ac91..3e5f0c5d3 100644
--- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -7,7 +7,7 @@
CoreDataStack.xcscheme_^#shared#^_
orderHint
- 14
+ 13
Mastodon - RTL.xcscheme_^#shared#^_
@@ -27,7 +27,7 @@
NotificationService.xcscheme_^#shared#^_
orderHint
- 17
+ 14
SuppressBuildableAutocreation
diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift
index c2608fe83..9770aa9a4 100644
--- a/Mastodon/Coordinator/SceneCoordinator.swift
+++ b/Mastodon/Coordinator/SceneCoordinator.swift
@@ -61,6 +61,9 @@ extension SceneCoordinator {
case profile(viewModel: ProfileViewModel)
case favorite(viewModel: FavoriteViewModel)
+ // setting
+ case settings(viewModel: SettingsViewModel)
+
// misc
case safari(url: URL)
case alertController(alertController: UIAlertController)
@@ -68,7 +71,6 @@ extension SceneCoordinator {
#if DEBUG
case publicTimeline
- case settings
#endif
var isOnboarding: Bool {
@@ -246,6 +248,10 @@ private extension SceneCoordinator {
let _viewController = FavoriteViewController()
_viewController.viewModel = viewModel
viewController = _viewController
+ case .settings(let viewModel):
+ let _viewController = SettingsViewController()
+ _viewController.viewModel = viewModel
+ viewController = _viewController
case .safari(let url):
guard let scheme = url.scheme?.lowercased(),
scheme == "http" || scheme == "https" else {
@@ -270,10 +276,6 @@ private extension SceneCoordinator {
let _viewController = PublicTimelineViewController()
_viewController.viewModel = PublicTimelineViewModel(context: appContext)
viewController = _viewController
- case .settings:
- let _viewController = SettingsViewController()
- _viewController.viewModel = SettingsViewModel(context: appContext, coordinator: self)
- viewController = _viewController
#endif
}
diff --git a/Mastodon/Diffiable/FetchedResultsController/SettingFetchedResultController.swift b/Mastodon/Diffiable/FetchedResultsController/SettingFetchedResultController.swift
new file mode 100644
index 000000000..52eafc6b6
--- /dev/null
+++ b/Mastodon/Diffiable/FetchedResultsController/SettingFetchedResultController.swift
@@ -0,0 +1,64 @@
+//
+// SettingFetchedResultController.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-4-25.
+//
+
+import os.log
+import UIKit
+import Combine
+import CoreData
+import CoreDataStack
+import MastodonSDK
+
+final class SettingFetchedResultController: NSObject {
+
+ var disposeBag = Set()
+
+ let fetchedResultsController: NSFetchedResultsController
+
+ // input
+
+ // output
+ let settings = CurrentValueSubject<[Setting], Never>([])
+
+ init(managedObjectContext: NSManagedObjectContext, additionalPredicate: NSPredicate?) {
+ self.fetchedResultsController = {
+ let fetchRequest = Setting.sortedFetchRequest
+ fetchRequest.returnsObjectsAsFaults = false
+ if let additionalPredicate = additionalPredicate {
+ fetchRequest.predicate = additionalPredicate
+ }
+ fetchRequest.fetchBatchSize = 20
+ let controller = NSFetchedResultsController(
+ fetchRequest: fetchRequest,
+ managedObjectContext: managedObjectContext,
+ sectionNameKeyPath: nil,
+ cacheName: nil
+ )
+
+ return controller
+ }()
+ super.init()
+
+ fetchedResultsController.delegate = self
+
+ do {
+ try self.fetchedResultsController.performFetch()
+ } catch {
+ assertionFailure(error.localizedDescription)
+ }
+ }
+
+}
+
+// MARK: - NSFetchedResultsControllerDelegate
+extension SettingFetchedResultController: NSFetchedResultsControllerDelegate {
+ func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
+ os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
+
+ let objects = fetchedResultsController.fetchedObjects ?? []
+ self.settings.value = objects
+ }
+}
diff --git a/Mastodon/Diffiable/Item/SettingsItem.swift b/Mastodon/Diffiable/Item/SettingsItem.swift
new file mode 100644
index 000000000..8aabdc741
--- /dev/null
+++ b/Mastodon/Diffiable/Item/SettingsItem.swift
@@ -0,0 +1,67 @@
+//
+// SettingsItem.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-4-25.
+//
+
+import UIKit
+import CoreData
+
+enum SettingsItem: Hashable {
+ case apperance(settingObjectID: NSManagedObjectID)
+ case notification(settingObjectID: NSManagedObjectID, switchMode: NotificationSwitchMode)
+ case boringZone(item: Link)
+ case spicyZone(item: Link)
+}
+
+extension SettingsItem {
+
+ enum AppearanceMode: String {
+ case automatic
+ case light
+ case dark
+ }
+
+ enum NotificationSwitchMode: CaseIterable {
+ case favorite
+ case follow
+ case reblog
+ case mention
+
+ var title: String {
+ switch self {
+ case .favorite: return L10n.Scene.Settings.Section.Notifications.favorites
+ case .follow: return L10n.Scene.Settings.Section.Notifications.follows
+ case .reblog: return L10n.Scene.Settings.Section.Notifications.boosts
+ case .mention: return L10n.Scene.Settings.Section.Notifications.mentions
+ }
+ }
+ }
+
+ enum Link: CaseIterable {
+ case termsOfService
+ case privacyPolicy
+ case clearMediaCache
+ case signOut
+
+ var title: String {
+ switch self {
+ case .termsOfService: return L10n.Scene.Settings.Section.Boringzone.terms
+ case .privacyPolicy: return L10n.Scene.Settings.Section.Boringzone.privacy
+ case .clearMediaCache: return L10n.Scene.Settings.Section.Spicyzone.clear
+ case .signOut: return L10n.Scene.Settings.Section.Spicyzone.signout
+ }
+ }
+
+ var textColor: UIColor {
+ switch self {
+ case .termsOfService: return .systemBlue
+ case .privacyPolicy: return .systemBlue
+ case .clearMediaCache: return .systemRed
+ case .signOut: return .systemRed
+ }
+ }
+ }
+
+}
diff --git a/Mastodon/Diffiable/Section/SettingsSection.swift b/Mastodon/Diffiable/Section/SettingsSection.swift
new file mode 100644
index 000000000..7ec78a2ed
--- /dev/null
+++ b/Mastodon/Diffiable/Section/SettingsSection.swift
@@ -0,0 +1,24 @@
+//
+// SettingsSection.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-4-25.
+//
+
+import Foundation
+
+enum SettingsSection: Hashable {
+ case apperance
+ case notifications
+ case boringZone
+ case spicyZone
+
+ var title: String {
+ switch self {
+ case .apperance: return L10n.Scene.Settings.Section.Appearance.title
+ case .notifications: return L10n.Scene.Settings.Section.Notifications.title
+ case .boringZone: return L10n.Scene.Settings.Section.Boringzone.title
+ case .spicyZone: return L10n.Scene.Settings.Section.Spicyzone.title
+ }
+ }
+}
diff --git a/Mastodon/Extension/CoreDataStack/Setting.swift b/Mastodon/Extension/CoreDataStack/Setting.swift
new file mode 100644
index 000000000..b995b80e3
--- /dev/null
+++ b/Mastodon/Extension/CoreDataStack/Setting.swift
@@ -0,0 +1,24 @@
+//
+// Setting.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-4-25.
+//
+
+import Foundation
+import CoreDataStack
+import MastodonSDK
+
+extension Setting {
+
+ var appearance: SettingsItem.AppearanceMode {
+ return SettingsItem.AppearanceMode(rawValue: appearanceRaw) ?? .automatic
+ }
+
+ var activeSubscription: Subscription? {
+ return (subscriptions ?? Set())
+ .sorted(by: { $0.activedAt > $1.activedAt })
+ .first
+ }
+
+}
diff --git a/Mastodon/Extension/CoreDataStack/Subscription.swift b/Mastodon/Extension/CoreDataStack/Subscription.swift
new file mode 100644
index 000000000..8253264a0
--- /dev/null
+++ b/Mastodon/Extension/CoreDataStack/Subscription.swift
@@ -0,0 +1,20 @@
+//
+// Subscription.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-4-25.
+//
+
+import Foundation
+import CoreDataStack
+import MastodonSDK
+
+typealias NotificationSubscription = Subscription
+
+extension Subscription {
+
+ var policy: Mastodon.API.Subscriptions.Policy {
+ return Mastodon.API.Subscriptions.Policy(rawValue: policyRaw) ?? .all
+ }
+
+}
diff --git a/Mastodon/Extension/CoreDataStack/SubscriptionAlerts.swift b/Mastodon/Extension/CoreDataStack/SubscriptionAlerts.swift
new file mode 100644
index 000000000..edf2df0c9
--- /dev/null
+++ b/Mastodon/Extension/CoreDataStack/SubscriptionAlerts.swift
@@ -0,0 +1,28 @@
+//
+// SubscriptionAlerts.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-4-25.
+//
+
+
+import Foundation
+import CoreDataStack
+import MastodonSDK
+
+extension SubscriptionAlerts.Property {
+
+ init(policy: Mastodon.API.Subscriptions.Policy) {
+ switch policy {
+ case .all:
+ self.init(favourite: true, follow: true, followRequest: true, mention: true, poll: true, reblog: true)
+ case .follower:
+ self.init(favourite: true, follow: nil, followRequest: nil, mention: true, poll: true, reblog: true)
+ case .followed:
+ self.init(favourite: true, follow: true, followRequest: true, mention: true, poll: true, reblog: true)
+ case .none, ._other:
+ self.init(favourite: nil, follow: nil, followRequest: nil, mention: nil, poll: nil, reblog: nil)
+ }
+ }
+
+}
diff --git a/Mastodon/Extension/MastodonSDK/Mastodon+API+Subscriptions+Policy.swift b/Mastodon/Extension/MastodonSDK/Mastodon+API+Subscriptions+Policy.swift
new file mode 100644
index 000000000..24bbfdace
--- /dev/null
+++ b/Mastodon/Extension/MastodonSDK/Mastodon+API+Subscriptions+Policy.swift
@@ -0,0 +1,20 @@
+//
+// Mastodon+API+Subscriptions+Policy.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-4-26.
+//
+
+import Foundation
+import MastodonSDK
+
+extension Mastodon.API.Subscriptions.Policy {
+ var title: String {
+ switch self {
+ case .all: return L10n.Scene.Settings.Section.Notifications.Trigger.anyone
+ case .follower: return L10n.Scene.Settings.Section.Notifications.Trigger.follower
+ case .followed: return L10n.Scene.Settings.Section.Notifications.Trigger.follow
+ case .none, ._other: return L10n.Scene.Settings.Section.Notifications.Trigger.noone
+ }
+ }
+}
diff --git a/Mastodon/Extension/UserDefaults.swift b/Mastodon/Extension/UserDefaults.swift
new file mode 100644
index 000000000..5e067bbe9
--- /dev/null
+++ b/Mastodon/Extension/UserDefaults.swift
@@ -0,0 +1,31 @@
+//
+// UserDefaults.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-4-26.
+//
+
+import Foundation
+
+extension UserDefaults {
+ static let shared = UserDefaults(suiteName: AppSharedName.groupID)!
+}
+
+extension UserDefaults {
+
+ subscript(key: String) -> T? {
+ get {
+ if let rawValue = value(forKey: key) as? T.RawValue {
+ return T(rawValue: rawValue)
+ }
+ return nil
+ }
+ set { set(newValue?.rawValue, forKey: key) }
+ }
+
+ subscript(key: String) -> T? {
+ get { return value(forKey: key) as? T }
+ set { set(newValue, forKey: key) }
+ }
+
+}
diff --git a/Mastodon/Preference/AppearancePreference.swift b/Mastodon/Preference/AppearancePreference.swift
new file mode 100644
index 000000000..8f2818c39
--- /dev/null
+++ b/Mastodon/Preference/AppearancePreference.swift
@@ -0,0 +1,20 @@
+//
+// AppearancePreference.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-4-26.
+//
+
+import UIKit
+
+extension UserDefaults {
+
+ @objc dynamic var customUserInterfaceStyle: UIUserInterfaceStyle {
+ get {
+ register(defaults: [#function: UIUserInterfaceStyle.unspecified.rawValue])
+ return UIUserInterfaceStyle(rawValue: integer(forKey: #function)) ?? .unspecified
+ }
+ set { UserDefaults.shared[#function] = newValue.rawValue }
+ }
+
+}
diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift
index 8bbf9436e..63f76152f 100644
--- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift
+++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift
@@ -328,7 +328,9 @@ extension HomeTimelineViewController {
}
@objc private func showSettings(_ sender: UIAction) {
- coordinator.present(scene: .settings, from: self, transition: .modal(animated: true, completion: nil))
+ guard let currentSetting = context.settingService.currentSetting.value else { return }
+ let settingsViewModel = SettingsViewModel(context: context, setting: currentSetting)
+ coordinator.present(scene: .settings(viewModel: settingsViewModel), from: self, transition: .modal(animated: true, completion: nil))
}
}
#endif
diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift
index 53909b2df..93e632f77 100644
--- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift
+++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift
@@ -88,14 +88,8 @@ extension HomeTimelineViewController {
// long press to trigger debug menu
settingBarButtonItem.menu = debugMenu
#else
- // settingBarButtonItem.target = self
- // settingBarButtonItem.action = #selector(HomeTimelineViewController.settingBarButtonItemPressed(_:))
- settingBarButtonItem.menu = UIMenu(title: "Settings", image: nil, identifier: nil, options: .displayInline, children: [
- UIAction(title: "Sign Out", image: UIImage(systemName: "escape"), attributes: .destructive) { [weak self] action in
- guard let self = self else { return }
- self.signOutAction(action)
- }
- ])
+ settingBarButtonItem.target = self
+ settingBarButtonItem.action = #selector(HomeTimelineViewController.settingBarButtonItemPressed(_:))
#endif
navigationItem.rightBarButtonItem = composeBarButtonItem
@@ -220,7 +214,9 @@ extension HomeTimelineViewController {
@objc private func settingBarButtonItemPressed(_ sender: UIBarButtonItem) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
-
+ guard let setting = context.settingService.currentSetting.value else { return }
+ let settingsViewModel = SettingsViewModel(context: context, setting: setting)
+ coordinator.present(scene: .settings(viewModel: settingsViewModel), from: self, transition: .modal(animated: true, completion: nil))
}
@objc private func composeBarButtonItemPressed(_ sender: UIBarButtonItem) {
diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift
index 8fc915a0a..e4be1eb1f 100644
--- a/Mastodon/Scene/Profile/ProfileViewController.swift
+++ b/Mastodon/Scene/Profile/ProfileViewController.swift
@@ -517,7 +517,9 @@ extension ProfileViewController {
@objc private func settingBarButtonItemPressed(_ sender: UIBarButtonItem) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
-
+ guard let setting = context.settingService.currentSetting.value else { return }
+ let settingsViewModel = SettingsViewModel(context: context, setting: setting)
+ coordinator.present(scene: .settings(viewModel: settingsViewModel), from: self, transition: .modal(animated: true, completion: nil))
}
@objc private func shareBarButtonItemPressed(_ sender: UIBarButtonItem) {
diff --git a/Mastodon/Scene/Settings/SettingsViewController.swift b/Mastodon/Scene/Settings/SettingsViewController.swift
index b9ded67d0..aeed943eb 100644
--- a/Mastodon/Scene/Settings/SettingsViewController.swift
+++ b/Mastodon/Scene/Settings/SettingsViewController.swift
@@ -11,11 +11,10 @@ import Combine
import ActiveLabel
import CoreData
import CoreDataStack
+import MastodonSDK
import AlamofireImage
import Kingfisher
-// iTODO: when to ask permission to Use Notifications
-
class SettingsViewController: UIViewController, NeedsDependency {
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
@@ -23,6 +22,7 @@ class SettingsViewController: UIViewController, NeedsDependency {
var viewModel: SettingsViewModel! { willSet { precondition(!isViewLoaded) } }
var disposeBag = Set()
+ var notificationPolicySubscription: AnyCancellable?
var triggerMenu: UIMenu {
let anyone = L10n.Scene.Settings.Section.Notifications.Trigger.anyone
@@ -35,23 +35,23 @@ class SettingsViewController: UIViewController, NeedsDependency {
options: .displayInline,
children: [
UIAction(title: anyone, image: UIImage(systemName: "person.3"), attributes: []) { [weak self] action in
- self?.updateTrigger(by: anyone)
+ self?.updateTrigger(policy: .all)
},
UIAction(title: follower, image: UIImage(systemName: "person.crop.circle.badge.plus"), attributes: []) { [weak self] action in
- self?.updateTrigger(by: follower)
+ self?.updateTrigger(policy: .follower)
},
UIAction(title: follow, image: UIImage(systemName: "person.crop.circle.badge.checkmark"), attributes: []) { [weak self] action in
- self?.updateTrigger(by: follow)
+ self?.updateTrigger(policy: .followed)
},
UIAction(title: noOne, image: UIImage(systemName: "nosign"), attributes: []) { [weak self] action in
- self?.updateTrigger(by: noOne)
+ self?.updateTrigger(policy: .none)
},
]
)
return menu
}
- lazy var notifySectionHeader: UIView = {
+ private(set) lazy var notifySectionHeader: UIView = {
let view = UIStackView()
view.translatesAutoresizingMaskIntoConstraints = false
view.isLayoutMarginsRelativeArrangement = true
@@ -71,15 +71,12 @@ class SettingsViewController: UIViewController, NeedsDependency {
return view
}()
- lazy var whoButton: UIButton = {
+ private(set) lazy var whoButton: UIButton = {
let whoButton = UIButton(type: .roundedRect)
whoButton.menu = triggerMenu
whoButton.showsMenuAsPrimaryAction = true
whoButton.setBackgroundColor(Asset.Colors.battleshipGrey.color, for: .normal)
whoButton.setTitleColor(Asset.Colors.Label.primary.color, for: .normal)
- if let setting = self.viewModel.setting.value, let trigger = setting.triggerBy {
- whoButton.setTitle(trigger, for: .normal)
- }
whoButton.titleLabel?.font = UIFontMetrics(forTextStyle: .title3).scaledFont(for: UIFont.systemFont(ofSize: 20, weight: .semibold))
whoButton.contentEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
whoButton.layer.cornerRadius = 10
@@ -87,7 +84,7 @@ class SettingsViewController: UIViewController, NeedsDependency {
return whoButton
}()
- lazy var tableView: UITableView = {
+ private(set) lazy var tableView: UITableView = {
// init with a frame to fix a conflict ('UIView-Encapsulated-Layout-Width' UIStackView:0x7f8c2b6c0590.width == 0)
let tableView = UITableView(frame: CGRect(x: 0, y: 0, width: 320, height: 320), style: .grouped)
tableView.translatesAutoresizingMaskIntoConstraints = false
@@ -95,13 +92,13 @@ class SettingsViewController: UIViewController, NeedsDependency {
tableView.rowHeight = UITableView.automaticDimension
tableView.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
- tableView.register(SettingsAppearanceTableViewCell.self, forCellReuseIdentifier: "SettingsAppearanceTableViewCell")
- tableView.register(SettingsToggleTableViewCell.self, forCellReuseIdentifier: "SettingsToggleTableViewCell")
- tableView.register(SettingsLinkTableViewCell.self, forCellReuseIdentifier: "SettingsLinkTableViewCell")
+ tableView.register(SettingsAppearanceTableViewCell.self, forCellReuseIdentifier: String(describing: SettingsAppearanceTableViewCell.self))
+ tableView.register(SettingsToggleTableViewCell.self, forCellReuseIdentifier: String(describing: SettingsToggleTableViewCell.self))
+ tableView.register(SettingsLinkTableViewCell.self, forCellReuseIdentifier: String(describing: SettingsLinkTableViewCell.self))
return tableView
}()
- lazy var footerView: UIView = {
+ lazy var tableFooterView: UIView = {
// init with a frame to fix a conflict ('UIView-Encapsulated-Layout-Height' UIStackView:0x7ffe41e47da0.height == 0)
let view = UIStackView(frame: CGRect(x: 0, y: 0, width: 320, height: 320))
view.isLayoutMarginsRelativeArrangement = true
@@ -143,14 +140,30 @@ class SettingsViewController: UIViewController, NeedsDependency {
// MAKR: - Private methods
private func bindViewModel() {
- let input = SettingsViewModel.Input()
- _ = viewModel.transform(input: input)
+ self.whoButton.setTitle(viewModel.setting.value.activeSubscription?.policy.title, for: .normal)
+ viewModel.setting
+ .sink { [weak self] setting in
+ guard let self = self else { return }
+ self.notificationPolicySubscription = ManagedObjectObserver.observe(object: setting)
+ .sink { _ in
+ // do nothing
+ } receiveValue: { [weak self] change in
+ guard let self = self else { return }
+ guard case let .update(object) = change.changeType,
+ let setting = object as? Setting else { return }
+ if let activeSubscription = setting.activeSubscription {
+ self.whoButton.setTitle(activeSubscription.policy.title, for: .normal)
+ } else {
+ assertionFailure()
+ }
+ }
+ }
+ .store(in: &disposeBag)
}
private func setupView() {
view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
setupNavigation()
- setupTableView()
view.addSubview(tableView)
NSLayoutConstraint.activate([
@@ -159,6 +172,7 @@ class SettingsViewController: UIViewController, NeedsDependency {
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
+ setupTableView()
}
private func setupNavigation() {
@@ -177,35 +191,12 @@ class SettingsViewController: UIViewController, NeedsDependency {
}
private func setupTableView() {
- viewModel.dataSource = UITableViewDiffableDataSource(tableView: tableView, cellProvider: { [weak self] (tableView, indexPath, item) -> UITableViewCell? in
- guard let self = self else { return nil }
-
- switch item {
- case .apperance(let item):
- guard let cell = tableView.dequeueReusableCell(withIdentifier: "SettingsAppearanceTableViewCell") as? SettingsAppearanceTableViewCell else {
- assertionFailure()
- return nil
- }
- cell.update(with: item, delegate: self)
- return cell
- case .notification(let item):
- guard let cell = tableView.dequeueReusableCell(withIdentifier: "SettingsToggleTableViewCell") as? SettingsToggleTableViewCell else {
- assertionFailure()
- return nil
- }
- cell.update(with: item, delegate: self)
- return cell
- case .boringZone(let item), .spicyZone(let item):
- guard let cell = tableView.dequeueReusableCell(withIdentifier: "SettingsLinkTableViewCell") as? SettingsLinkTableViewCell else {
- assertionFailure()
- return nil
- }
- cell.update(with: item)
- return cell
- }
- })
-
- tableView.tableFooterView = footerView
+ viewModel.setupDiffableDataSource(
+ for: tableView,
+ settingsAppearanceTableViewCellDelegate: self,
+ settingsToggleCellDelegate: self
+ )
+ tableView.tableFooterView = tableFooterView
}
func alertToSignout() {
@@ -218,7 +209,7 @@ class SettingsViewController: UIViewController, NeedsDependency {
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil)
let signOutAction = UIAlertAction(title: L10n.Common.Alerts.SignOut.confirm, style: .destructive) { [weak self] _ in
guard let self = self else { return }
- self.signout()
+ self.signOut()
}
alertController.addAction(cancelAction)
alertController.addAction(signOutAction)
@@ -229,7 +220,7 @@ class SettingsViewController: UIViewController, NeedsDependency {
)
}
- func signout() {
+ func signOut() {
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
return
}
@@ -258,8 +249,11 @@ class SettingsViewController: UIViewController, NeedsDependency {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s:", ((#file as NSString).lastPathComponent), #line, #function)
}
- // Mark: - Actions
- @objc func doneButtonDidClick() {
+}
+
+// Mark: - Actions
+extension SettingsViewController {
+ @objc private func doneButtonDidClick() {
dismiss(animated: true, completion: nil)
}
}
@@ -268,51 +262,39 @@ extension SettingsViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let sections = viewModel.dataSource.snapshot().sectionIdentifiers
guard section < sections.count else { return nil }
- let sectionData = sections[section]
+
+ let sectionIdentifier = sections[section]
let header: SettingsSectionHeader
- if section == 1 {
+ switch sectionIdentifier {
+ case .notifications:
header = SettingsSectionHeader(
frame: CGRect(x: 0, y: 0, width: 375, height: 66),
customView: notifySectionHeader)
- header.update(title: sectionData.title)
-
- if let setting = self.viewModel.setting.value, let trigger = setting.triggerBy {
- whoButton.setTitle(trigger, for: .normal)
- } else {
- let anyone = L10n.Scene.Settings.Section.Notifications.Trigger.anyone
- whoButton.setTitle(anyone, for: .normal)
- }
- } else {
+ header.update(title: sectionIdentifier.title)
+ default:
header = SettingsSectionHeader(frame: CGRect(x: 0, y: 0, width: 375, height: 66))
- header.update(title: sectionData.title)
+ header.update(title: sectionIdentifier.title)
}
-
header.preservesSuperviewLayoutMargins = true
-
+
return header
}
-
+
// remove the gap of table's footer
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
-
return UIView()
}
-
+
// remove the gap of table's footer
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
- return 0
+ return CGFloat.leastNonzeroMagnitude
}
-
+
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
- let snapshot = self.viewModel.dataSource.snapshot()
- let sectionIds = snapshot.sectionIdentifiers
- guard indexPath.section < sectionIds.count else { return }
- let sectionIdentifier = sectionIds[indexPath.section]
- let items = snapshot.itemIdentifiers(inSection: sectionIdentifier)
- guard indexPath.row < items.count else { return }
- let item = items[indexPath.item]
-
+ guard let dataSource = viewModel.dataSource else { return }
+ let item = dataSource.itemIdentifier(for: indexPath)
+
switch item {
case .boringZone:
guard let url = viewModel.privacyURL else { break }
@@ -331,7 +313,7 @@ extension SettingsViewController: UITableViewDelegate {
ImageDownloader.defaultURLCache().removeAllCachedResponses()
let cleanedDiskBytes = ImageDownloader.defaultURLCache().currentDiskUsage
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: diskBytes %d", ((#file as NSString).lastPathComponent), #line, #function, cleanedDiskBytes)
-
+
// clean Kingfisher Cache
KingfisherManager.shared.cache.clearDiskCache()
}
@@ -347,82 +329,77 @@ 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 }
+ func updateTrigger(policy: Mastodon.API.Subscriptions.Policy) {
+ let objectID = self.viewModel.setting.value.objectID
+ let managedObjectContext = context.backgroundManagedObjectContext
- 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
+ managedObjectContext.performChanges {
+ let setting = managedObjectContext.object(with: objectID) as! Setting
+ let (subscription, _) = APIService.CoreData.createOrFetchSubscription(
+ into: managedObjectContext,
+ setting: setting,
+ policy: policy
+ )
+ let now = Date()
+ subscription.update(activedAt: now)
+ setting.didUpdate(at: now)
}
- }
-
- func updateAlert(title: String?, isOn: Bool) {
- guard let title = title else { return }
- guard let settings = self.viewModel.setting.value else { return }
- guard let triggerBy = settings.triggerBy else { return }
-
- if let alerts = settings.subscription?.first(where: { (s) -> Bool in
- return s.type == settings.triggerBy
- })?.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))
+ .sink { _ in
+ // do nothing
+ } receiveValue: { _ in
+ // do nohting
}
+ .store(in: &disposeBag)
}
}
+// MARK: - SettingsAppearanceTableViewCellDelegate
extension SettingsViewController: SettingsAppearanceTableViewCellDelegate {
- func settingsAppearanceCell(_ view: SettingsAppearanceTableViewCell, didSelect: SettingsItem.AppearanceMode) {
- guard let setting = self.viewModel.setting.value else { return }
-
+ func settingsAppearanceCell(_ cell: SettingsAppearanceTableViewCell, didSelectAppearanceMode appearanceMode: SettingsItem.AppearanceMode) {
+ guard let dataSource = viewModel.dataSource else { return }
+ guard let indexPath = tableView.indexPath(for: cell) else { return }
+ let item = dataSource.itemIdentifier(for: indexPath)
+ guard case let .apperance(settingObjectID) = item else { return }
+
context.managedObjectContext.performChanges {
- setting.update(appearance: didSelect.rawValue)
+ let setting = self.context.managedObjectContext.object(with: settingObjectID) as! Setting
+ setting.update(appearanceRaw: appearanceMode.rawValue)
}
- .sink { (_) in
- // change light / dark mode
- var overrideUserInterfaceStyle: UIUserInterfaceStyle!
- switch didSelect {
- case .automatic:
- overrideUserInterfaceStyle = .unspecified
- case .light:
- overrideUserInterfaceStyle = .light
- case .dark:
- overrideUserInterfaceStyle = .dark
- }
- view.window?.overrideUserInterfaceStyle = overrideUserInterfaceStyle
+ .sink { _ in
+ // do nothing
}.store(in: &disposeBag)
}
}
extension SettingsViewController: SettingsToggleCellDelegate {
- func settingsToggleCell(_ cell: SettingsToggleTableViewCell, didChangeStatus: Bool) {
- updateAlert(title: cell.data?.title, isOn: didChangeStatus)
+ func settingsToggleCell(_ cell: SettingsToggleTableViewCell, switchValueDidChange switch: UISwitch) {
+ guard let dataSource = viewModel.dataSource else { return }
+ guard let indexPath = tableView.indexPath(for: cell) else { return }
+ let item = dataSource.itemIdentifier(for: indexPath)
+ switch item {
+ case .notification(let settingObjectID, let switchMode):
+ let isOn = `switch`.isOn
+ let managedObjectContext = context.backgroundManagedObjectContext
+ managedObjectContext.performChanges {
+ let setting = managedObjectContext.object(with: settingObjectID) as! Setting
+ guard let subscription = setting.activeSubscription else { return }
+ let alert = subscription.alert
+ switch switchMode {
+ case .favorite: alert.update(favourite: isOn)
+ case .follow: alert.update(follow: isOn)
+ case .reblog: alert.update(reblog: isOn)
+ case .mention: alert.update(mention: isOn)
+ }
+ // trigger setting update
+ alert.subscription.setting?.didUpdate(at: Date())
+ }
+ .sink { _ in
+ // do nothing
+ }
+ .store(in: &disposeBag)
+ default:
+ break
+ }
}
}
@@ -436,43 +413,6 @@ extension SettingsViewController: ActiveLabelDelegate {
}
}
-extension SettingsViewController {
- static func updateOverrideUserInterfaceStyle(window: UIWindow?) {
- guard let box = AppContext.shared.authenticationService.activeMastodonAuthenticationBox.value else {
- return
- }
-
- guard let setting: Setting? = {
- let domain = box.domain
- let request = Setting.sortedFetchRequest
- request.predicate = Setting.predicate(domain: domain, userID: box.userID)
- request.fetchLimit = 1
- request.returnsObjectsAsFaults = false
- do {
- return try AppContext.shared.managedObjectContext.fetch(request).first
- } catch {
- assertionFailure(error.localizedDescription)
- return nil
- }
- }() else { return }
-
- guard let didSelect = SettingsItem.AppearanceMode(rawValue: setting?.appearance ?? "") else {
- return
- }
-
- var overrideUserInterfaceStyle: UIUserInterfaceStyle!
- switch didSelect {
- case .automatic:
- overrideUserInterfaceStyle = .unspecified
- case .light:
- overrideUserInterfaceStyle = .light
- case .dark:
- overrideUserInterfaceStyle = .dark
- }
- window?.overrideUserInterfaceStyle = overrideUserInterfaceStyle
- }
-}
-
#if canImport(SwiftUI) && DEBUG
import SwiftUI
diff --git a/Mastodon/Scene/Settings/SettingsViewModel.swift b/Mastodon/Scene/Settings/SettingsViewModel.swift
index f7ee4c71b..c168b5611 100644
--- a/Mastodon/Scene/Settings/SettingsViewModel.swift
+++ b/Mastodon/Scene/Settings/SettingsViewModel.swift
@@ -13,38 +13,21 @@ import MastodonSDK
import UIKit
import os.log
-class SettingsViewModel: NSObject, NeedsDependency {
- // confirm set only once
- weak var context: AppContext! { willSet { precondition(context == nil) } }
- weak var coordinator: SceneCoordinator! { willSet { precondition(coordinator == nil) } }
+class SettingsViewModel {
- var dataSource: UITableViewDiffableDataSource!
var disposeBag = Set()
+
+ let context: AppContext
+
+ // input
+ let setting: CurrentValueSubject
var updateDisposeBag = Set()
var createDisposeBag = Set()
let viewDidLoad = PassthroughSubject()
- lazy var fetchResultsController: NSFetchedResultsController = {
- let fetchRequest = Setting.sortedFetchRequest
- if let box =
- self.context.authenticationService.activeMastodonAuthenticationBox.value {
- let domain = box.domain
- fetchRequest.predicate = Setting.predicate(domain: domain, userID: box.userID)
- }
-
- fetchRequest.fetchLimit = 1
- fetchRequest.returnsObjectsAsFaults = false
- let controller = NSFetchedResultsController(
- fetchRequest: fetchRequest,
- managedObjectContext: context.managedObjectContext,
- sectionNameKeyPath: nil,
- cacheName: nil
- )
- controller.delegate = self
- return controller
- }()
- let setting = CurrentValueSubject(nil)
+ // output
+ var dataSource: UITableViewDiffableDataSource!
/// create a subscription when:
/// - does not has one
/// - does not find subscription for selected trigger when change trigger
@@ -54,22 +37,6 @@ class SettingsViewModel: NSObject, NeedsDependency {
/// - change switch for specified alerts
let updateSubscriptionSubject = PassthroughSubject<(triggerBy: String, values: [Bool?]), Never>()
- lazy var notificationDefaultValue: [String: [Bool?]] = {
- let followerSwitchItems: [Bool?] = [true, nil, true, true]
- let anyoneSwitchItems: [Bool?] = [true, true, true, true]
- let noOneSwitchItems: [Bool?] = [nil, nil, nil, nil]
- let followSwitchItems: [Bool?] = [true, true, true, true]
-
- let anyone = L10n.Scene.Settings.Section.Notifications.Trigger.anyone
- let follower = L10n.Scene.Settings.Section.Notifications.Trigger.follower
- let follow = L10n.Scene.Settings.Section.Notifications.Trigger.follow
- let noOne = L10n.Scene.Settings.Section.Notifications.Trigger.noone
- return [anyone: anyoneSwitchItems,
- follower: followerSwitchItems,
- follow: followSwitchItems,
- noOne: noOneSwitchItems]
- }()
-
lazy var privacyURL: URL? = {
guard let box = AppContext.shared.authenticationService.activeMastodonAuthenticationBox.value else {
return nil
@@ -78,321 +45,151 @@ class SettingsViewModel: NSObject, NeedsDependency {
return Mastodon.API.privacyURL(domain: box.domain)
}()
- /// to store who trigger the notification.
- var triggerBy: String?
-
- struct Input {
- }
-
- struct Output {
- }
-
- init(context: AppContext, coordinator: SceneCoordinator) {
+ init(context: AppContext, setting: Setting) {
self.context = context
- self.coordinator = coordinator
+ self.setting = CurrentValueSubject(setting)
- super.init()
- }
-
- func transform(input: Input?) -> Output? {
- typealias SubscriptionResponse = Mastodon.Response.Content
-// createSubscriptionSubject
-// .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
-// .sink { _ in
-// } receiveValue: { [weak self] (arg) in
-// let (triggerBy, values) = arg
-// guard let self = self else {
-// return
-// }
-// guard let activeMastodonAuthenticationBox =
-// self.context.authenticationService.activeMastodonAuthenticationBox.value else {
-// return
-// }
-// guard values.count >= 4 else {
-// return
-// }
-//
-// self.createDisposeBag.removeAll()
-// typealias Query = Mastodon.API.Subscriptions.CreateSubscriptionQuery
-// let domain = activeMastodonAuthenticationBox.domain
-// let query = Query(
-// // FIXME: to replace the correct endpoint, p256dh, auth
-// endpoint: "http://www.google.com",
-// p256dh: "BLQELIDm-6b9Bl07YrEuXJ4BL_YBVQ0dvt9NQGGJxIQidJWHPNa9YrouvcQ9d7_MqzvGS9Alz60SZNCG3qfpk=",
-// auth: "4vQK-SvRAN5eo-8ASlrwA==",
-// favourite: values[0],
-// follow: values[1],
-// reblog: values[2],
-// mention: values[3],
-// poll: nil
-// )
-// self.context.apiService.changeSubscription(
-// domain: domain,
-// mastodonAuthenticationBox: activeMastodonAuthenticationBox,
-// query: query,
-// triggerBy: triggerBy,
-// userID: activeMastodonAuthenticationBox.userID
-// )
-// .sink { (_) in
-// } receiveValue: { (_) in
-// }
-// .store(in: &self.createDisposeBag)
-// }
-// .store(in: &disposeBag)
-
- updateSubscriptionSubject
- .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
- .sink { _ in
- } receiveValue: { [weak self] (arg) in
- let (triggerBy, values) = arg
- guard let self = self else {
- return
- }
- guard let activeMastodonAuthenticationBox =
- self.context.authenticationService.activeMastodonAuthenticationBox.value else {
- return
- }
- guard values.count >= 4 else {
- return
- }
-
- self.updateDisposeBag.removeAll()
- typealias Query = Mastodon.API.Subscriptions.UpdateSubscriptionQuery
- let domain = activeMastodonAuthenticationBox.domain
- let query = Query(
- data: Mastodon.API.Subscriptions.QueryData(
- alerts: Mastodon.API.Subscriptions.QueryData.Alerts(
- favourite: values[0],
- follow: values[1],
- reblog: values[2],
- mention: values[3],
- poll: nil
- )
- )
- )
- self.context.apiService.updateSubscription(
- domain: domain,
- mastodonAuthenticationBox: activeMastodonAuthenticationBox,
- query: query,
- triggerBy: triggerBy,
- userID: activeMastodonAuthenticationBox.userID
- )
- .sink { (_) in
- } receiveValue: { (_) in
- }
- .store(in: &self.updateDisposeBag)
- }
+ self.setting
+ .sink(receiveValue: { [weak self] setting in
+ guard let self = self else { return }
+ self.processDataSource(setting)
+ })
.store(in: &disposeBag)
-
- // build data for table view
- buildDataSource()
-
- // request subsription data for updating or initialization
- requestSubscription()
- return nil
- }
-
- // MARK: - Private methods
- fileprivate func processDataSource(_ settings: Setting?) {
- var snapshot = NSDiffableDataSourceSnapshot()
-
- // appearance
- let appearnceMode = SettingsItem.AppearanceMode(rawValue: settings?.appearance ?? "") ?? .automatic
- let appearanceItem = SettingsItem.apperance(item: appearnceMode)
- let appearance = SettingsSection.apperance(title: L10n.Scene.Settings.Section.Appearance.title, selectedMode:appearanceItem)
- snapshot.appendSections([appearance])
- snapshot.appendItems([appearanceItem])
-
- // notifications
- var switches: [Bool?]?
- 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
- } else if let triggerBy = settings?.triggerBy,
- let values = self.notificationDefaultValue[triggerBy] {
- switches = 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,
- L10n.Scene.Settings.Section.Notifications.mentions,]
- var notificationItems = [SettingsItem]()
- for (i, noti) in notifications.enumerated() {
- var value: Bool? = nil
- if let switches = switches, i < switches.count {
- value = switches[i]
- }
-
- let item = SettingsItem.notification(item: SettingsItem.NotificationSwitch(title: noti, isOn: value == true, enable: value != nil))
- notificationItems.append(item)
- }
- let notificationSection = SettingsSection.notifications(title: L10n.Scene.Settings.Section.Notifications.title, items: notificationItems)
- snapshot.appendSections([notificationSection])
- snapshot.appendItems(notificationItems)
-
- // boring zone
- let boringLinks = [L10n.Scene.Settings.Section.Boringzone.terms,
- L10n.Scene.Settings.Section.Boringzone.privacy]
- var boringLinkItems = [SettingsItem]()
- for l in boringLinks {
- let item = SettingsItem.boringZone(item: SettingsItem.Link(title: l, color: .systemBlue))
- boringLinkItems.append(item)
- }
- let boringSection = SettingsSection.boringZone(title: L10n.Scene.Settings.Section.Boringzone.title, items: boringLinkItems)
- snapshot.appendSections([boringSection])
- snapshot.appendItems(boringLinkItems)
-
- // spicy zone
- let spicyLinks = [L10n.Scene.Settings.Section.Spicyzone.clear,
- L10n.Scene.Settings.Section.Spicyzone.signout]
- var spicyLinkItems = [SettingsItem]()
- for l in spicyLinks {
- let item = SettingsItem.spicyZone(item: SettingsItem.Link(title: l, color: .systemRed))
- spicyLinkItems.append(item)
- }
- let spicySection = SettingsSection.spicyZone(title: L10n.Scene.Settings.Section.Spicyzone.title, items: spicyLinkItems)
- snapshot.appendSections([spicySection])
- snapshot.appendItems(spicyLinkItems)
-
- self.dataSource.apply(snapshot, animatingDifferences: false)
- }
-
- private func buildDataSource() {
- setting.sink { [weak self] (settings) in
- guard let self = self else { return }
- self.processDataSource(settings)
- }
- .store(in: &disposeBag)
- }
-
- private func requestSubscription() {
- 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
- }
-
- // should create a subscription whenever change trigger
- if let values = switches, let triggerBy = who {
- self.createSubscriptionSubject.send((triggerBy: triggerBy, values: values))
- }
- }
- .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 {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s:", ((#file as NSString).lastPathComponent), #line, #function)
}
+
}
-// MARK: - NSFetchedResultsControllerDelegate
-extension SettingsViewModel: NSFetchedResultsControllerDelegate {
+extension SettingsViewModel {
- func controllerWillChangeContent(_ controller: NSFetchedResultsController) {
- os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
- }
+ // MARK: - Private methods
+ private func processDataSource(_ setting: Setting) {
+ guard let dataSource = self.dataSource else { return }
+ var snapshot = NSDiffableDataSourceSnapshot()
- func controllerDidChangeContent(_ controller: NSFetchedResultsController) {
- guard controller === fetchResultsController else {
- return
+ // appearance
+ let appearanceItems = [SettingsItem.apperance(settingObjectID: setting.objectID)]
+ snapshot.appendSections([.apperance])
+ snapshot.appendItems(appearanceItems, toSection: .apperance)
+
+ let notificationItems = SettingsItem.NotificationSwitchMode.allCases.map { mode in
+ SettingsItem.notification(settingObjectID: setting.objectID, switchMode: mode)
+ }
+ snapshot.appendSections([.notifications])
+ snapshot.appendItems(notificationItems, toSection: .notifications)
+
+ // boring zone
+ let boringZoneSettingsItems: [SettingsItem] = {
+ let links: [SettingsItem.Link] = [
+ .termsOfService,
+ .privacyPolicy
+ ]
+ let items = links.map { SettingsItem.boringZone(item: $0) }
+ return items
+ }()
+ snapshot.appendSections([.boringZone])
+ snapshot.appendItems(boringZoneSettingsItems, toSection: .boringZone)
+
+ let spicyZoneSettingsItems: [SettingsItem] = {
+ let links: [SettingsItem.Link] = [
+ .clearMediaCache,
+ .signOut
+ ]
+ let items = links.map { SettingsItem.spicyZone(item: $0) }
+ return items
+ }()
+ snapshot.appendSections([.spicyZone])
+ snapshot.appendItems(spicyZoneSettingsItems, toSection: .spicyZone)
+
+ dataSource.apply(snapshot, animatingDifferences: false)
+ }
+
+}
+
+extension SettingsViewModel {
+ func setupDiffableDataSource(
+ for tableView: UITableView,
+ settingsAppearanceTableViewCellDelegate: SettingsAppearanceTableViewCellDelegate,
+ settingsToggleCellDelegate: SettingsToggleCellDelegate
+ ) {
+ dataSource = UITableViewDiffableDataSource(tableView: tableView) { [
+ weak self,
+ weak settingsAppearanceTableViewCellDelegate,
+ weak settingsToggleCellDelegate
+ ] tableView, indexPath, item -> UITableViewCell? in
+ guard let self = self else { return nil }
+
+ switch item {
+ case .apperance(let objectID):
+ let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsAppearanceTableViewCell.self), for: indexPath) as! SettingsAppearanceTableViewCell
+ self.context.managedObjectContext.performAndWait {
+ let setting = self.context.managedObjectContext.object(with: objectID) as! Setting
+ cell.update(with: setting.appearance)
+ ManagedObjectObserver.observe(object: setting)
+ .sink(receiveCompletion: { _ in
+ // do nothing
+ }, receiveValue: { [weak cell] change in
+ guard let cell = cell else { return }
+ guard case .update(let object) = change.changeType,
+ let setting = object as? Setting else { return }
+ cell.update(with: setting.appearance)
+ })
+ .store(in: &cell.disposeBag)
+ }
+ cell.delegate = settingsAppearanceTableViewCellDelegate
+ return cell
+ case .notification(let objectID, let switchMode):
+ let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsToggleTableViewCell.self), for: indexPath) as! SettingsToggleTableViewCell
+ self.context.managedObjectContext.performAndWait {
+ let setting = self.context.managedObjectContext.object(with: objectID) as! Setting
+ if let subscription = setting.activeSubscription {
+ SettingsViewModel.configureSettingToggle(cell: cell, switchMode: switchMode, subscription: subscription)
+ }
+ ManagedObjectObserver.observe(object: setting)
+ .sink(receiveCompletion: { _ in
+ // do nothing
+ }, receiveValue: { [weak cell] change in
+ guard let cell = cell else { return }
+ guard case .update(let object) = change.changeType,
+ let setting = object as? Setting else { return }
+ guard let subscription = setting.activeSubscription else { return }
+ SettingsViewModel.configureSettingToggle(cell: cell, switchMode: switchMode, subscription: subscription)
+ })
+ .store(in: &cell.disposeBag)
+ }
+ cell.delegate = settingsToggleCellDelegate
+ return cell
+ case .boringZone(let item), .spicyZone(let item):
+ let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsLinkTableViewCell.self), for: indexPath) as! SettingsLinkTableViewCell
+ cell.update(with: item)
+ return cell
+ }
}
- setting.value = fetchResultsController.fetchedObjects?.first
+ processDataSource(self.setting.value)
}
-
}
-enum SettingsSection: Hashable {
- case apperance(title: String, selectedMode: SettingsItem)
- case notifications(title: String, items: [SettingsItem])
- case boringZone(title: String, items: [SettingsItem])
- case spicyZone(title: String, items: [SettingsItem])
+extension SettingsViewModel {
- var title: String {
- switch self {
- case .apperance(let title, _),
- .notifications(let title, _),
- .boringZone(let title, _),
- .spicyZone(let title, _):
- return title
+ static func configureSettingToggle(
+ cell: SettingsToggleTableViewCell,
+ switchMode: SettingsItem.NotificationSwitchMode,
+ subscription: NotificationSubscription
+ ) {
+ cell.textLabel?.text = switchMode.title
+
+ let enabled: Bool?
+ switch switchMode {
+ case .favorite: enabled = subscription.alert.favourite
+ case .follow: enabled = subscription.alert.follow
+ case .reblog: enabled = subscription.alert.reblog
+ case .mention: enabled = subscription.alert.mention
}
+ cell.update(enabled: enabled)
}
-}
-
-enum SettingsItem: Hashable {
- enum AppearanceMode: String {
- case automatic
- case light
- case dark
- }
-
- struct NotificationSwitch: Hashable {
- let title: String
- let isOn: Bool
- let enable: Bool
- }
-
- struct Link: Hashable {
- let title: String
- let color: UIColor
- }
-
- case apperance(item: AppearanceMode)
- case notification(item: NotificationSwitch)
- case boringZone(item: Link)
- case spicyZone(item: Link)
+
}
diff --git a/Mastodon/Scene/Settings/View/Cell/SettingsAppearanceTableViewCell.swift b/Mastodon/Scene/Settings/View/Cell/SettingsAppearanceTableViewCell.swift
index a477661ee..44a7e7574 100644
--- a/Mastodon/Scene/Settings/View/Cell/SettingsAppearanceTableViewCell.swift
+++ b/Mastodon/Scene/Settings/View/Cell/SettingsAppearanceTableViewCell.swift
@@ -6,9 +6,10 @@
//
import UIKit
+import Combine
protocol SettingsAppearanceTableViewCellDelegate: class {
- func settingsAppearanceCell(_ view: SettingsAppearanceTableViewCell, didSelect: SettingsItem.AppearanceMode)
+ func settingsAppearanceCell(_ cell: SettingsAppearanceTableViewCell, didSelectAppearanceMode appearanceMode: SettingsItem.AppearanceMode)
}
class AppearanceView: UIView {
@@ -85,6 +86,9 @@ class AppearanceView: UIView {
}
class SettingsAppearanceTableViewCell: UITableViewCell {
+
+ var disposeBag = Set()
+
weak var delegate: SettingsAppearanceTableViewCellDelegate?
var appearance: SettingsItem.AppearanceMode = .automatic
@@ -123,6 +127,12 @@ class SettingsAppearanceTableViewCell: UITableViewCell {
tapGestureRecognizer.addTarget(self, action: #selector(appearanceDidTap(sender:)))
return tapGestureRecognizer
}()
+
+ override func prepareForReuse() {
+ super.prepareForReuse()
+
+ disposeBag.removeAll()
+ }
// MARK: - Methods
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
@@ -145,9 +155,8 @@ class SettingsAppearanceTableViewCell: UITableViewCell {
}
}
- func update(with data: SettingsItem.AppearanceMode, delegate: SettingsAppearanceTableViewCellDelegate?) {
+ func update(with data: SettingsItem.AppearanceMode) {
appearance = data
- self.delegate = delegate
automatic.selected = false
light.selected = false
@@ -200,6 +209,6 @@ class SettingsAppearanceTableViewCell: UITableViewCell {
}
guard let delegate = self.delegate else { return }
- delegate.settingsAppearanceCell(self, didSelect: appearance)
+ delegate.settingsAppearanceCell(self, didSelectAppearanceMode: appearance)
}
}
diff --git a/Mastodon/Scene/Settings/View/Cell/SettingsLinkTableViewCell.swift b/Mastodon/Scene/Settings/View/Cell/SettingsLinkTableViewCell.swift
index b5d0306d4..7fdbf7f02 100644
--- a/Mastodon/Scene/Settings/View/Cell/SettingsLinkTableViewCell.swift
+++ b/Mastodon/Scene/Settings/View/Cell/SettingsLinkTableViewCell.swift
@@ -8,6 +8,7 @@
import UIKit
class SettingsLinkTableViewCell: UITableViewCell {
+
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
@@ -22,10 +23,13 @@ class SettingsLinkTableViewCell: UITableViewCell {
super.setHighlighted(highlighted, animated: animated)
textLabel?.alpha = highlighted ? 0.6 : 1.0
}
-
- // MARK: - Methods
- func update(with data: SettingsItem.Link) {
- textLabel?.text = data.title
- textLabel?.textColor = data.color
+
+}
+
+// MARK: - Methods
+extension SettingsLinkTableViewCell {
+ func update(with link: SettingsItem.Link) {
+ textLabel?.text = link.title
+ textLabel?.textColor = link.textColor
}
}
diff --git a/Mastodon/Scene/Settings/View/Cell/SettingsToggleTableViewCell.swift b/Mastodon/Scene/Settings/View/Cell/SettingsToggleTableViewCell.swift
index b35b2b50f..b4a62635b 100644
--- a/Mastodon/Scene/Settings/View/Cell/SettingsToggleTableViewCell.swift
+++ b/Mastodon/Scene/Settings/View/Cell/SettingsToggleTableViewCell.swift
@@ -6,18 +6,21 @@
//
import UIKit
+import Combine
protocol SettingsToggleCellDelegate: class {
- func settingsToggleCell(_ cell: SettingsToggleTableViewCell, didChangeStatus: Bool)
+ func settingsToggleCell(_ cell: SettingsToggleTableViewCell, switchValueDidChange switch: UISwitch)
}
class SettingsToggleTableViewCell: UITableViewCell {
- lazy var switchButton: UISwitch = {
+
+ var disposeBag = Set()
+
+ private(set) lazy var switchButton: UISwitch = {
let view = UISwitch(frame:.zero)
return view
}()
- var data: SettingsItem.NotificationSwitch?
weak var delegate: SettingsToggleCellDelegate?
// MARK: - Methods
@@ -27,21 +30,8 @@ class SettingsToggleTableViewCell: UITableViewCell {
}
required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- func update(with data: SettingsItem.NotificationSwitch, delegate: SettingsToggleCellDelegate?) {
- self.delegate = delegate
- self.data = data
- textLabel?.text = data.title
- switchButton.isOn = data.isOn
- setup(enable: data.enable)
- }
-
- // MARK: Actions
- @objc func valueDidChange(sender: UISwitch) {
- guard let delegate = delegate else { return }
- delegate.settingsToggleCell(self, didChangeStatus: sender.isOn)
+ super.init(coder: coder)
+ setupUI()
}
// MARK: Private methods
@@ -49,15 +39,27 @@ class SettingsToggleTableViewCell: UITableViewCell {
selectionStyle = .none
accessoryView = switchButton
- switchButton.addTarget(self, action: #selector(valueDidChange(sender:)), for: .valueChanged)
+ switchButton.addTarget(self, action: #selector(switchValueDidChange(sender:)), for: .valueChanged)
+ }
+
+}
+
+// MARK: - Actions
+extension SettingsToggleTableViewCell {
+
+ @objc private func switchValueDidChange(sender: UISwitch) {
+ guard let delegate = delegate else { return }
+ delegate.settingsToggleCell(self, switchValueDidChange: sender)
+ }
+
+}
+
+extension SettingsToggleTableViewCell {
+
+ func update(enabled: Bool?) {
+ switchButton.isEnabled = enabled != nil
+ textLabel?.textColor = enabled != nil ? Asset.Colors.Label.primary.color : Asset.Colors.Label.secondary.color
+ switchButton.isOn = enabled ?? false
}
- private func setup(enable: Bool) {
- if enable {
- textLabel?.textColor = Asset.Colors.Label.primary.color
- } else {
- textLabel?.textColor = Asset.Colors.Label.secondary.color
- }
- switchButton.isEnabled = enable
- }
}
diff --git a/Mastodon/Scene/Share/View/Content/TimelineHeaderView.swift b/Mastodon/Scene/Share/View/Content/TimelineHeaderView.swift
index b5e4c5bde..f095f6f44 100644
--- a/Mastodon/Scene/Share/View/Content/TimelineHeaderView.swift
+++ b/Mastodon/Scene/Share/View/Content/TimelineHeaderView.swift
@@ -5,6 +5,8 @@
// Created by MainasuK Cirno on 2021-4-6.
//
+import UIKit
+
final class TimelineHeaderView: UIView {
let iconImageView: UIImageView = {
diff --git a/Mastodon/Service/APIService/APIService+Subscriptions.swift b/Mastodon/Service/APIService/APIService+Subscriptions.swift
index 5260452ce..3e2d2a0aa 100644
--- a/Mastodon/Service/APIService/APIService+Subscriptions.swift
+++ b/Mastodon/Service/APIService/APIService+Subscriptions.swift
@@ -5,6 +5,7 @@
// Created by ihugo on 2021/4/9.
//
+import os.log
import Combine
import CoreData
import CoreDataStack
@@ -13,66 +14,14 @@ import MastodonSDK
extension APIService {
- func subscription(
+ func createSubscription(
+ subscriptionObjectID: NSManagedObjectID,
+ query: Mastodon.API.Subscriptions.CreateSubscriptionQuery,
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
) -> AnyPublisher, Error> {
let authorization = mastodonAuthenticationBox.userAuthorization
let domain = mastodonAuthenticationBox.domain
- let userID = mastodonAuthenticationBox.userID
- 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,
- triggerBy: triggerBy,
- setting: setting)
- }
- .setFailureType(to: Error.self)
- .map { _ in return response }
- .eraseToAnyPublisher()
- }.eraseToAnyPublisher()
- }
-
- func createSubscription(
- mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox,
- query: Mastodon.API.Subscriptions.CreateSubscriptionQuery,
- triggerBy: String,
- userID: String
- ) -> AnyPublisher, Error> {
- let authorization = mastodonAuthenticationBox.userAuthorization
- let domain = mastodonAuthenticationBox.domain
- let userID = mastodonAuthenticationBox.userID
-
- let setting = self.createSettingIfNeed(
- domain: domain,
- userId: userID,
- triggerBy: triggerBy
- )
return Mastodon.API.Subscriptions.createSubscription(
session: session,
domain: domain,
@@ -80,14 +29,18 @@ extension APIService {
query: query
)
.flatMap { response -> AnyPublisher, Error> in
- return self.backgroundManagedObjectContext.performChanges {
- _ = APIService.CoreData.createOrMergeSubscription(
- into: self.backgroundManagedObjectContext,
- entity: response.value,
- domain: domain,
- triggerBy: triggerBy,
- setting: setting
- )
+ os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: create subscription successful ", ((#file as NSString).lastPathComponent), #line, #function)
+
+ let managedObjectContext = self.backgroundManagedObjectContext
+ return managedObjectContext.performChanges {
+ guard let subscription = managedObjectContext.object(with: subscriptionObjectID) as? NotificationSubscription else {
+ assertionFailure()
+ return
+ }
+ subscription.endpoint = response.value.endpoint
+ subscription.serverKey = response.value.serverKey
+ subscription.userToken = authorization.accessToken
+ subscription.didUpdate(at: response.networkDate)
}
.setFailureType(to: Error.self)
.map { _ in return response }
@@ -95,72 +48,22 @@ extension APIService {
}.eraseToAnyPublisher()
}
- func updateSubscription(
- domain: String,
- mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox,
- query: Mastodon.API.Subscriptions.UpdateSubscriptionQuery,
- triggerBy: String,
- userID: String
- ) -> AnyPublisher, Error> {
+ func cancelSubscription(
+ mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
+ ) -> AnyPublisher, Error> {
let authorization = mastodonAuthenticationBox.userAuthorization
-
- let setting = self.createSettingIfNeed(domain: domain,
- userId: userID,
- triggerBy: triggerBy)
-
- return Mastodon.API.Subscriptions.updateSubscription(
+ let domain = mastodonAuthenticationBox.domain
+
+ return Mastodon.API.Subscriptions.removeSubscription(
session: session,
domain: domain,
- authorization: authorization,
- query: query
+ 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 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
+ .handleEvents(receiveOutput: { _ in
+ os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: cancel subscription successful", ((#file as NSString).lastPathComponent), #line, #function)
+ })
+ .eraseToAnyPublisher()
}
+
}
diff --git a/Mastodon/Service/APIService/CoreData/APIService+CoreData+Setting.swift b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Setting.swift
new file mode 100644
index 000000000..fb6879da9
--- /dev/null
+++ b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Setting.swift
@@ -0,0 +1,61 @@
+//
+// APIService+CoreData+Setting.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-4-25.
+//
+
+
+import os.log
+import Foundation
+import CoreData
+import CoreDataStack
+import MastodonSDK
+
+extension APIService.CoreData {
+
+ static func createOrMergeSetting(
+ into managedObjectContext: NSManagedObjectContext,
+ property: Setting.Property
+ ) -> (Subscription: Setting, isCreated: Bool) {
+ let oldSetting: Setting? = {
+ let request = Setting.sortedFetchRequest
+ request.predicate = Setting.predicate(domain: property.domain, userID: property.userID)
+ request.fetchLimit = 1
+ request.returnsObjectsAsFaults = false
+ return managedObjectContext.safeFetch(request).first
+ }()
+
+ if let oldSetting = oldSetting {
+ return (oldSetting, false)
+ } else {
+ let setting = Setting.insert(
+ into: managedObjectContext,
+ property: property
+ )
+ let policies: [Mastodon.API.Subscriptions.Policy] = [
+ .all,
+ .followed,
+ .follower,
+ .none
+ ]
+ let now = Date()
+ policies.forEach { policy in
+ let (subscription, _) = createOrFetchSubscription(
+ into: managedObjectContext,
+ setting: setting,
+ policy: policy
+ )
+ if policy == .all {
+ subscription.update(activedAt: now)
+ } else {
+ subscription.update(activedAt: now.addingTimeInterval(-10))
+ }
+ }
+
+
+ return (setting, true)
+ }
+ }
+
+}
diff --git a/Mastodon/Service/APIService/CoreData/APIService+CoreData+Subscriptions.swift b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Subscriptions.swift
index f5a4022ea..5e42a8abe 100644
--- a/Mastodon/Service/APIService/CoreData/APIService+CoreData+Subscriptions.swift
+++ b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Subscriptions.swift
@@ -13,96 +13,49 @@ import MastodonSDK
extension APIService.CoreData {
- static func createOrMergeSetting(
+ static func createOrFetchSubscription(
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, userID: userID)
- request.fetchLimit = 1
- request.returnsObjectsAsFaults = false
- do {
- return try managedObjectContext.fetch(request).first
- } catch {
- assertionFailure(error.localizedDescription)
- return nil
- }
- }()
-
- if let oldSetting = oldSetting {
- return (oldSetting, false)
- } else {
- let setting = Setting.insert(
- into: managedObjectContext,
- property: property)
- return (setting, true)
- }
- }
-
- static func createOrMergeSubscription(
- into managedObjectContext: NSManagedObjectContext,
- entity: Mastodon.Entity.Subscription,
- domain: String,
- triggerBy: String,
- setting: Setting
- ) -> (Subscription: Subscription, isCreated: Bool) {
+ setting: Setting,
+ policy: Mastodon.API.Subscriptions.Policy
+ ) -> (subscription: Subscription, isCreated: Bool) {
let oldSubscription: Subscription? = {
let request = Subscription.sortedFetchRequest
- request.predicate = Subscription.predicate(type: triggerBy)
+ request.predicate = Subscription.predicate(policyRaw: policy.rawValue)
request.fetchLimit = 1
request.returnsObjectsAsFaults = false
- do {
- return try managedObjectContext.fetch(request).first
- } catch {
- assertionFailure(error.localizedDescription)
- return nil
- }
+ return managedObjectContext.safeFetch(request).first
}()
- let property = Subscription.Property(
- endpoint: entity.endpoint,
- id: entity.id,
- serverKey: entity.serverKey,
- type: triggerBy
- )
- let alertEntity = entity.alerts
- let alert = SubscriptionAlerts.Property(
- favourite: alertEntity.favouriteNumber,
- follow: alertEntity.followNumber,
- mention: alertEntity.mentionNumber,
- poll: alertEntity.pollNumber,
- reblog: alertEntity.reblogNumber
- )
if let oldSubscription = oldSubscription {
- oldSubscription.updateIfNeed(property: property)
- if nil == oldSubscription.alert {
- oldSubscription.alert = SubscriptionAlerts.insert(
- into: managedObjectContext,
- property: alert
- )
- } else {
- oldSubscription.alert?.updateIfNeed(property: alert)
- }
-
- if oldSubscription.alert?.hasChanges == true || oldSubscription.hasChanges {
- // don't expand subscription if add existed subscription
- //setting.mutableSetValue(forKey: #keyPath(Setting.subscription)).add(oldSubscription)
- oldSubscription.didUpdate(at: Date())
- }
return (oldSubscription, false)
} else {
+ let subscriptionProperty = Subscription.Property(policyRaw: policy.rawValue)
let subscription = Subscription.insert(
into: managedObjectContext,
- property: property
+ property: subscriptionProperty,
+ setting: setting
)
+ let alertProperty = SubscriptionAlerts.Property(policy: policy)
subscription.alert = SubscriptionAlerts.insert(
into: managedObjectContext,
- property: alert)
- setting.mutableSetValue(forKey: #keyPath(Setting.subscription)).add(subscription)
+ property: alertProperty,
+ subscription: subscription
+ )
+
return (subscription, true)
}
}
+
+}
+
+extension APIService.CoreData {
+
+ static func merge(
+ subscription: Subscription,
+ property: Subscription.Property,
+ networkDate: Date
+ ) {
+ // TODO:
+ }
+
}
diff --git a/Mastodon/Service/AuthenticationService.swift b/Mastodon/Service/AuthenticationService.swift
index 89ce7a182..6b35486d6 100644
--- a/Mastodon/Service/AuthenticationService.swift
+++ b/Mastodon/Service/AuthenticationService.swift
@@ -15,6 +15,7 @@ import MastodonSDK
final class AuthenticationService: NSObject {
var disposeBag = Set()
+
// input
weak var apiService: APIService?
let managedObjectContext: NSManagedObjectContext // read-only
@@ -23,6 +24,7 @@ final class AuthenticationService: NSObject {
// output
let mastodonAuthentications = CurrentValueSubject<[MastodonAuthentication], Never>([])
+ let mastodonAuthenticationBoxes = CurrentValueSubject<[AuthenticationService.MastodonAuthenticationBox], Never>([])
let activeMastodonAuthentication = CurrentValueSubject(nil)
let activeMastodonAuthenticationBox = CurrentValueSubject(nil)
@@ -58,16 +60,24 @@ final class AuthenticationService: NSObject {
.assign(to: \.value, on: activeMastodonAuthentication)
.store(in: &disposeBag)
- activeMastodonAuthentication
- .map { authentication -> AuthenticationService.MastodonAuthenticationBox? in
- guard let authentication = authentication else { return nil }
- return AuthenticationService.MastodonAuthenticationBox(
- domain: authentication.domain,
- userID: authentication.userID,
- appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.appAccessToken),
- userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken)
- )
+ mastodonAuthentications
+ .map { authentications -> [AuthenticationService.MastodonAuthenticationBox] in
+ return authentications
+ .sorted(by: { $0.activedAt > $1.activedAt })
+ .compactMap { authentication -> AuthenticationService.MastodonAuthenticationBox? in
+ return AuthenticationService.MastodonAuthenticationBox(
+ domain: authentication.domain,
+ userID: authentication.userID,
+ appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.appAccessToken),
+ userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken)
+ )
+ }
}
+ .assign(to: \.value, on: mastodonAuthenticationBoxes)
+ .store(in: &disposeBag)
+
+ mastodonAuthenticationBoxes
+ .map { $0.first }
.assign(to: \.value, on: activeMastodonAuthenticationBox)
.store(in: &disposeBag)
@@ -114,16 +124,37 @@ extension AuthenticationService {
func signOutMastodonUser(domain: String, userID: MastodonUser.ID) -> AnyPublisher, Never> {
var isSignOut = false
- return backgroundManagedObjectContext.performChanges {
+ var _mastodonAutenticationBox: MastodonAuthenticationBox?
+ let managedObjectContext = backgroundManagedObjectContext
+ return managedObjectContext.performChanges {
let request = MastodonAuthentication.sortedFetchRequest
request.predicate = MastodonAuthentication.predicate(domain: domain, userID: userID)
request.fetchLimit = 1
- guard let mastodonAutentication = try? self.backgroundManagedObjectContext.fetch(request).first else {
+ guard let mastodonAutentication = try? managedObjectContext.fetch(request).first else {
return
}
- self.backgroundManagedObjectContext.delete(mastodonAutentication)
+ _mastodonAutenticationBox = AuthenticationService.MastodonAuthenticationBox(
+ domain: mastodonAutentication.domain,
+ userID: mastodonAutentication.userID,
+ appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: mastodonAutentication.appAccessToken),
+ userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: mastodonAutentication.userAccessToken)
+ )
+ managedObjectContext.delete(mastodonAutentication)
isSignOut = true
}
+ .flatMap { result -> AnyPublisher, Never> in
+ guard let apiService = self.apiService,
+ let mastodonAuthenticationBox = _mastodonAutenticationBox else {
+ return Just(result).eraseToAnyPublisher()
+ }
+
+ return apiService.cancelSubscription(
+ mastodonAuthenticationBox: mastodonAuthenticationBox
+ )
+ .map { _ in result }
+ .catch { _ in Just(result).eraseToAnyPublisher() }
+ .eraseToAnyPublisher()
+ }
.map { result in
return result.map { isSignOut }
}
diff --git a/Mastodon/Service/NotificationService.swift b/Mastodon/Service/NotificationService.swift
index 098fdff62..526c35883 100644
--- a/Mastodon/Service/NotificationService.swift
+++ b/Mastodon/Service/NotificationService.swift
@@ -19,69 +19,28 @@ final class NotificationService {
let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.NotificationService.working-queue")
// input
- weak var apiService: APIService?
weak var authenticationService: AuthenticationService?
let isNotificationPermissionGranted = CurrentValueSubject(false)
let deviceToken = CurrentValueSubject(nil)
- let mastodonAuthenticationBoxes = CurrentValueSubject<[AuthenticationService.MastodonAuthenticationBox], Never>([])
-
+
// output
/// [Token: UserID]
- let notificationSubscriptionDict: [String: NotificationSubscription] = [:]
+ let notificationSubscriptionDict: [String: NotificationViewModel] = [:]
init(
- apiService: APIService,
authenticationService: AuthenticationService
) {
- self.apiService = apiService
self.authenticationService = authenticationService
- authenticationService.mastodonAuthentications
- .handleEvents(receiveOutput: { [weak self] mastodonAuthentications in
- guard let self = self else { return }
-
- // request permission when sign-in
- guard !mastodonAuthentications.isEmpty else { return }
- self.requestNotificationPermission()
- })
- .map { authentications -> [AuthenticationService.MastodonAuthenticationBox] in
- return authentications.compactMap { authentication -> AuthenticationService.MastodonAuthenticationBox? in
- return AuthenticationService.MastodonAuthenticationBox(
- domain: authentication.domain,
- userID: authentication.userID,
- appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.appAccessToken),
- userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken)
- )
- }
- }
- .assign(to: \.value, on: mastodonAuthenticationBoxes)
- .store(in: &disposeBag)
-
deviceToken
.receive(on: DispatchQueue.main)
.sink { [weak self] deviceToken in
- guard let self = self else { return }
+ guard let _ = self else { return }
guard let deviceToken = deviceToken else { return }
let token = [UInt8](deviceToken).toHexString()
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: deviceToken: %s", ((#file as NSString).lastPathComponent), #line, #function, token)
}
.store(in: &disposeBag)
-
- Publishers.CombineLatest3(
- isNotificationPermissionGranted,
- deviceToken,
- mastodonAuthenticationBoxes
- )
- .sink { [weak self] isNotificationPermissionGranted, deviceToken, mastodonAuthenticationBoxes in
- guard let self = self else { return }
- guard isNotificationPermissionGranted else { return }
- guard let deviceToken = deviceToken else { return }
- self.registerNotificationSubscriptions(
- deviceToken: [UInt8](deviceToken).toHexString(),
- mastodonAuthenticationBoxes: mastodonAuthenticationBoxes
- )
- }
- .store(in: &disposeBag)
}
}
@@ -102,35 +61,14 @@ extension NotificationService {
// Enable or disable features based on the authorization.
}
}
+}
+
+extension NotificationService {
- private func registerNotificationSubscriptions(
- deviceToken: String,
- mastodonAuthenticationBoxes: [AuthenticationService.MastodonAuthenticationBox]
- ) {
- for mastodonAuthenticationBox in mastodonAuthenticationBoxes {
- guard let notificationSubscription = dequeueNotificationSubscription(mastodonAuthenticationBox: mastodonAuthenticationBox) else { continue }
- let token = NotificationSubscription.SubscribeToken(
- deviceToken: deviceToken,
- authenticationBox: mastodonAuthenticationBox
- )
- guard let subscription = subscribe(
- notificationSubscription: notificationSubscription,
- token: token
- ) else { continue }
-
- subscription
- .sink { completion in
- // handle error
- } receiveValue: { response in
- os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: did create subscription %s with userToken %s", ((#file as NSString).lastPathComponent), #line, #function, response.value.id, mastodonAuthenticationBox.userAuthorization.accessToken)
- // do nothing
- }
- .store(in: &self.disposeBag)
- }
- }
-
- private func dequeueNotificationSubscription(mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox) -> NotificationSubscription? {
- var _notificationSubscription: NotificationSubscription?
+ func dequeueNotificationViewModel(
+ mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
+ ) -> NotificationViewModel? {
+ var _notificationSubscription: NotificationViewModel?
workingQueue.sync {
let domain = mastodonAuthenticationBox.domain
let userID = mastodonAuthenticationBox.userID
@@ -139,56 +77,13 @@ extension NotificationService {
if let notificationSubscription = notificationSubscriptionDict[key] {
_notificationSubscription = notificationSubscription
} else {
- let notificationSubscription = NotificationSubscription(domain: domain, userID: userID)
+ let notificationSubscription = NotificationViewModel(domain: domain, userID: userID)
_notificationSubscription = notificationSubscription
}
}
return _notificationSubscription
}
- private func subscribe(
- notificationSubscription: NotificationSubscription,
- token: NotificationSubscription.SubscribeToken
- ) -> AnyPublisher, Error>? {
- guard let apiService = self.apiService else { return nil }
-
- if let oldToken = notificationSubscription.token {
- guard oldToken != token else { return nil }
- }
- notificationSubscription.token = token
-
- let appSecret = AppSecret.default
- let endpoint = appSecret.notificationEndpoint + "/" + token.deviceToken
- let p256dh = appSecret.uncompressionNotificationPublicKeyData
- let auth = appSecret.notificationAuth
-
- let query = Mastodon.API.Subscriptions.CreateSubscriptionQuery(
- subscription: Mastodon.API.Subscriptions.QuerySubscription(
- endpoint: endpoint,
- keys: Mastodon.API.Subscriptions.QuerySubscription.Keys(
- p256dh: p256dh,
- auth: auth
- )
- ),
- data: Mastodon.API.Subscriptions.QueryData(
- alerts: Mastodon.API.Subscriptions.QueryData.Alerts(
- favourite: true,
- follow: true,
- reblog: true,
- mention: true,
- poll: true
- )
- )
- )
-
- return apiService.createSubscription(
- mastodonAuthenticationBox: token.authenticationBox,
- query: query,
- triggerBy: "anyone",
- userID: token.authenticationBox.userID
- )
- }
-
static func createRandomAuthBytes() -> Data {
let byteCount = 16
var bytes = Data(count: byteCount)
@@ -198,7 +93,7 @@ extension NotificationService {
}
extension NotificationService {
- final class NotificationSubscription {
+ final class NotificationViewModel {
var disposeBag = Set()
@@ -206,36 +101,39 @@ extension NotificationService {
let domain: String
let userID: Mastodon.Entity.Account.ID
- var token: SubscribeToken?
+ // output
init(domain: String, userID: Mastodon.Entity.Account.ID) {
self.domain = domain
self.userID = userID
}
-
- struct SubscribeToken: Equatable {
-
- let deviceToken: String
- let authenticationBox: AuthenticationService.MastodonAuthenticationBox
- // TODO: set other parameter
-
- init(
- deviceToken: String,
- authenticationBox: AuthenticationService.MastodonAuthenticationBox
- ) {
- self.deviceToken = deviceToken
- self.authenticationBox = authenticationBox
- }
-
- static func == (
- lhs: NotificationService.NotificationSubscription.SubscribeToken,
- rhs: NotificationService.NotificationSubscription.SubscribeToken
- ) -> Bool {
- return lhs.deviceToken == rhs.deviceToken &&
- lhs.authenticationBox.domain == rhs.authenticationBox.domain &&
- lhs.authenticationBox.userID == rhs.authenticationBox.userID &&
- lhs.authenticationBox.userAuthorization.accessToken == rhs.authenticationBox.userAuthorization.accessToken
- }
- }
+ }
+}
+
+extension NotificationService.NotificationViewModel {
+ func createSubscribeQuery(
+ deviceToken: Data,
+ queryData: Mastodon.API.Subscriptions.QueryData,
+ mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
+ ) -> Mastodon.API.Subscriptions.CreateSubscriptionQuery {
+ let deviceToken = [UInt8](deviceToken).toHexString()
+
+ let appSecret = AppSecret.default
+ let endpoint = appSecret.notificationEndpoint + "/" + deviceToken
+ let p256dh = appSecret.uncompressionNotificationPublicKeyData
+ let auth = appSecret.notificationAuth
+
+ let query = Mastodon.API.Subscriptions.CreateSubscriptionQuery(
+ subscription: Mastodon.API.Subscriptions.QuerySubscription(
+ endpoint: endpoint,
+ keys: Mastodon.API.Subscriptions.QuerySubscription.Keys(
+ p256dh: p256dh,
+ auth: auth
+ )
+ ),
+ data: queryData
+ )
+
+ return query
}
}
diff --git a/Mastodon/Service/SettingService.swift b/Mastodon/Service/SettingService.swift
new file mode 100644
index 000000000..e097b4dc4
--- /dev/null
+++ b/Mastodon/Service/SettingService.swift
@@ -0,0 +1,162 @@
+//
+// SettingService.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-4-25.
+//
+
+import UIKit
+import Combine
+import CoreDataStack
+import MastodonSDK
+
+final class SettingService {
+
+ var disposeBag = Set()
+
+ private var currentSettingUpdateSubscription: AnyCancellable?
+
+ // input
+ weak var apiService: APIService?
+ weak var authenticationService: AuthenticationService?
+ weak var notificationService: NotificationService?
+
+ // output
+ let settingFetchedResultController: SettingFetchedResultController
+ let currentSetting = CurrentValueSubject(nil)
+
+ init(
+ apiService: APIService,
+ authenticationService: AuthenticationService,
+ notificationService: NotificationService
+ ) {
+ self.apiService = apiService
+ self.authenticationService = authenticationService
+ self.notificationService = notificationService
+ self.settingFetchedResultController = SettingFetchedResultController(
+ managedObjectContext: authenticationService.managedObjectContext,
+ additionalPredicate: nil
+ )
+
+ // create setting (if non-exist) for authenticated users
+ authenticationService.mastodonAuthenticationBoxes
+ .compactMap { [weak self] mastodonAuthenticationBoxes -> AnyPublisher<[AuthenticationService.MastodonAuthenticationBox], Never>? in
+ guard let self = self else { return nil }
+ guard let authenticationService = self.authenticationService else { return nil }
+ guard let activeMastodonAuthenticationBox = mastodonAuthenticationBoxes.first else { return nil }
+
+ let domain = activeMastodonAuthenticationBox.domain
+ let userID = activeMastodonAuthenticationBox.userID
+ return authenticationService.backgroundManagedObjectContext.performChanges {
+ _ = APIService.CoreData.createOrMergeSetting(
+ into: authenticationService.backgroundManagedObjectContext,
+ property: Setting.Property(
+ domain: domain,
+ userID: userID,
+ appearanceRaw: SettingsItem.AppearanceMode.automatic.rawValue
+ )
+ )
+ }
+ .map { _ in mastodonAuthenticationBoxes }
+ .eraseToAnyPublisher()
+ }
+ .sink { _ in
+ // do nothing
+ }
+ .store(in: &disposeBag)
+
+ // bind current setting
+ Publishers.CombineLatest(
+ authenticationService.activeMastodonAuthenticationBox,
+ settingFetchedResultController.settings
+ )
+ .sink { [weak self] activeMastodonAuthenticationBox, settings in
+ guard let self = self else { return }
+ guard let activeMastodonAuthenticationBox = activeMastodonAuthenticationBox else { return }
+ let currentSetting = settings.first(where: { setting in
+ return setting.domain == activeMastodonAuthenticationBox.domain &&
+ setting.userID == activeMastodonAuthenticationBox.userID
+ })
+ self.currentSetting.value = currentSetting
+ }
+ .store(in: &disposeBag)
+
+ // observe current setting
+ currentSetting
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] setting in
+ guard let self = self else { return }
+ guard let setting = setting else {
+ self.currentSettingUpdateSubscription = nil
+ return
+ }
+
+ self.currentSettingUpdateSubscription = ManagedObjectObserver.observe(object: setting)
+ .sink(receiveCompletion: { _ in
+ // do nothing
+ }, receiveValue: { change in
+ guard case .update(let object) = change.changeType,
+ let setting = object as? Setting else { return }
+
+ // observe apparance mode
+ switch setting.appearance {
+ case .automatic: UserDefaults.shared.customUserInterfaceStyle = .unspecified
+ case .light: UserDefaults.shared.customUserInterfaceStyle = .light
+ case .dark: UserDefaults.shared.customUserInterfaceStyle = .dark
+ }
+ })
+ }
+ .store(in: &disposeBag)
+
+ Publishers.CombineLatest(
+ notificationService.deviceToken,
+ currentSetting
+ )
+ .compactMap { [weak self] deviceToken, setting -> AnyPublisher, Error>? in
+ guard let self = self else { return nil }
+ guard let apiService = self.apiService else { return nil }
+ guard let deviceToken = deviceToken else { return nil }
+ guard let authenticationBox = self.authenticationService?.activeMastodonAuthenticationBox.value else { return nil }
+ guard let setting = setting else { return nil }
+ guard let subscription = setting.activeSubscription else { return nil }
+
+ guard setting.domain == authenticationBox.domain,
+ setting.userID == authenticationBox.userID else { return nil }
+
+ let _viewModel = self.notificationService?.dequeueNotificationViewModel(
+ mastodonAuthenticationBox: authenticationBox
+ )
+ guard let viewModel = _viewModel else { return nil }
+ let queryData = Mastodon.API.Subscriptions.QueryData(
+ policy: subscription.policy,
+ alerts: Mastodon.API.Subscriptions.QueryData.Alerts(
+ favourite: subscription.alert.favourite,
+ follow: subscription.alert.follow,
+ reblog: subscription.alert.reblog,
+ mention: subscription.alert.mention,
+ poll: subscription.alert.poll
+ )
+ )
+ let query = viewModel.createSubscribeQuery(
+ deviceToken: deviceToken,
+ queryData: queryData,
+ mastodonAuthenticationBox: authenticationBox
+ )
+
+ return apiService.createSubscription(
+ subscriptionObjectID: subscription.objectID,
+ query: query,
+ mastodonAuthenticationBox: authenticationBox
+ )
+ }
+ .switchToLatest()
+ .sink { _ in
+ // do nothing
+ } receiveValue: { _ in
+ // do nothing
+ }
+ .store(in: &disposeBag)
+
+ }
+
+}
diff --git a/Mastodon/State/AppContext.swift b/Mastodon/State/AppContext.swift
index 1ed2c283f..0c40ff127 100644
--- a/Mastodon/State/AppContext.swift
+++ b/Mastodon/State/AppContext.swift
@@ -29,6 +29,7 @@ class AppContext: ObservableObject {
let statusPrefetchingService: StatusPrefetchingService
let statusPublishService = StatusPublishService()
let notificationService: NotificationService
+ let settingService: SettingService
let documentStore: DocumentStore
private var documentStoreSubscription: AnyCancellable!
@@ -59,10 +60,16 @@ class AppContext: ObservableObject {
statusPrefetchingService = StatusPrefetchingService(
apiService: _apiService
)
- notificationService = NotificationService(
- apiService: _apiService,
+ let _notificationService = NotificationService(
authenticationService: _authenticationService
)
+ notificationService = _notificationService
+
+ settingService = SettingService(
+ apiService: _apiService,
+ authenticationService: _authenticationService,
+ notificationService: _notificationService
+ )
documentStore = DocumentStore()
documentStoreSubscription = documentStore.objectWillChange
diff --git a/Mastodon/Supporting Files/AppSharedName.swift b/Mastodon/Supporting Files/AppSharedName.swift
new file mode 100644
index 000000000..3570c68da
--- /dev/null
+++ b/Mastodon/Supporting Files/AppSharedName.swift
@@ -0,0 +1,12 @@
+//
+// AppSharedName.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-4-26.
+//
+
+import Foundation
+
+enum AppSharedName {
+ static let groupID = "group.org.joinmastodon.mastodon-temp"
+}
diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift
index 0f5e2bd59..1e6c13e41 100644
--- a/Mastodon/Supporting Files/SceneDelegate.swift
+++ b/Mastodon/Supporting Files/SceneDelegate.swift
@@ -6,10 +6,13 @@
//
import UIKit
+import Combine
import CoreDataStack
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
+ var observations = Set()
+
var window: UIWindow?
var coordinator: SceneCoordinator?
@@ -28,8 +31,11 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
sceneCoordinator.setupOnboardingIfNeeds(animated: false)
window.makeKeyAndVisible()
- // update `overrideUserInterfaceStyle` with current setting
- SettingsViewController.updateOverrideUserInterfaceStyle(window: window)
+ UserDefaults.shared.observe(\.customUserInterfaceStyle, options: [.initial, .new]) { [weak self] defaults, _ in
+ guard let self = self else { return }
+ self.window?.overrideUserInterfaceStyle = defaults.customUserInterfaceStyle
+ }
+ .store(in: &observations)
}
func sceneDidDisconnect(_ scene: UIScene) {
diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Push.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Push.swift
index f78c2c6c6..b66b89aad 100644
--- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Push.swift
+++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Push.swift
@@ -21,7 +21,7 @@ extension Mastodon.API.Subscriptions {
/// - Since: 2.4.0
/// - Version: 3.3.0
/// # Last Update
- /// 2021/4/9
+ /// 2021/4/25
/// # Reference
/// [Document](https://docs.joinmastodon.org/methods/notifications/push/)
/// - Parameters:
@@ -54,7 +54,7 @@ extension Mastodon.API.Subscriptions {
/// - Since: 2.4.0
/// - Version: 3.3.0
/// # Last Update
- /// 2021/4/9
+ /// 2021/4/25
/// # Reference
/// [Document](https://docs.joinmastodon.org/methods/notifications/push/)
/// - Parameters:
@@ -88,7 +88,7 @@ extension Mastodon.API.Subscriptions {
/// - Since: 2.4.0
/// - Version: 3.3.0
/// # Last Update
- /// 2021/4/9
+ /// 2021/4/25
/// # Reference
/// [Document](https://docs.joinmastodon.org/methods/notifications/push/)
/// - Parameters:
@@ -114,10 +114,45 @@ extension Mastodon.API.Subscriptions {
}
.eraseToAnyPublisher()
}
+
+ /// Remove current subscription
+ ///
+ /// Removes the current Web Push API subscription.
+ ///
+ /// - Since: 2.4.0
+ /// - Version: 3.3.0
+ /// # Last Update
+ /// 2021/4/26
+ /// # Reference
+ /// [Document](https://docs.joinmastodon.org/methods/notifications/push/)
+ /// - Parameters:
+ /// - session: `URLSession`
+ /// - domain: Mastodon instance domain. e.g. "example.com"
+ /// - authorization: User token. Could be nil if status is public
+ /// - Returns: `AnyPublisher` contains `Subscription` nested in the response
+ public static func removeSubscription(
+ session: URLSession,
+ domain: String,
+ authorization: Mastodon.API.OAuth.Authorization
+ ) -> AnyPublisher, Error> {
+ let request = Mastodon.API.delete(
+ url: pushEndpointURL(domain: domain),
+ query: nil,
+ authorization: authorization
+ )
+ return session.dataTaskPublisher(for: request)
+ .tryMap { data, response in
+ let value = try Mastodon.API.decode(type: Mastodon.Entity.EmptySubscription.self, from: data, response: response)
+ return Mastodon.Response.Content(value: value, response: response)
+ }
+ .eraseToAnyPublisher()
+ }
}
extension Mastodon.API.Subscriptions {
+ public typealias Policy = QueryData.Policy
+
public struct QuerySubscription: Codable {
let endpoint: String
let keys: Keys
@@ -142,9 +177,14 @@ extension Mastodon.API.Subscriptions {
}
public struct QueryData: Codable {
+ let policy: Policy?
let alerts: Alerts
- public init(alerts: Mastodon.API.Subscriptions.QueryData.Alerts) {
+ public init(
+ policy: Policy?,
+ alerts: Mastodon.API.Subscriptions.QueryData.Alerts
+ ) {
+ self.policy = policy
self.alerts = alerts
}
@@ -163,8 +203,39 @@ extension Mastodon.API.Subscriptions {
self.poll = poll
}
}
+
+ public enum Policy: RawRepresentable, Codable {
+ case all
+ case followed
+ case follower
+ case none
+
+ case _other(String)
+
+ public init?(rawValue: String) {
+ switch rawValue {
+ case "all": self = .all
+ case "followed": self = .followed
+ case "follower": self = .follower
+ case "none": self = .none
+
+ default: self = ._other(rawValue)
+ }
+ }
+
+ public var rawValue: String {
+ switch self {
+ case .all: return "all"
+ case .followed: return "followed"
+ case .follower: return "follower"
+ case .none: return "none"
+ case ._other(let value): return value
+ }
+ }
+ }
}
+
public struct CreateSubscriptionQuery: Codable, PostQuery {
let subscription: QuerySubscription
let data: QueryData
diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift
index 1a4496ed3..4fcc8fc9e 100644
--- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift
+++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift
@@ -151,6 +151,14 @@ extension Mastodon.API {
) -> URLRequest {
return buildRequest(url: url, method: .PUT, query: query, authorization: authorization)
}
+
+ static func delete(
+ url: URL,
+ query: DeleteQuery?,
+ authorization: OAuth.Authorization?
+ ) -> URLRequest {
+ return buildRequest(url: url, method: .DELETE, query: query, authorization: authorization)
+ }
private static func buildRequest(
url: URL,
diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Subscription.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Subscription.swift
index 3ae5718e6..e968c32d6 100644
--- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Subscription.swift
+++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Subscription.swift
@@ -1,5 +1,5 @@
//
-// File.swift
+// Mastodon+Entity+Subscription.swift
//
//
// Created by ihugo on 2021/4/9.
@@ -14,7 +14,7 @@ extension Mastodon.Entity {
/// - Since: 2.4.0
/// - Version: 3.3.0
/// # Last Update
- /// 2021/4/9
+ /// 2021/4/26
/// # Reference
/// [Document](https://docs.joinmastodon.org/entities/pushsubscription/)
public struct Subscription: Codable {
@@ -33,30 +33,19 @@ extension Mastodon.Entity {
public struct Alerts: Codable {
public let follow: Bool?
+ public let followRequest: Bool?
public let favourite: Bool?
public let reblog: Bool?
public let mention: Bool?
public let poll: Bool?
- public var followNumber: NSNumber? {
- guard let value = follow else { return nil }
- return NSNumber(booleanLiteral: value)
- }
- public var favouriteNumber: NSNumber? {
- guard let value = favourite else { return nil }
- return NSNumber(booleanLiteral: value)
- }
- public var reblogNumber: NSNumber? {
- guard let value = reblog else { return nil }
- return NSNumber(booleanLiteral: value)
- }
- public var mentionNumber: NSNumber? {
- guard let value = mention else { return nil }
- return NSNumber(booleanLiteral: value)
- }
- public var pollNumber: NSNumber? {
- guard let value = poll else { return nil }
- return NSNumber(booleanLiteral: value)
+ enum CodingKeys: String, CodingKey {
+ case follow
+ case followRequest = "follow_request"
+ case favourite
+ case reblog
+ case mention
+ case poll
}
}
@@ -74,4 +63,8 @@ extension Mastodon.Entity {
serverKey = try container.decode(String.self, forKey: .serverKey)
}
}
+
+ public struct EmptySubscription: Codable {
+
+ }
}
diff --git a/MastodonSDK/Sources/MastodonSDK/Query/Query.swift b/MastodonSDK/Sources/MastodonSDK/Query/Query.swift
index 39f6e3ec4..b729129bd 100644
--- a/MastodonSDK/Sources/MastodonSDK/Query/Query.swift
+++ b/MastodonSDK/Sources/MastodonSDK/Query/Query.swift
@@ -58,3 +58,5 @@ protocol PatchQuery: RequestQuery { }
// PUT
protocol PutQuery: RequestQuery { }
+// DELETE
+protocol DeleteQuery: RequestQuery { }