feature: settings
This commit is contained in:
parent
239b6bac4f
commit
191370e712
|
@ -154,6 +154,15 @@
|
|||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser"/>
|
||||
<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"/>
|
||||
<relationship name="subscription" 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"/>
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
|
@ -192,6 +201,26 @@
|
|||
<relationship name="replyTo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="replyFrom" inverseEntity="Status"/>
|
||||
<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="endpoint" optional="YES" attributeType="String"/>
|
||||
<attribute name="id" optional="YES" 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"/>
|
||||
</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="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="subscription" optional="YES" 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"/>
|
||||
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
|
@ -207,14 +236,16 @@
|
|||
<element name="Emoji" positionX="0" positionY="0" width="128" height="149"/>
|
||||
<element name="History" positionX="0" positionY="0" width="128" height="119"/>
|
||||
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="134"/>
|
||||
<element name="MastodonAuthentication" positionX="0" positionY="0" width="128" height="209"/>
|
||||
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="659"/>
|
||||
<element name="Mention" positionX="0" positionY="0" width="128" height="134"/>
|
||||
<element name="Poll" positionX="0" positionY="0" width="128" height="194"/>
|
||||
<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="MastodonAuthentication" positionX="18" positionY="162" width="128" height="209"/>
|
||||
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="629"/>
|
||||
<element name="Mention" positionX="9" positionY="108" width="128" height="134"/>
|
||||
<element name="Poll" positionX="72" positionY="162" width="128" height="194"/>
|
||||
<element name="PollOption" positionX="81" positionY="171" width="128" height="134"/>
|
||||
<element name="PrivateNote" positionX="72" positionY="153" width="128" height="89"/>
|
||||
<element name="Setting" positionX="72" positionY="162" width="128" height="134"/>
|
||||
<element name="Status" positionX="0" positionY="0" width="128" height="569"/>
|
||||
<element name="Tag" positionX="0" positionY="0" width="128" height="134"/>
|
||||
<element name="Subscription" positionX="81" positionY="171" width="128" height="149"/>
|
||||
<element name="SubscriptionAlerts" positionX="72" positionY="162" width="128" height="149"/>
|
||||
<element name="Tag" positionX="18" positionY="117" width="128" height="119"/>
|
||||
</elements>
|
||||
</model>
|
||||
</model>
|
|
@ -0,0 +1,84 @@
|
|||
//
|
||||
// Setting.swift
|
||||
// CoreDataStack
|
||||
//
|
||||
// Created by ihugo on 2021/4/9.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
@objc(Setting)
|
||||
public final class Setting: NSManagedObject {
|
||||
@NSManaged public var appearance: String?
|
||||
@NSManaged public var triggerBy: String?
|
||||
@NSManaged public var domain: String?
|
||||
|
||||
@NSManaged public private(set) var createdAt: Date
|
||||
@NSManaged public private(set) var updatedAt: Date
|
||||
|
||||
// relationships
|
||||
@NSManaged public var subscription: Set<Subscription>?
|
||||
}
|
||||
|
||||
public extension Setting {
|
||||
override func awakeFromInsert() {
|
||||
super.awakeFromInsert()
|
||||
setPrimitiveValue(Date(), forKey: #keyPath(Setting.createdAt))
|
||||
}
|
||||
|
||||
func didUpdate(at networkDate: Date) {
|
||||
self.updatedAt = networkDate
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
static func insert(
|
||||
into context: NSManagedObjectContext,
|
||||
property: Property
|
||||
) -> Setting {
|
||||
let setting: Setting = context.insertObject()
|
||||
setting.appearance = property.appearance
|
||||
setting.triggerBy = property.triggerBy
|
||||
setting.domain = property.domain
|
||||
return setting
|
||||
}
|
||||
|
||||
func update(appearance: String?) {
|
||||
guard appearance != self.appearance else { return }
|
||||
self.appearance = appearance
|
||||
didUpdate(at: Date())
|
||||
}
|
||||
|
||||
func update(triggerBy: String?) {
|
||||
guard triggerBy != self.triggerBy else { return }
|
||||
self.triggerBy = triggerBy
|
||||
didUpdate(at: Date())
|
||||
}
|
||||
}
|
||||
|
||||
public extension Setting {
|
||||
struct Property {
|
||||
public let appearance: String
|
||||
public let triggerBy: String
|
||||
public let domain: String
|
||||
|
||||
public init(appearance: String, triggerBy: String, domain: String) {
|
||||
self.appearance = appearance
|
||||
self.triggerBy = triggerBy
|
||||
self.domain = domain
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Setting: Managed {
|
||||
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
||||
return [NSSortDescriptor(keyPath: \Setting.createdAt, ascending: false)]
|
||||
}
|
||||
}
|
||||
|
||||
extension Setting {
|
||||
public static func predicate(domain: String) -> NSPredicate {
|
||||
return NSPredicate(format: "%K == %@", #keyPath(Setting.domain), domain)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
//
|
||||
// SettingNotification+CoreDataClass.swift
|
||||
// CoreDataStack
|
||||
//
|
||||
// Created by ihugo on 2021/4/9.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
@objc(Subscription)
|
||||
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 private(set) var createdAt: Date
|
||||
@NSManaged public private(set) var updatedAt: Date
|
||||
|
||||
// MARK: - relationships
|
||||
@NSManaged public var alert: SubscriptionAlerts?
|
||||
// MARK: holder
|
||||
@NSManaged public var setting: Setting?
|
||||
}
|
||||
|
||||
public extension Subscription {
|
||||
override func awakeFromInsert() {
|
||||
super.awakeFromInsert()
|
||||
setPrimitiveValue(Date(), forKey: #keyPath(Subscription.createdAt))
|
||||
}
|
||||
|
||||
func didUpdate(at networkDate: Date) {
|
||||
self.updatedAt = networkDate
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
static func insert(
|
||||
into context: NSManagedObjectContext,
|
||||
property: Property
|
||||
) -> Subscription {
|
||||
let setting: Subscription = context.insertObject()
|
||||
setting.id = property.id
|
||||
setting.endpoint = property.endpoint
|
||||
setting.serverKey = property.serverKey
|
||||
|
||||
return setting
|
||||
}
|
||||
}
|
||||
|
||||
public extension Subscription {
|
||||
struct Property {
|
||||
public let endpoint: String
|
||||
public let id: String
|
||||
public let serverKey: String
|
||||
public let type: 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Subscription: Managed {
|
||||
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
||||
return [NSSortDescriptor(keyPath: \Subscription.createdAt, ascending: false)]
|
||||
}
|
||||
}
|
||||
|
||||
extension Subscription {
|
||||
|
||||
public static func predicate(id: String) -> NSPredicate {
|
||||
return NSPredicate(format: "%K == %@", #keyPath(Subscription.id), id)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,142 +0,0 @@
|
|||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "ActiveLabel",
|
||||
"repositoryURL": "https://github.com/TwidereProject/ActiveLabel.swift",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "d6cf96e0ca4f2269021bcf8f11381ab57897f84a",
|
||||
"version": "4.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Alamofire",
|
||||
"repositoryURL": "https://github.com/Alamofire/Alamofire.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "eaf6e622dd41b07b251d8f01752eab31bc811493",
|
||||
"version": "5.4.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "AlamofireImage",
|
||||
"repositoryURL": "https://github.com/Alamofire/AlamofireImage.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "3e8edbeb75227f8542aa87f90240cf0424d6362f",
|
||||
"version": "4.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "AlamofireNetworkActivityIndicator",
|
||||
"repositoryURL": "https://github.com/Alamofire/AlamofireNetworkActivityIndicator",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "392bed083e8d193aca16bfa684ee24e4bcff0510",
|
||||
"version": "3.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "CommonOSLog",
|
||||
"repositoryURL": "https://github.com/MainasuK/CommonOSLog",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "c121624a30698e9886efe38aebb36ff51c01b6c2",
|
||||
"version": "0.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Kingfisher",
|
||||
"repositoryURL": "https://github.com/onevcat/Kingfisher.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "daebf8ddf974164d1b9a050c8231e263f3106b09",
|
||||
"version": "6.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Pageboy",
|
||||
"repositoryURL": "https://github.com/uias/Pageboy",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "34ecb6e7c4e0e07494960ab2f7cc9a02293915a6",
|
||||
"version": "3.6.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-nio",
|
||||
"repositoryURL": "https://github.com/apple/swift-nio.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "8da5c5a4e6c5084c296b9f39dc54f00be146e0fa",
|
||||
"version": "1.14.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-nio-zlib-support",
|
||||
"repositoryURL": "https://github.com/apple/swift-nio-zlib-support.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "37760e9a52030bb9011972c5213c3350fa9d41fd",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "SwiftyJSON",
|
||||
"repositoryURL": "https://github.com/SwiftyJSON/SwiftyJSON.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "2b6054efa051565954e1d2b9da831680026cd768",
|
||||
"version": "5.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Tabman",
|
||||
"repositoryURL": "https://github.com/uias/Tabman",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "bce2c87659c0ed868e6ef0aa1e05a330e202533f",
|
||||
"version": "2.11.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "ThirdPartyMailer",
|
||||
"repositoryURL": "https://github.com/vtourraine/ThirdPartyMailer.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "923c60ee7588da47db8cfc4e0f5b96e5e605ef84",
|
||||
"version": "1.7.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "TOCropViewController",
|
||||
"repositoryURL": "https://github.com/TimOliver/TOCropViewController.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "dad97167bf1be16aeecd109130900995dd01c515",
|
||||
"version": "2.6.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "TwitterTextEditor",
|
||||
"repositoryURL": "https://github.com/MainasuK/TwitterTextEditor",
|
||||
"state": {
|
||||
"branch": "feature/input-view",
|
||||
"revision": "1e565d13e3c26fc2bedeb418890df42f80d6e3d5",
|
||||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "UITextView+Placeholder",
|
||||
"repositoryURL": "https://github.com/MainasuK/UITextView-Placeholder",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "20f513ded04a040cdf5467f0891849b1763ede3b",
|
||||
"version": "1.4.1"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
}
|
|
@ -43,3 +43,23 @@ extension UIButton {
|
|||
}
|
||||
}
|
||||
|
||||
extension UIButton {
|
||||
// https://stackoverflow.com/questions/14523348/how-to-change-the-background-color-of-a-uibutton-while-its-highlighted
|
||||
private func image(withColor color: UIColor) -> UIImage? {
|
||||
let rect = CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0)
|
||||
UIGraphicsBeginImageContext(rect.size)
|
||||
let context = UIGraphicsGetCurrentContext()
|
||||
|
||||
context?.setFillColor(color.cgColor)
|
||||
context?.fill(rect)
|
||||
|
||||
let image = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
func setBackgroundColor(_ color: UIColor, for state: UIControl.State) {
|
||||
self.setBackgroundImage(image(withColor: color), for: state)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -581,6 +581,62 @@ internal enum L10n {
|
|||
internal static let confirm = L10n.tr("Localizable", "Scene.ServerRules.Button.Confirm")
|
||||
}
|
||||
}
|
||||
internal enum Settings {
|
||||
/// Settings
|
||||
internal static let title = L10n.tr("Localizable", "Scene.Settings.Title")
|
||||
internal enum Section {
|
||||
internal enum Appearance {
|
||||
/// Automatic
|
||||
internal static let automatic = L10n.tr("Localizable", "Scene.Settings.Section.Appearance.Automatic")
|
||||
/// Always Dark
|
||||
internal static let dark = L10n.tr("Localizable", "Scene.Settings.Section.Appearance.Dark")
|
||||
/// Always Light
|
||||
internal static let light = L10n.tr("Localizable", "Scene.Settings.Section.Appearance.Light")
|
||||
/// Appearance
|
||||
internal static let title = L10n.tr("Localizable", "Scene.Settings.Section.Appearance.Title")
|
||||
}
|
||||
internal enum BoringZone {
|
||||
/// Privacy Policy
|
||||
internal static let privacy = L10n.tr("Localizable", "Scene.Settings.Section.BoringZone.Privacy")
|
||||
/// Terms of Service
|
||||
internal static let terms = L10n.tr("Localizable", "Scene.Settings.Section.BoringZone.Terms")
|
||||
/// The Boring zone
|
||||
internal static let title = L10n.tr("Localizable", "Scene.Settings.Section.BoringZone.Title")
|
||||
}
|
||||
internal enum Notifications {
|
||||
/// Boosts my post
|
||||
internal static let boosts = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Boosts")
|
||||
/// Favorites my post
|
||||
internal static let favorites = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Favorites")
|
||||
/// Follows me
|
||||
internal static let follows = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Follows")
|
||||
/// Mentions me
|
||||
internal static let mentions = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Mentions")
|
||||
/// Notifications
|
||||
internal static let title = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Title")
|
||||
internal enum Trigger {
|
||||
/// anyone
|
||||
internal static let anyone = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Trigger.Anyone")
|
||||
/// anyone I follow
|
||||
internal static let follow = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Trigger.Follow")
|
||||
/// a follower
|
||||
internal static let follower = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Trigger.Follower")
|
||||
/// no one
|
||||
internal static let noOne = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Trigger.NoOne")
|
||||
/// Notify me when
|
||||
internal static let title = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Trigger.Title")
|
||||
}
|
||||
}
|
||||
internal enum SpicyZone {
|
||||
/// Clear Media Cache
|
||||
internal static let clear = L10n.tr("Localizable", "Scene.Settings.Section.SpicyZone.Clear")
|
||||
/// Sign Out
|
||||
internal static let signOut = L10n.tr("Localizable", "Scene.Settings.Section.SpicyZone.SignOut")
|
||||
/// The spicy zone
|
||||
internal static let title = L10n.tr("Localizable", "Scene.Settings.Section.SpicyZone.Title")
|
||||
}
|
||||
}
|
||||
}
|
||||
internal enum Welcome {
|
||||
/// Social networking\nback in your hands.
|
||||
internal static let slogan = L10n.tr("Localizable", "Scene.Welcome.Slogan")
|
||||
|
|
Binary file not shown.
|
@ -5,9 +5,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.851",
|
||||
"green" : "0.565",
|
||||
"red" : "0.169"
|
||||
"blue" : "217",
|
||||
"green" : "144",
|
||||
"red" : "43"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "0.200",
|
||||
"blue" : "0x80",
|
||||
"green" : "0x78",
|
||||
"red" : "0x78"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
12
Mastodon/Resources/Assets.xcassets/Settings/appearance.automatic.imageset/Contents.json
vendored
Normal file
12
Mastodon/Resources/Assets.xcassets/Settings/appearance.automatic.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "iPhone 11 Pro _ X - 1.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
Mastodon/Resources/Assets.xcassets/Settings/appearance.automatic.imageset/iPhone 11 Pro _ X - 1.pdf
vendored
Normal file
BIN
Mastodon/Resources/Assets.xcassets/Settings/appearance.automatic.imageset/iPhone 11 Pro _ X - 1.pdf
vendored
Normal file
Binary file not shown.
12
Mastodon/Resources/Assets.xcassets/Settings/appearance.dark.imageset/Contents.json
vendored
Normal file
12
Mastodon/Resources/Assets.xcassets/Settings/appearance.dark.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "iPhone 11 Pro _ X - 1 (2).pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
Mastodon/Resources/Assets.xcassets/Settings/appearance.dark.imageset/iPhone 11 Pro _ X - 1 (2).pdf
vendored
Normal file
BIN
Mastodon/Resources/Assets.xcassets/Settings/appearance.dark.imageset/iPhone 11 Pro _ X - 1 (2).pdf
vendored
Normal file
Binary file not shown.
12
Mastodon/Resources/Assets.xcassets/Settings/appearance.light.imageset/Contents.json
vendored
Normal file
12
Mastodon/Resources/Assets.xcassets/Settings/appearance.light.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "iPhone 11 Pro _ X - 1 (1).pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
Mastodon/Resources/Assets.xcassets/Settings/appearance.light.imageset/iPhone 11 Pro _ X - 1 (1).pdf
vendored
Normal file
BIN
Mastodon/Resources/Assets.xcassets/Settings/appearance.light.imageset/iPhone 11 Pro _ X - 1 (1).pdf
vendored
Normal file
Binary file not shown.
Binary file not shown.
|
@ -187,4 +187,25 @@ any server.";
|
|||
"Scene.ServerRules.TermsOfService" = "terms of service";
|
||||
"Scene.ServerRules.Title" = "Some ground rules.";
|
||||
"Scene.Welcome.Slogan" = "Social networking
|
||||
back in your hands.";
|
||||
back in your hands.";
|
||||
"Scene.Settings.Title" = "Settings";
|
||||
"Scene.Settings.Section.Appearance.Title" = "Appearance";
|
||||
"Scene.Settings.Section.Appearance.Automatic" = "Automatic";
|
||||
"Scene.Settings.Section.Appearance.Light" = "Always Light";
|
||||
"Scene.Settings.Section.Appearance.Dark" = "Always Dark";
|
||||
"Scene.Settings.Section.Notifications.Title" = "Notifications";
|
||||
"Scene.Settings.Section.Notifications.Favorites" = "Favorites my post";
|
||||
"Scene.Settings.Section.Notifications.Follows" = "Follows me";
|
||||
"Scene.Settings.Section.Notifications.Boosts" = "Boosts my post";
|
||||
"Scene.Settings.Section.Notifications.Mentions" = "Mentions me";
|
||||
"Scene.Settings.Section.Notifications.Trigger.Anyone" = "anyone";
|
||||
"Scene.Settings.Section.Notifications.Trigger.Follower" = "a follower";
|
||||
"Scene.Settings.Section.Notifications.Trigger.Follow" = "anyone I follow";
|
||||
"Scene.Settings.Section.Notifications.Trigger.NoOne" = "no one";
|
||||
"Scene.Settings.Section.Notifications.Trigger.Title" = "Notify me when";
|
||||
"Scene.Settings.Section.BoringZone.Title" = "The Boring zone";
|
||||
"Scene.Settings.Section.BoringZone.Terms" = "Terms of Service";
|
||||
"Scene.Settings.Section.BoringZone.Privacy" = "Privacy Policy";
|
||||
"Scene.Settings.Section.SpicyZone.Title" = "The spicy zone";
|
||||
"Scene.Settings.Section.SpicyZone.Clear" = "Clear Media Cache";
|
||||
"Scene.Settings.Section.SpicyZone.SignOut" = "Sign Out";
|
||||
|
|
|
@ -33,6 +33,9 @@ extension HomeTimelineViewController {
|
|||
guard let self = self else { return }
|
||||
self.showProfileAction(action)
|
||||
},
|
||||
UIAction(title: "Settings", image: UIImage(systemName: "escape"), attributes: []) { [weak self] action in
|
||||
self?.coordinator.present(scene: .settings, from: self, transition: .modal(animated: true, completion: nil))
|
||||
},
|
||||
UIAction(title: "Sign Out", image: UIImage(systemName: "escape"), attributes: .destructive) { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
self.signOutAction(action)
|
||||
|
|
|
@ -0,0 +1,430 @@
|
|||
//
|
||||
// SettingsViewController.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by ihugo on 2021/4/7.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import ActiveLabel
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
|
||||
class SettingsViewController: UIViewController, NeedsDependency {
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
var viewModel: SettingsViewModel! { willSet { precondition(!isViewLoaded) } }
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
var triggerMenu: UIMenu {
|
||||
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
|
||||
let menu = UIMenu(
|
||||
image: UIImage(systemName: "escape"),
|
||||
identifier: nil,
|
||||
options: .displayInline,
|
||||
children: [
|
||||
UIAction(title: anyone, image: UIImage(systemName: "person.3"), attributes: []) { [weak self] action in
|
||||
self?.updateTrigger(by: anyone)
|
||||
},
|
||||
UIAction(title: follower, image: UIImage(systemName: "person.crop.circle.badge.plus"), attributes: []) { [weak self] action in
|
||||
self?.updateTrigger(by: follower)
|
||||
},
|
||||
UIAction(title: follow, image: UIImage(systemName: "person.crop.circle.badge.checkmark"), attributes: []) { [weak self] action in
|
||||
self?.updateTrigger(by: follow)
|
||||
},
|
||||
UIAction(title: noOne, image: UIImage(systemName: "nosign"), attributes: []) { [weak self] action in
|
||||
self?.updateTrigger(by: noOne)
|
||||
},
|
||||
].reversed()
|
||||
)
|
||||
return menu
|
||||
}
|
||||
|
||||
lazy var notifySectionHeader: UIView = {
|
||||
let view = UIStackView()
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.isLayoutMarginsRelativeArrangement = true
|
||||
view.layoutMargins = UIEdgeInsets(top: 15, left: 4, bottom: 5, right: 4)
|
||||
view.axis = .horizontal
|
||||
view.alignment = .fill
|
||||
view.distribution = .equalSpacing
|
||||
view.spacing = 4
|
||||
|
||||
let notifyLabel = UILabel()
|
||||
notifyLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
notifyLabel.font = UIFontMetrics(forTextStyle: .title3).scaledFont(for: UIFont.systemFont(ofSize: 20, weight: .semibold))
|
||||
notifyLabel.textColor = Asset.Colors.Label.primary.color
|
||||
notifyLabel.text = L10n.Scene.Settings.Section.Notifications.Trigger.title
|
||||
view.addArrangedSubview(notifyLabel)
|
||||
view.addArrangedSubview(whoButton)
|
||||
return view
|
||||
}()
|
||||
|
||||
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
|
||||
whoButton.clipsToBounds = true
|
||||
return whoButton
|
||||
}()
|
||||
|
||||
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
|
||||
tableView.delegate = self
|
||||
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")
|
||||
return tableView
|
||||
}()
|
||||
|
||||
lazy var footerView: 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
|
||||
view.layoutMargins = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
|
||||
view.axis = .vertical
|
||||
view.alignment = .center
|
||||
|
||||
let label = ActiveLabel(style: .default)
|
||||
label.textAlignment = .center
|
||||
label.configure(content: "Mastodon is open source software. You can contribute or report issues on GitHub at <a href=\"https://github.com/tootsuite/mastodon\">tootsuite/mastodon</a> (v3.3.0).")
|
||||
label.delegate = self
|
||||
|
||||
view.addArrangedSubview(label)
|
||||
return view
|
||||
}()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
setupView()
|
||||
bindViewModel()
|
||||
|
||||
viewModel.viewDidLoad.send()
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
guard let footerView = self.tableView.tableFooterView else {
|
||||
return
|
||||
}
|
||||
|
||||
let width = self.tableView.bounds.size.width
|
||||
let size = footerView.systemLayoutSizeFitting(CGSize(width: width, height: UIView.layoutFittingCompressedSize.height))
|
||||
if footerView.frame.size.height != size.height {
|
||||
footerView.frame.size.height = size.height
|
||||
self.tableView.tableFooterView = footerView
|
||||
}
|
||||
}
|
||||
|
||||
// MAKR: - Private methods
|
||||
private func bindViewModel() {
|
||||
let input = SettingsViewModel.Input()
|
||||
_ = viewModel.transform(input: input)
|
||||
}
|
||||
|
||||
private func setupView() {
|
||||
view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||
setupNavigation()
|
||||
setupTableView()
|
||||
|
||||
view.addSubview(tableView)
|
||||
NSLayoutConstraint.activate([
|
||||
tableView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
private func setupNavigation() {
|
||||
navigationController?.navigationBar.prefersLargeTitles = true
|
||||
navigationItem.rightBarButtonItem
|
||||
= UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.done,
|
||||
target: self,
|
||||
action: #selector(doneButtonDidClick))
|
||||
navigationItem.title = L10n.Scene.Settings.title
|
||||
|
||||
let barAppearance = UINavigationBarAppearance()
|
||||
barAppearance.configureWithDefaultBackground()
|
||||
navigationItem.standardAppearance = barAppearance
|
||||
navigationItem.compactAppearance = barAppearance
|
||||
navigationItem.scrollEdgeAppearance = barAppearance
|
||||
}
|
||||
|
||||
private func setupTableView() {
|
||||
viewModel.dataSource = UITableViewDiffableDataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
|
||||
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
|
||||
}
|
||||
|
||||
func signout() {
|
||||
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
return
|
||||
}
|
||||
|
||||
context.authenticationService.signOutMastodonUser(
|
||||
domain: activeMastodonAuthenticationBox.domain,
|
||||
userID: activeMastodonAuthenticationBox.userID
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
switch result {
|
||||
case .failure(let error):
|
||||
assertionFailure(error.localizedDescription)
|
||||
case .success(let isSignOut):
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: sign out %s", ((#file as NSString).lastPathComponent), #line, #function, isSignOut ? "success" : "fail")
|
||||
guard isSignOut else { return }
|
||||
self.coordinator.setup()
|
||||
self.coordinator.setupOnboardingIfNeeds(animated: true)
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
// Mark: - Actions
|
||||
@objc func doneButtonDidClick() {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
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]
|
||||
|
||||
if section == 1 {
|
||||
let 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)
|
||||
}
|
||||
return header
|
||||
} else {
|
||||
let header = SettingsSectionHeader(frame: CGRect(x: 0, y: 0, width: 375, height: 66))
|
||||
header.update(title: sectionData.title)
|
||||
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
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
guard indexPath.section == 2 || indexPath.section == 3 else { return }
|
||||
|
||||
if indexPath.section == 2 {
|
||||
coordinator.present(
|
||||
scene: .webview(url: URL(string: "https://mastodon.online/terms")!),
|
||||
from: self,
|
||||
transition: .modal(animated: true, completion: nil))
|
||||
}
|
||||
|
||||
// iTODO: clear media cache
|
||||
|
||||
|
||||
// logout
|
||||
if indexPath.section == 3, indexPath.row == 2 {
|
||||
signout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update setting into core data
|
||||
extension SettingsViewController {
|
||||
func updateTrigger(by who: String) {
|
||||
guard let setting = self.viewModel.setting.value else { return }
|
||||
|
||||
context.managedObjectContext.performChanges {
|
||||
setting.update(triggerBy: who)
|
||||
}
|
||||
.sink { (_) in
|
||||
}.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
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 }
|
||||
|
||||
var values: [Bool?]?
|
||||
if let alerts = settings.subscription?.first(where: { (s) -> Bool in
|
||||
return s.type == settings.triggerBy
|
||||
})?.alert {
|
||||
var items = [Bool?]()
|
||||
items.append(alerts.favourite)
|
||||
items.append(alerts.follow)
|
||||
items.append(alerts.reblog)
|
||||
items.append(alerts.mention)
|
||||
values = items
|
||||
}
|
||||
guard var alertValues = values else { return }
|
||||
guard alertValues.count >= 4 else { return }
|
||||
|
||||
switch title {
|
||||
case L10n.Scene.Settings.Section.Notifications.favorites:
|
||||
alertValues[0] = isOn
|
||||
case L10n.Scene.Settings.Section.Notifications.follows:
|
||||
alertValues[1] = isOn
|
||||
case L10n.Scene.Settings.Section.Notifications.boosts:
|
||||
alertValues[2] = isOn
|
||||
case L10n.Scene.Settings.Section.Notifications.mentions:
|
||||
alertValues[3] = isOn
|
||||
default: break
|
||||
}
|
||||
self.viewModel.alertUpdate.send((triggerBy: triggerBy, values: alertValues))
|
||||
}
|
||||
}
|
||||
|
||||
extension SettingsViewController: SettingsAppearanceTableViewCellDelegate {
|
||||
func settingsAppearanceCell(_ view: SettingsAppearanceTableViewCell, didSelect: SettingsItem.AppearanceMode) {
|
||||
print("[SettingsViewController]: didSelect \(didSelect)")
|
||||
guard let setting = self.viewModel.setting.value else { return }
|
||||
|
||||
context.managedObjectContext.performChanges {
|
||||
setting.update(appearance: didSelect.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
|
||||
}.store(in: &disposeBag)
|
||||
}
|
||||
}
|
||||
|
||||
extension SettingsViewController: SettingsToggleCellDelegate {
|
||||
func settingsToggleCell(_ cell: SettingsToggleTableViewCell, didChangeStatus: Bool) {
|
||||
updateAlert(title: cell.data?.title, isOn: didChangeStatus)
|
||||
}
|
||||
}
|
||||
|
||||
extension SettingsViewController: ActiveLabelDelegate {
|
||||
func activeLabel(_ activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
|
||||
coordinator.present(
|
||||
scene: .webview(url: URL(string: "https://github.com/tootsuite/mastodon")!),
|
||||
from: self,
|
||||
transition: .modal(animated: true, completion: nil))
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
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
|
||||
|
||||
struct SettingsViewController_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
Group {
|
||||
UIViewControllerPreview { () -> UIViewController in
|
||||
return SettingsViewController()
|
||||
}
|
||||
.previewLayout(.fixed(width: 390, height: 844))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,295 @@
|
|||
//
|
||||
// SettingsViewModel.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by ihugo on 2021/4/7.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import Foundation
|
||||
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) } }
|
||||
|
||||
var dataSource: UITableViewDiffableDataSource<SettingsSection, SettingsItem>!
|
||||
var disposeBag = 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)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
/// trigger when
|
||||
/// - init alerts
|
||||
/// - change subscription status everytime
|
||||
let alertUpdate = 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]
|
||||
}()
|
||||
|
||||
struct Input {
|
||||
}
|
||||
|
||||
struct Output {
|
||||
}
|
||||
|
||||
init(context: AppContext, coordinator: SceneCoordinator) {
|
||||
self.context = context
|
||||
self.coordinator = coordinator
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
func transform(input: Input?) -> Output? {
|
||||
//guard let input = input else { return nil }
|
||||
|
||||
// build data for table view
|
||||
buildDataSource()
|
||||
|
||||
// request subsription data for updating or initialization
|
||||
requestSubscription()
|
||||
|
||||
typealias SubscriptionResponse = Mastodon.Response.Content<Mastodon.Entity.Subscription>
|
||||
alertUpdate
|
||||
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
|
||||
.flatMap { [weak self] (arg) -> AnyPublisher<SubscriptionResponse, Error> in
|
||||
let (triggerBy, values) = arg
|
||||
guard let self = self else {
|
||||
return Empty<SubscriptionResponse, Error>().eraseToAnyPublisher()
|
||||
}
|
||||
guard let activeMastodonAuthenticationBox =
|
||||
self.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
return Empty<SubscriptionResponse, Error>().eraseToAnyPublisher()
|
||||
}
|
||||
guard values.count >= 4 else {
|
||||
return Empty<SubscriptionResponse, Error>().eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
typealias Query = Mastodon.API.Notification.CreateSubscriptionQuery
|
||||
let domain = activeMastodonAuthenticationBox.domain
|
||||
return self.context.apiService.changeSubscription(
|
||||
domain: domain,
|
||||
mastodonAuthenticationBox: activeMastodonAuthenticationBox,
|
||||
query: Query(favourite: values[0], follow: values[1], reblog: values[2], mention: values[3], poll: nil),
|
||||
triggerBy: triggerBy)
|
||||
}
|
||||
.sink { _ in
|
||||
} receiveValue: { (subscription) in
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
|
||||
do {
|
||||
try fetchResultsController.performFetch()
|
||||
setting.value = fetchResultsController.fetchedObjects?.first
|
||||
} catch {
|
||||
assertionFailure(error.localizedDescription)
|
||||
}
|
||||
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)
|
||||
items.append(alerts.follow)
|
||||
items.append(alerts.reblog)
|
||||
items.append(alerts.mention)
|
||||
switches = items
|
||||
} else if let triggerBy = settings?.triggerBy,
|
||||
let values = self.notificationDefaultValue[triggerBy] {
|
||||
switches = values
|
||||
self.alertUpdate.send((triggerBy: triggerBy, values: values))
|
||||
} else {
|
||||
// fallback a default value
|
||||
let anyone = L10n.Scene.Settings.Section.Notifications.Trigger.anyone
|
||||
switches = self.notificationDefaultValue[anyone]
|
||||
}
|
||||
let notifications = [L10n.Scene.Settings.Section.Notifications.favorites,
|
||||
L10n.Scene.Settings.Section.Notifications.follows,
|
||||
L10n.Scene.Settings.Section.Notifications.boosts,
|
||||
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 {
|
||||
// FIXME: update color in both light and dark mode
|
||||
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 {
|
||||
// FIXME: update color in both light and dark mode
|
||||
let item = SettingsItem.boringZone(item: SettingsItem.Link(title: l, color: .systemRed))
|
||||
spicyLinkItems.append(item)
|
||||
}
|
||||
let spicySection = SettingsSection.boringZone(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.filter({ $0 != nil }).sink { [weak self] (settings) in
|
||||
guard let self = self else { return }
|
||||
self.processDataSource(settings)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// init with no subscription for notification
|
||||
let settings: Setting? = nil
|
||||
self.processDataSource(settings)
|
||||
}
|
||||
|
||||
private func requestSubscription() {
|
||||
// request subscription of notifications
|
||||
typealias SubscriptionResponse = Mastodon.Response.Content<Mastodon.Entity.Subscription>
|
||||
viewDidLoad.flatMap { [weak self] (_) -> AnyPublisher<SubscriptionResponse, Error> in
|
||||
guard let self = self,
|
||||
let activeMastodonAuthenticationBox =
|
||||
self.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
return Empty<SubscriptionResponse, Error>().eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
let domain = activeMastodonAuthenticationBox.domain
|
||||
return self.context.apiService.subscription(
|
||||
domain: domain,
|
||||
mastodonAuthenticationBox: activeMastodonAuthenticationBox)
|
||||
}
|
||||
.sink { _ in
|
||||
} receiveValue: { (subscription) in
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - NSFetchedResultsControllerDelegate
|
||||
extension SettingsViewModel: NSFetchedResultsControllerDelegate {
|
||||
|
||||
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
|
||||
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
|
||||
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
|
||||
guard controller === fetchResultsController else {
|
||||
return
|
||||
}
|
||||
|
||||
setting.value = fetchResultsController.fetchedObjects?.first
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
enum SettingsSection: Hashable {
|
||||
case apperance(title: String, selectedMode: SettingsItem)
|
||||
case notifications(title: String, items: [SettingsItem])
|
||||
case boringZone(title: String, items: [SettingsItem])
|
||||
case spicyZone(tilte: String, items: [SettingsItem])
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .apperance(let title, _),
|
||||
.notifications(let title, _),
|
||||
.boringZone(let title, _),
|
||||
.spicyZone(let title, _):
|
||||
return title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
//
|
||||
// SettingsAppearanceTableViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by ihugo on 2021/4/8.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
protocol SettingsAppearanceTableViewCellDelegate: class {
|
||||
func settingsAppearanceCell(_ view: SettingsAppearanceTableViewCell, didSelect: SettingsItem.AppearanceMode)
|
||||
}
|
||||
|
||||
class AppearanceView: UIView {
|
||||
lazy var imageView: UIImageView = {
|
||||
let view = UIImageView()
|
||||
return view
|
||||
}()
|
||||
lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 12, weight: .regular)
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.textAlignment = .center
|
||||
return label
|
||||
}()
|
||||
lazy var checkBox: UIButton = {
|
||||
let button = UIButton()
|
||||
button.isUserInteractionEnabled = false
|
||||
button.setImage(UIImage(systemName: "circle"), for: .normal)
|
||||
button.setImage(UIImage(systemName: "checkmark.circle.fill"), for: .selected)
|
||||
button.imageView?.preferredSymbolConfiguration = UIImage.SymbolConfiguration(textStyle: .body)
|
||||
button.imageView?.tintColor = Asset.Colors.lightSecondaryText.color
|
||||
button.imageView?.contentMode = .scaleAspectFill
|
||||
return button
|
||||
}()
|
||||
lazy var stackView: UIStackView = {
|
||||
let view = UIStackView()
|
||||
view.axis = .vertical
|
||||
view.spacing = 10
|
||||
view.distribution = .equalSpacing
|
||||
return view
|
||||
}()
|
||||
|
||||
var selected: Bool = false {
|
||||
didSet {
|
||||
checkBox.isSelected = selected
|
||||
if selected {
|
||||
checkBox.imageView?.tintColor = Asset.Colors.lightBrandBlue.color
|
||||
} else {
|
||||
checkBox.imageView?.tintColor = Asset.Colors.lightSecondaryText.color
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Methods
|
||||
init(image: UIImage?, title: String) {
|
||||
super.init(frame: .zero)
|
||||
setupUI()
|
||||
|
||||
imageView.image = image
|
||||
titleLabel.text = title
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: - Private methods
|
||||
private func setupUI() {
|
||||
stackView.addArrangedSubview(imageView)
|
||||
stackView.addArrangedSubview(titleLabel)
|
||||
stackView.addArrangedSubview(checkBox)
|
||||
|
||||
addSubview(stackView)
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
stackView.topAnchor.constraint(equalTo: self.topAnchor),
|
||||
stackView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
|
||||
stackView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
|
||||
stackView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
|
||||
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 218.0 / 100.0),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsAppearanceTableViewCell: UITableViewCell {
|
||||
weak var delegate: SettingsAppearanceTableViewCellDelegate?
|
||||
var appearance: SettingsItem.AppearanceMode = .automatic {
|
||||
didSet {
|
||||
guard let delegate = self.delegate else { return }
|
||||
delegate.settingsAppearanceCell(self, didSelect: appearance)
|
||||
}
|
||||
}
|
||||
|
||||
lazy var stackView: UIStackView = {
|
||||
let view = UIStackView()
|
||||
view.isLayoutMarginsRelativeArrangement = true
|
||||
view.layoutMargins = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20)
|
||||
view.axis = .horizontal
|
||||
view.distribution = .fillEqually
|
||||
view.spacing = 18
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
return view
|
||||
}()
|
||||
|
||||
let automatic = AppearanceView(image: Asset.Settings.appearanceAutomatic.image,
|
||||
title: L10n.Scene.Settings.Section.Appearance.automatic)
|
||||
let light = AppearanceView(image: Asset.Settings.appearanceLight.image,
|
||||
title: L10n.Scene.Settings.Section.Appearance.light)
|
||||
let dark = AppearanceView(image: Asset.Settings.appearanceDark.image,
|
||||
title: L10n.Scene.Settings.Section.Appearance.dark)
|
||||
|
||||
lazy var automaticTap: UITapGestureRecognizer = {
|
||||
let tapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
|
||||
tapGestureRecognizer.addTarget(self, action: #selector(appearanceDidTap(sender:)))
|
||||
return tapGestureRecognizer
|
||||
}()
|
||||
|
||||
lazy var lightTap: UITapGestureRecognizer = {
|
||||
let tapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
|
||||
tapGestureRecognizer.addTarget(self, action: #selector(appearanceDidTap(sender:)))
|
||||
return tapGestureRecognizer
|
||||
}()
|
||||
|
||||
lazy var darkTap: UITapGestureRecognizer = {
|
||||
let tapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
|
||||
tapGestureRecognizer.addTarget(self, action: #selector(appearanceDidTap(sender:)))
|
||||
return tapGestureRecognizer
|
||||
}()
|
||||
|
||||
// MARK: - Methods
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
// remove seperator line in section of group tableview
|
||||
for subview in self.subviews {
|
||||
if subview != self.contentView && subview.frame.width == self.frame.width {
|
||||
subview.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(with data: SettingsItem.AppearanceMode, delegate: SettingsAppearanceTableViewCellDelegate?) {
|
||||
appearance = data
|
||||
self.delegate = delegate
|
||||
|
||||
automatic.selected = false
|
||||
light.selected = false
|
||||
dark.selected = false
|
||||
|
||||
switch data {
|
||||
case .automatic:
|
||||
automatic.selected = true
|
||||
case .light:
|
||||
light.selected = true
|
||||
case .dark:
|
||||
dark.selected = true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Private methods
|
||||
private func setupUI() {
|
||||
backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||
selectionStyle = .none
|
||||
contentView.addSubview(stackView)
|
||||
|
||||
stackView.addArrangedSubview(automatic)
|
||||
stackView.addArrangedSubview(light)
|
||||
stackView.addArrangedSubview(dark)
|
||||
|
||||
automatic.addGestureRecognizer(automaticTap)
|
||||
light.addGestureRecognizer(lightTap)
|
||||
dark.addGestureRecognizer(darkTap)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
stackView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||
stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
@objc func appearanceDidTap(sender: UIGestureRecognizer) {
|
||||
if sender == automaticTap {
|
||||
appearance = .automatic
|
||||
}
|
||||
|
||||
if sender == lightTap {
|
||||
appearance = .light
|
||||
}
|
||||
|
||||
if sender == darkTap {
|
||||
appearance = .dark
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// SettingsLinkTableViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by ihugo on 2021/4/8.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class SettingsLinkTableViewCell: UITableViewCell {
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
selectionStyle = .none
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func setHighlighted(_ highlighted: Bool, animated: Bool) {
|
||||
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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
//
|
||||
// SettingsToggleTableViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by ihugo on 2021/4/8.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
protocol SettingsToggleCellDelegate: class {
|
||||
func settingsToggleCell(_ cell: SettingsToggleTableViewCell, didChangeStatus: Bool)
|
||||
}
|
||||
|
||||
class SettingsToggleTableViewCell: UITableViewCell {
|
||||
lazy var switchButton: UISwitch = {
|
||||
let view = UISwitch(frame:.zero)
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
return view
|
||||
}()
|
||||
|
||||
var data: SettingsItem.NotificationSwitch?
|
||||
weak var delegate: SettingsToggleCellDelegate?
|
||||
|
||||
// MARK: - Methods
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: .default, reuseIdentifier: reuseIdentifier)
|
||||
setupUI()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// MARK: Private methods
|
||||
private func setupUI() {
|
||||
selectionStyle = .none
|
||||
textLabel?.font = .systemFont(ofSize: 17, weight: .regular)
|
||||
contentView.addSubview(switchButton)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
switchButton.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
|
||||
switchButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
])
|
||||
|
||||
switchButton.addTarget(self, action: #selector(valueDidChange(sender:)), for: .valueChanged)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
//
|
||||
// SettingsSectionHeader.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by ihugo on 2021/4/8.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// section header which supports add a custom view blelow the title
|
||||
class SettingsSectionHeader: UIView {
|
||||
lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
label.font = .systemFont(ofSize: 13, weight: .regular)
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var stackView: UIStackView = {
|
||||
let view = UIStackView()
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.isLayoutMarginsRelativeArrangement = true
|
||||
view.layoutMargins = UIEdgeInsets(top: 40, left: 12, bottom: 10, right: 12)
|
||||
view.axis = .vertical
|
||||
return view
|
||||
}()
|
||||
|
||||
init(frame: CGRect, customView: UIView? = nil) {
|
||||
super.init(frame: frame)
|
||||
|
||||
backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||
stackView.addArrangedSubview(titleLabel)
|
||||
if let view = customView {
|
||||
stackView.addArrangedSubview(view)
|
||||
}
|
||||
|
||||
addSubview(stackView)
|
||||
NSLayoutConstraint.activate([
|
||||
stackView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
|
||||
stackView.trailingAnchor.constraint(lessThanOrEqualTo: self.trailingAnchor),
|
||||
stackView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
|
||||
stackView.topAnchor.constraint(equalTo: self.topAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(title: String?) {
|
||||
titleLabel.text = title?.uppercased()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
//
|
||||
// APIService+Settings.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by ihugo on 2021/4/9.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
import Combine
|
||||
|
||||
extension APIService {
|
||||
|
||||
func subscription(
|
||||
domain: String,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Subscription>, Error> {
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
|
||||
|
||||
return Mastodon.API.Notification.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)
|
||||
}
|
||||
.setFailureType(to: Error.self)
|
||||
.map { _ in return response }
|
||||
.eraseToAnyPublisher()
|
||||
}.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func changeSubscription(
|
||||
domain: String,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox,
|
||||
query: Mastodon.API.Notification.CreateSubscriptionQuery,
|
||||
triggerBy: String
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Subscription>, Error> {
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
|
||||
return Mastodon.API.Notification.createSubscription(
|
||||
session: session,
|
||||
domain: domain,
|
||||
authorization: authorization,
|
||||
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)
|
||||
}
|
||||
.setFailureType(to: Error.self)
|
||||
.map { _ in return response }
|
||||
.eraseToAnyPublisher()
|
||||
}.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
//
|
||||
// APIService+CoreData+Notification.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by ihugo on 2021/4/11.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import Foundation
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
extension APIService.CoreData {
|
||||
|
||||
static func createOrMergeSetting(
|
||||
into managedObjectContext: NSManagedObjectContext,
|
||||
domain: String,
|
||||
property: Setting.Property
|
||||
) -> (Subscription: Setting, isCreated: Bool) {
|
||||
let oldSetting: Setting? = {
|
||||
let request = Setting.sortedFetchRequest
|
||||
request.predicate = Setting.predicate(domain: property.domain)
|
||||
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? = nil
|
||||
) -> (Subscription: Subscription, isCreated: Bool) {
|
||||
// create setting entity if possible
|
||||
let oldSetting: Setting? = {
|
||||
let request = Setting.sortedFetchRequest
|
||||
request.predicate = Setting.predicate(domain: domain)
|
||||
request.fetchLimit = 1
|
||||
request.returnsObjectsAsFaults = false
|
||||
do {
|
||||
return try managedObjectContext.fetch(request).first
|
||||
} catch {
|
||||
assertionFailure(error.localizedDescription)
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
var setting: Setting!
|
||||
if let oldSetting = oldSetting {
|
||||
setting = oldSetting
|
||||
} else {
|
||||
let property = Setting.Property(
|
||||
appearance: "automatic",
|
||||
triggerBy: "anyone",
|
||||
domain: domain)
|
||||
(setting, _) = createOrMergeSetting(
|
||||
into: managedObjectContext,
|
||||
domain: domain,
|
||||
property: property)
|
||||
}
|
||||
|
||||
let oldSubscription: Subscription? = {
|
||||
let request = Subscription.sortedFetchRequest
|
||||
request.predicate = Subscription.predicate(id: entity.id)
|
||||
request.fetchLimit = 1
|
||||
request.returnsObjectsAsFaults = false
|
||||
do {
|
||||
return try managedObjectContext.fetch(request).first
|
||||
} catch {
|
||||
assertionFailure(error.localizedDescription)
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
|
||||
let property = Subscription.Property(
|
||||
endpoint: entity.endpoint,
|
||||
id: entity.id,
|
||||
serverKey: entity.serverKey,
|
||||
type: triggerBy ?? setting.triggerBy ?? "")
|
||||
let alertEntity = entity.alerts
|
||||
let alert = SubscriptionAlerts.Property(
|
||||
favourite: alertEntity.favourite,
|
||||
follow: alertEntity.follow,
|
||||
mention: alertEntity.mention,
|
||||
poll: alertEntity.poll,
|
||||
reblog: alertEntity.reblog)
|
||||
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 {
|
||||
// don't expand subscription if add existed subscription
|
||||
setting.mutableSetValue(forKey: #keyPath(Setting.subscription)).add(oldSubscription)
|
||||
}
|
||||
return (oldSubscription, false)
|
||||
} else {
|
||||
let subscription = Subscription.insert(
|
||||
into: managedObjectContext,
|
||||
property: property
|
||||
)
|
||||
subscription.alert = SubscriptionAlerts.insert(
|
||||
into: managedObjectContext,
|
||||
property: alert)
|
||||
setting.mutableSetValue(forKey: #keyPath(Setting.subscription)).add(subscription)
|
||||
return (subscription, true)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -27,6 +27,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
sceneCoordinator.setup()
|
||||
sceneCoordinator.setupOnboardingIfNeeds(animated: false)
|
||||
window.makeKeyAndVisible()
|
||||
|
||||
// update `overrideUserInterfaceStyle` with current setting
|
||||
SettingsViewController.updateOverrideUserInterfaceStyle(window: window)
|
||||
}
|
||||
|
||||
func sceneDidDisconnect(_ scene: UIScene) {
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by ihugo on 2021/4/9.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
extension Mastodon.API.Notification {
|
||||
|
||||
static func pushEndpointURL(domain: String) -> URL {
|
||||
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("push/subscription")
|
||||
}
|
||||
|
||||
/// Get current subscription
|
||||
///
|
||||
/// Using this endpoint to get current subscription
|
||||
///
|
||||
/// - Since: 2.4.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/4/9
|
||||
/// # 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 `Poll` nested in the response
|
||||
public static func subscription(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
authorization: Mastodon.API.OAuth.Authorization?
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Subscription>, Error> {
|
||||
let request = Mastodon.API.get(
|
||||
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.Subscription.self, from: data, response: response)
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
/// Change types of notifications
|
||||
///
|
||||
/// Using this endpoint to change types of notifications
|
||||
///
|
||||
/// - Since: 2.4.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/4/9
|
||||
/// # 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 `Poll` nested in the response
|
||||
public static func createSubscription(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
authorization: Mastodon.API.OAuth.Authorization?,
|
||||
query: CreateSubscriptionQuery
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Subscription>, Error> {
|
||||
let request = Mastodon.API.post(
|
||||
url: pushEndpointURL(domain: domain),
|
||||
query: query,
|
||||
authorization: authorization
|
||||
)
|
||||
return session.dataTaskPublisher(for: request)
|
||||
.tryMap { data, response in
|
||||
let value = try Mastodon.API.decode(type: Mastodon.Entity.Subscription.self, from: data, response: response)
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
extension Mastodon.API.Notification {
|
||||
public struct CreateSubscriptionQuery: PostQuery {
|
||||
var queryItems: [URLQueryItem]?
|
||||
var contentType: String?
|
||||
var body: Data?
|
||||
|
||||
let follow: Bool?
|
||||
let favourite: Bool?
|
||||
let reblog: Bool?
|
||||
let mention: Bool?
|
||||
let poll: Bool?
|
||||
|
||||
// iTODO: missing parameters
|
||||
// subscription[endpoint]
|
||||
// subscription[keys][p256dh]
|
||||
// subscription[keys][auth]
|
||||
public init(favourite: Bool?,
|
||||
follow: Bool?,
|
||||
reblog: Bool?,
|
||||
mention: Bool?,
|
||||
poll: Bool?) {
|
||||
self.follow = follow
|
||||
self.favourite = favourite
|
||||
self.reblog = reblog
|
||||
self.mention = mention
|
||||
self.poll = poll
|
||||
|
||||
queryItems = [URLQueryItem]()
|
||||
|
||||
if let followValue = follow?.queryItemValue {
|
||||
let followItem = URLQueryItem(name: "data[alerts][follow]", value: followValue)
|
||||
queryItems?.append(followItem)
|
||||
}
|
||||
|
||||
if let favouriteValue = favourite?.queryItemValue {
|
||||
let favouriteItem = URLQueryItem(name: "data[alerts][favourite]", value: favouriteValue)
|
||||
queryItems?.append(favouriteItem)
|
||||
}
|
||||
|
||||
if let reblogValue = reblog?.queryItemValue {
|
||||
let reblogItem = URLQueryItem(name: "data[alerts][reblog]", value: reblogValue)
|
||||
queryItems?.append(reblogItem)
|
||||
}
|
||||
|
||||
if let mentionValue = mention?.queryItemValue {
|
||||
let mentionItem = URLQueryItem(name: "data[alerts][mention]", value: mentionValue)
|
||||
queryItems?.append(mentionItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -115,6 +115,7 @@ extension Mastodon.API {
|
|||
public enum Trends { }
|
||||
public enum Suggestions { }
|
||||
public enum Notifications { }
|
||||
public enum Notification { }
|
||||
}
|
||||
|
||||
extension Mastodon.API {
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by ihugo on 2021/4/9.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
extension Mastodon.Entity {
|
||||
/// Subscription
|
||||
///
|
||||
/// - Since: 2.4.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/4/9
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/pushsubscription/)
|
||||
public struct Subscription: Codable {
|
||||
// Base
|
||||
public let id: String
|
||||
public let endpoint: String
|
||||
public let alerts: Alerts
|
||||
public let serverKey: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case endpoint
|
||||
case serverKey = "server_key"
|
||||
case alerts
|
||||
}
|
||||
|
||||
public struct Alerts: Codable {
|
||||
public let follow: Bool
|
||||
public let favourite: Bool
|
||||
public let reblog: Bool
|
||||
public let mention: Bool
|
||||
public let poll: Bool
|
||||
}
|
||||
}
|
||||
}
|
|
@ -25,4 +25,4 @@ SPEC CHECKSUMS:
|
|||
|
||||
PODFILE CHECKSUM: 30e8e3a555251a512e7b5e91183747152f126e7a
|
||||
|
||||
COCOAPODS: 1.10.1
|
||||
COCOAPODS: 1.10.0
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
//
|
||||
// PushSubscriptionAlerts+CoreDataClass.swift
|
||||
// CoreDataStack
|
||||
//
|
||||
// Created by ihugo on 2021/4/9.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
@objc(SubscriptionAlerts)
|
||||
public final class SubscriptionAlerts: NSManagedObject {
|
||||
@NSManaged public var follow: Bool
|
||||
@NSManaged public var favourite: Bool
|
||||
@NSManaged public var reblog: Bool
|
||||
@NSManaged public var mention: Bool
|
||||
@NSManaged public var poll: Bool
|
||||
|
||||
@NSManaged public private(set) var createdAt: Date
|
||||
@NSManaged public private(set) var updatedAt: Date
|
||||
|
||||
// MARK: - relationships
|
||||
@NSManaged public var pushSubscription: Subscription?
|
||||
}
|
||||
|
||||
public extension SubscriptionAlerts {
|
||||
override func awakeFromInsert() {
|
||||
super.awakeFromInsert()
|
||||
setPrimitiveValue(Date(), forKey: #keyPath(SubscriptionAlerts.createdAt))
|
||||
}
|
||||
|
||||
func didUpdate(at networkDate: Date) {
|
||||
self.updatedAt = networkDate
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
static func insert(
|
||||
into context: NSManagedObjectContext,
|
||||
property: Property
|
||||
) -> 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
|
||||
return alerts
|
||||
}
|
||||
|
||||
func update(favourite: Bool) {
|
||||
guard self.favourite != favourite else { return }
|
||||
self.favourite = favourite
|
||||
|
||||
didUpdate(at: Date())
|
||||
}
|
||||
|
||||
func update(follow: Bool) {
|
||||
guard self.follow != follow else { return }
|
||||
self.follow = follow
|
||||
|
||||
didUpdate(at: Date())
|
||||
}
|
||||
|
||||
func update(mention: Bool) {
|
||||
guard self.mention != mention else { return }
|
||||
self.mention = mention
|
||||
|
||||
didUpdate(at: Date())
|
||||
}
|
||||
|
||||
func update(poll: Bool) {
|
||||
guard self.poll != poll else { return }
|
||||
self.poll = poll
|
||||
|
||||
didUpdate(at: Date())
|
||||
}
|
||||
|
||||
func update(reblog: Bool) {
|
||||
guard self.reblog != reblog else { return }
|
||||
self.reblog = reblog
|
||||
|
||||
didUpdate(at: Date())
|
||||
}
|
||||
}
|
||||
|
||||
public extension SubscriptionAlerts {
|
||||
struct Property {
|
||||
public let favourite: Bool
|
||||
public let follow: Bool
|
||||
public let mention: Bool
|
||||
public let poll: Bool
|
||||
public let reblog: Bool
|
||||
|
||||
public init(favourite: Bool?, follow: Bool?, mention: Bool?, poll: Bool?, reblog: Bool?) {
|
||||
self.favourite = favourite ?? true
|
||||
self.follow = follow ?? true
|
||||
self.mention = mention ?? true
|
||||
self.poll = poll ?? true
|
||||
self.reblog = reblog ?? true
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
||||
return [NSSortDescriptor(keyPath: \SubscriptionAlerts.createdAt, ascending: false)]
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue