mirror of
https://github.com/mastodon/mastodon-ios.git
synced 2025-02-01 09:57:21 +01:00
feat: make push notification trigger update when change setting
This commit is contained in:
parent
9001289801
commit
cbd598739e
@ -172,14 +172,12 @@
|
||||
<relationship name="hashtag" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Tag"/>
|
||||
</entity>
|
||||
<entity name="Setting" representedClassName=".Setting" syncable="YES">
|
||||
<attribute name="appearance" optional="YES" attributeType="String"/>
|
||||
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="domain" optional="YES" attributeType="String"/>
|
||||
<attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="triggerBy" optional="YES" attributeType="String"/>
|
||||
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="userID" optional="YES" attributeType="String"/>
|
||||
<relationship name="subscription" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Subscription" inverseName="setting" inverseEntity="Subscription"/>
|
||||
<attribute name="appearanceRaw" attributeType="String"/>
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="domain" attributeType="String"/>
|
||||
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="userID" attributeType="String"/>
|
||||
<relationship name="subscriptions" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Subscription" inverseName="setting" inverseEntity="Subscription"/>
|
||||
</entity>
|
||||
<entity name="Status" representedClassName=".Status" syncable="YES">
|
||||
<attribute name="content" attributeType="String"/>
|
||||
@ -221,24 +219,27 @@
|
||||
<relationship name="tags" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Tag" inverseName="statuses" inverseEntity="Tag"/>
|
||||
</entity>
|
||||
<entity name="Subscription" representedClassName=".Subscription" syncable="YES">
|
||||
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="activedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="endpoint" optional="YES" attributeType="String"/>
|
||||
<attribute name="id" optional="YES" attributeType="String"/>
|
||||
<attribute name="policyRaw" attributeType="String"/>
|
||||
<attribute name="serverKey" optional="YES" attributeType="String"/>
|
||||
<attribute name="type" optional="YES" attributeType="String"/>
|
||||
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="alert" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SubscriptionAlerts" inverseName="subscription" inverseEntity="SubscriptionAlerts"/>
|
||||
<relationship name="setting" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Setting" inverseName="subscription" inverseEntity="Setting"/>
|
||||
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="userToken" optional="YES" attributeType="String"/>
|
||||
<relationship name="alert" maxCount="1" deletionRule="Cascade" destinationEntity="SubscriptionAlerts" inverseName="subscription" inverseEntity="SubscriptionAlerts"/>
|
||||
<relationship name="setting" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Setting" inverseName="subscriptions" inverseEntity="Setting"/>
|
||||
</entity>
|
||||
<entity name="SubscriptionAlerts" representedClassName=".SubscriptionAlerts" syncable="YES">
|
||||
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="favourite" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="follow" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="mention" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="poll" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="reblog" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="favouriteRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="followRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="followRequestRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="mentionRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="pollRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="reblogRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="subscription" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Subscription" inverseName="alert" inverseEntity="Subscription"/>
|
||||
<relationship name="subscription" maxCount="1" deletionRule="Nullify" destinationEntity="Subscription" inverseName="alert" inverseEntity="Subscription"/>
|
||||
</entity>
|
||||
<entity name="Tag" representedClassName=".Tag" syncable="YES">
|
||||
<attribute name="createAt" attributeType="Date" defaultDateTimeInterval="631123200" usesScalarValueType="NO"/>
|
||||
@ -263,10 +264,10 @@
|
||||
<element name="PollOption" positionX="0" positionY="0" width="128" height="134"/>
|
||||
<element name="PrivateNote" positionX="0" positionY="0" width="128" height="89"/>
|
||||
<element name="SearchHistory" positionX="0" positionY="0" width="128" height="104"/>
|
||||
<element name="Setting" positionX="72" positionY="162" width="128" height="149"/>
|
||||
<element name="Setting" positionX="72" positionY="162" width="128" height="119"/>
|
||||
<element name="Status" positionX="0" positionY="0" width="128" height="584"/>
|
||||
<element name="Subscription" positionX="81" positionY="171" width="128" height="149"/>
|
||||
<element name="SubscriptionAlerts" positionX="72" positionY="162" width="128" height="149"/>
|
||||
<element name="Subscription" positionX="81" positionY="171" width="128" height="179"/>
|
||||
<element name="SubscriptionAlerts" positionX="72" positionY="162" width="128" height="14"/>
|
||||
<element name="Tag" positionX="0" positionY="0" width="128" height="134"/>
|
||||
</elements>
|
||||
</model>
|
||||
</model>
|
@ -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])
|
||||
}
|
||||
|
@ -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<Subscription>?
|
||||
// one-to-many relationships
|
||||
@NSManaged public var subscriptions: Set<Subscription>?
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 = "<group>"; };
|
||||
5B90C45B262599800002E742 /* SettingsLinkTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsLinkTableViewCell.swift; sourceTree = "<group>"; };
|
||||
5B90C45C262599800002E742 /* SettingsSectionHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsSectionHeader.swift; sourceTree = "<group>"; };
|
||||
5B90C45D262599800002E742 /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
|
||||
5B90C46C26259B2C0002E742 /* Subscription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Subscription.swift; sourceTree = "<group>"; };
|
||||
5B90C46D26259B2C0002E742 /* Setting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Setting.swift; sourceTree = "<group>"; };
|
||||
5B90C47E26259BA90002E742 /* SubscriptionAlerts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionAlerts.swift; sourceTree = "<group>"; };
|
||||
@ -703,8 +715,21 @@
|
||||
DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAttachmentService.swift; sourceTree = "<group>"; };
|
||||
DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error.swift"; sourceTree = "<group>"; };
|
||||
DB6D1B23263684C600ACB481 /* UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaults.swift; sourceTree = "<group>"; };
|
||||
DB6D1B2A2636852000ACB481 /* AppSharedName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSharedName.swift; sourceTree = "<group>"; };
|
||||
DB6D1B3C2636857500ACB481 /* AppearancePreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePreference.swift; sourceTree = "<group>"; };
|
||||
DB6D1B43263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+API+Subscriptions+Policy.swift"; sourceTree = "<group>"; };
|
||||
DB6D9F222635195E008423CD /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; };
|
||||
DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationService+Decrypt.swift"; sourceTree = "<group>"; };
|
||||
DB6D9F4826353FD6008423CD /* Subscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Subscription.swift; sourceTree = "<group>"; };
|
||||
DB6D9F4F2635761F008423CD /* SubscriptionAlerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionAlerts.swift; sourceTree = "<group>"; };
|
||||
DB6D9F56263577D2008423CD /* APIService+CoreData+Setting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Setting.swift"; sourceTree = "<group>"; };
|
||||
DB6D9F6226357848008423CD /* SettingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingService.swift; sourceTree = "<group>"; };
|
||||
DB6D9F6E2635807F008423CD /* Setting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Setting.swift; sourceTree = "<group>"; };
|
||||
DB6D9F75263587C7008423CD /* SettingFetchedResultController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingFetchedResultController.swift; sourceTree = "<group>"; };
|
||||
DB6D9F7C26358ED4008423CD /* SettingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSection.swift; sourceTree = "<group>"; };
|
||||
DB6D9F8326358EEC008423CD /* SettingsItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsItem.swift; sourceTree = "<group>"; };
|
||||
DB6D9F9626367249008423CD /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
|
||||
DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarStackContainerButton.swift; sourceTree = "<group>"; };
|
||||
DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+PersistMemo.swift"; sourceTree = "<group>"; };
|
||||
DB71FD3B25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+PersistCache.swift"; sourceTree = "<group>"; };
|
||||
@ -1109,6 +1134,7 @@
|
||||
DB71FD4B25F8C80E00512AE1 /* StatusPrefetchingService.swift */,
|
||||
DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */,
|
||||
DB4924E126312AB200E9DB22 /* NotificationService.swift */,
|
||||
DB6D9F6226357848008423CD /* SettingService.swift */,
|
||||
);
|
||||
path = Service;
|
||||
sourceTree = "<group>";
|
||||
@ -1176,6 +1202,7 @@
|
||||
2D198648261C0B8500F0B013 /* SearchResultSection.swift */,
|
||||
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */,
|
||||
DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */,
|
||||
DB6D9F7C26358ED4008423CD /* SettingsSection.swift */,
|
||||
);
|
||||
path = Section;
|
||||
sourceTree = "<group>";
|
||||
@ -1232,6 +1259,7 @@
|
||||
DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */,
|
||||
DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */,
|
||||
DB44768A260B3F2100B66B82 /* CustomEmojiPickerItem.swift */,
|
||||
DB6D9F8326358EEC008423CD /* SettingsItem.swift */,
|
||||
);
|
||||
path = Item;
|
||||
sourceTree = "<group>";
|
||||
@ -1285,8 +1313,8 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5B90C457262599800002E742 /* View */,
|
||||
DB6D9F9626367249008423CD /* SettingsViewController.swift */,
|
||||
5B90C456262599800002E742 /* SettingsViewModel.swift */,
|
||||
5B90C45D262599800002E742 /* SettingsViewController.swift */,
|
||||
);
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
@ -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 = "<group>";
|
||||
@ -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 = "<group>";
|
||||
@ -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 = "<group>";
|
||||
@ -1787,6 +1822,7 @@
|
||||
0F20223826146553000C64BF /* Array.swift */,
|
||||
DBCC3B2F261440A50045B23D /* UITabBarController.swift */,
|
||||
DBCC3B35261440BA0045B23D /* UINavigationController.swift */,
|
||||
DB6D1B23263684C600ACB481 /* UserDefaults.swift */,
|
||||
);
|
||||
path = Extension;
|
||||
sourceTree = "<group>";
|
||||
@ -1994,6 +2030,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */,
|
||||
DB6D9F75263587C7008423CD /* SettingFetchedResultController.swift */,
|
||||
);
|
||||
path = FetchedResultsController;
|
||||
sourceTree = "<group>";
|
||||
@ -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 */,
|
||||
|
@ -7,7 +7,7 @@
|
||||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>14</integer>
|
||||
<integer>13</integer>
|
||||
</dict>
|
||||
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
@ -27,7 +27,7 @@
|
||||
<key>NotificationService.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>17</integer>
|
||||
<integer>14</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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<AnyCancellable>()
|
||||
|
||||
let fetchedResultsController: NSFetchedResultsController<Setting>
|
||||
|
||||
// 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<NSFetchRequestResult>, 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
|
||||
}
|
||||
}
|
67
Mastodon/Diffiable/Item/SettingsItem.swift
Normal file
67
Mastodon/Diffiable/Item/SettingsItem.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
24
Mastodon/Diffiable/Section/SettingsSection.swift
Normal file
24
Mastodon/Diffiable/Section/SettingsSection.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
24
Mastodon/Extension/CoreDataStack/Setting.swift
Normal file
24
Mastodon/Extension/CoreDataStack/Setting.swift
Normal file
@ -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
|
||||
}
|
||||
|
||||
}
|
20
Mastodon/Extension/CoreDataStack/Subscription.swift
Normal file
20
Mastodon/Extension/CoreDataStack/Subscription.swift
Normal file
@ -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
|
||||
}
|
||||
|
||||
}
|
28
Mastodon/Extension/CoreDataStack/SubscriptionAlerts.swift
Normal file
28
Mastodon/Extension/CoreDataStack/SubscriptionAlerts.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
31
Mastodon/Extension/UserDefaults.swift
Normal file
31
Mastodon/Extension/UserDefaults.swift
Normal file
@ -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<T: RawRepresentable>(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<T>(key: String) -> T? {
|
||||
get { return value(forKey: key) as? T }
|
||||
set { set(newValue, forKey: key) }
|
||||
}
|
||||
|
||||
}
|
20
Mastodon/Preference/AppearancePreference.swift
Normal file
20
Mastodon/Preference/AppearancePreference.swift
Normal file
@ -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 }
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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<AnyCancellable>()
|
||||
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
|
||||
|
||||
|
@ -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<SettingsSection, SettingsItem>!
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
let context: AppContext
|
||||
|
||||
// input
|
||||
let setting: CurrentValueSubject<Setting, Never>
|
||||
var updateDisposeBag = Set<AnyCancellable>()
|
||||
var createDisposeBag = Set<AnyCancellable>()
|
||||
|
||||
let viewDidLoad = PassthroughSubject<Void, Never>()
|
||||
lazy var fetchResultsController: NSFetchedResultsController<Setting> = {
|
||||
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<Setting?, Never>(nil)
|
||||
|
||||
// output
|
||||
var dataSource: UITableViewDiffableDataSource<SettingsSection, SettingsItem>!
|
||||
/// 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<Mastodon.Entity.Subscription>
|
||||
// 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<SettingsSection, SettingsItem>()
|
||||
|
||||
// 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<NSFetchRequestResult>) {
|
||||
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<SettingsSection, SettingsItem>()
|
||||
|
||||
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
|
||||
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)
|
||||
|
||||
}
|
||||
|
@ -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<AnyCancellable>()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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<AnyCancellable>()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,8 @@
|
||||
// Created by MainasuK Cirno on 2021-4-6.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class TimelineHeaderView: UIView {
|
||||
|
||||
let iconImageView: UIImageView = {
|
||||
|
@ -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<Mastodon.Response.Content<Mastodon.Entity.Subscription>, 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<Mastodon.Response.Content<Mastodon.Entity.Subscription>, 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<Mastodon.Response.Content<Mastodon.Entity.Subscription>, 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<Mastodon.Response.Content<Mastodon.Entity.Subscription>, 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<Mastodon.Response.Content<Mastodon.Entity.Subscription>, Error> {
|
||||
func cancelSubscription(
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.EmptySubscription>, 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<Mastodon.Response.Content<Mastodon.Entity.Subscription>, 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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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:
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import MastodonSDK
|
||||
final class AuthenticationService: NSObject {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
// 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<MastodonAuthentication?, Never>(nil)
|
||||
let activeMastodonAuthenticationBox = CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>(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<Result<Bool, Error>, 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<Result<Void, Error>, 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 }
|
||||
}
|
||||
|
@ -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<Bool, Never>(false)
|
||||
let deviceToken = CurrentValueSubject<Data?, Never>(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<Mastodon.Response.Content<Mastodon.Entity.Subscription>, 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<AnyCancellable>()
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
162
Mastodon/Service/SettingService.swift
Normal file
162
Mastodon/Service/SettingService.swift
Normal file
@ -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<AnyCancellable>()
|
||||
|
||||
private var currentSettingUpdateSubscription: AnyCancellable?
|
||||
|
||||
// input
|
||||
weak var apiService: APIService?
|
||||
weak var authenticationService: AuthenticationService?
|
||||
weak var notificationService: NotificationService?
|
||||
|
||||
// output
|
||||
let settingFetchedResultController: SettingFetchedResultController
|
||||
let currentSetting = CurrentValueSubject<Setting?, Never>(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<Mastodon.Response.Content<Mastodon.Entity.Subscription>, 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)
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
12
Mastodon/Supporting Files/AppSharedName.swift
Normal file
12
Mastodon/Supporting Files/AppSharedName.swift
Normal file
@ -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"
|
||||
}
|
@ -6,10 +6,13 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreDataStack
|
||||
|
||||
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
|
||||
var observations = Set<NSKeyValueObservation>()
|
||||
|
||||
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) {
|
||||
|
@ -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<Mastodon.Response.Content<Mastodon.Entity.EmptySubscription>, 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
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -58,3 +58,5 @@ protocol PatchQuery: RequestQuery { }
|
||||
// PUT
|
||||
protocol PutQuery: RequestQuery { }
|
||||
|
||||
// DELETE
|
||||
protocol DeleteQuery: RequestQuery { }
|
||||
|
Loading…
x
Reference in New Issue
Block a user