forked from zelo72/mastodon-ios
Merge branch 'develop' into feature/content-warning
# Conflicts: # CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents # Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved
This commit is contained in:
commit
f8127428dd
|
@ -120,4 +120,6 @@ xcuserdata
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/swift,swiftpm,xcode,cocoapods
|
# End of https://www.toptal.com/developers/gitignore/api/swift,swiftpm,xcode,cocoapods
|
||||||
|
|
||||||
Localization/StringsConvertor/input
|
Localization/StringsConvertor/input
|
||||||
Localization/StringsConvertor/output
|
Localization/StringsConvertor/output
|
||||||
|
.DS_Store
|
||||||
|
/Mastodon.xcworkspace/xcshareddata/swiftpm
|
||||||
|
|
|
@ -65,6 +65,22 @@
|
||||||
<attribute name="username" attributeType="String"/>
|
<attribute name="username" attributeType="String"/>
|
||||||
<relationship name="user" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="mastodonAuthentication" inverseEntity="MastodonUser"/>
|
<relationship name="user" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="mastodonAuthentication" inverseEntity="MastodonUser"/>
|
||||||
</entity>
|
</entity>
|
||||||
|
<entity name="MastodonNotification" representedClassName=".MastodonNotification" syncable="YES">
|
||||||
|
<attribute name="createAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="domain" attributeType="String"/>
|
||||||
|
<attribute name="id" attributeType="String"/>
|
||||||
|
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="typeRaw" attributeType="String"/>
|
||||||
|
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="userID" attributeType="String"/>
|
||||||
|
<relationship name="account" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser"/>
|
||||||
|
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="id"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
<entity name="MastodonUser" representedClassName=".MastodonUser" syncable="YES">
|
<entity name="MastodonUser" representedClassName=".MastodonUser" syncable="YES">
|
||||||
<attribute name="acct" attributeType="String"/>
|
<attribute name="acct" attributeType="String"/>
|
||||||
<attribute name="avatar" attributeType="String"/>
|
<attribute name="avatar" attributeType="String"/>
|
||||||
|
@ -155,6 +171,16 @@
|
||||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser"/>
|
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser"/>
|
||||||
<relationship name="hashtag" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Tag"/>
|
<relationship name="hashtag" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Tag"/>
|
||||||
</entity>
|
</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"/>
|
||||||
|
</entity>
|
||||||
<entity name="Status" representedClassName=".Status" syncable="YES">
|
<entity name="Status" representedClassName=".Status" syncable="YES">
|
||||||
<attribute name="content" attributeType="String"/>
|
<attribute name="content" attributeType="String"/>
|
||||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
@ -194,6 +220,26 @@
|
||||||
<relationship name="replyTo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="replyFrom" inverseEntity="Status"/>
|
<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"/>
|
<relationship name="tags" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Tag" inverseName="statuses" inverseEntity="Tag"/>
|
||||||
</entity>
|
</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">
|
<entity name="Tag" representedClassName=".Tag" syncable="YES">
|
||||||
<attribute name="createAt" attributeType="Date" defaultDateTimeInterval="631123200" usesScalarValueType="NO"/>
|
<attribute name="createAt" attributeType="Date" defaultDateTimeInterval="631123200" usesScalarValueType="NO"/>
|
||||||
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
|
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
|
||||||
|
@ -210,13 +256,17 @@
|
||||||
<element name="History" positionX="0" positionY="0" width="128" height="119"/>
|
<element name="History" positionX="0" positionY="0" width="128" height="119"/>
|
||||||
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="134"/>
|
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="134"/>
|
||||||
<element name="MastodonAuthentication" positionX="0" positionY="0" width="128" height="209"/>
|
<element name="MastodonAuthentication" positionX="0" positionY="0" width="128" height="209"/>
|
||||||
|
<element name="MastodonNotification" positionX="9" positionY="162" width="128" height="164"/>
|
||||||
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="659"/>
|
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="659"/>
|
||||||
<element name="Mention" positionX="0" positionY="0" width="128" height="149"/>
|
<element name="Mention" positionX="0" positionY="0" width="128" height="149"/>
|
||||||
<element name="Poll" positionX="0" positionY="0" width="128" height="194"/>
|
<element name="Poll" positionX="0" positionY="0" width="128" height="194"/>
|
||||||
<element name="PollOption" positionX="0" positionY="0" width="128" height="134"/>
|
<element name="PollOption" positionX="0" positionY="0" width="128" height="134"/>
|
||||||
<element name="PrivateNote" positionX="0" positionY="0" width="128" height="89"/>
|
<element name="PrivateNote" positionX="0" positionY="0" width="128" height="89"/>
|
||||||
<element name="SearchHistory" positionX="0" positionY="0" width="128" height="104"/>
|
<element name="SearchHistory" positionX="0" positionY="0" width="128" height="104"/>
|
||||||
|
<element name="Setting" positionX="72" positionY="162" width="128" height="149"/>
|
||||||
<element name="Status" positionX="0" positionY="0" width="128" height="584"/>
|
<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="Tag" positionX="0" positionY="0" width="128" height="134"/>
|
<element name="Tag" positionX="0" positionY="0" width="128" height="134"/>
|
||||||
</elements>
|
</elements>
|
||||||
</model>
|
</model>
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
//
|
||||||
|
// MastodonNotification.swift
|
||||||
|
// CoreDataStack
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/4/13.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
public final class MastodonNotification: NSManagedObject {
|
||||||
|
public typealias ID = UUID
|
||||||
|
@NSManaged public private(set) var identifier: ID
|
||||||
|
@NSManaged public private(set) var id: String
|
||||||
|
@NSManaged public private(set) var createAt: Date
|
||||||
|
@NSManaged public private(set) var updatedAt: Date
|
||||||
|
@NSManaged public private(set) var typeRaw: String
|
||||||
|
@NSManaged public private(set) var account: MastodonUser
|
||||||
|
@NSManaged public private(set) var status: Status?
|
||||||
|
|
||||||
|
@NSManaged public private(set) var domain: String
|
||||||
|
@NSManaged public private(set) var userID: String
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MastodonNotification {
|
||||||
|
public override func awakeFromInsert() {
|
||||||
|
super.awakeFromInsert()
|
||||||
|
setPrimitiveValue(UUID(), forKey: #keyPath(MastodonNotification.identifier))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension MastodonNotification {
|
||||||
|
@discardableResult
|
||||||
|
static func insert(
|
||||||
|
into context: NSManagedObjectContext,
|
||||||
|
domain: String,
|
||||||
|
userID: String,
|
||||||
|
networkDate: Date,
|
||||||
|
property: Property
|
||||||
|
) -> MastodonNotification {
|
||||||
|
let notification: MastodonNotification = context.insertObject()
|
||||||
|
notification.id = property.id
|
||||||
|
notification.createAt = property.createdAt
|
||||||
|
notification.updatedAt = networkDate
|
||||||
|
notification.typeRaw = property.typeRaw
|
||||||
|
notification.account = property.account
|
||||||
|
notification.status = property.status
|
||||||
|
notification.domain = domain
|
||||||
|
notification.userID = userID
|
||||||
|
return notification
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension MastodonNotification {
|
||||||
|
struct Property {
|
||||||
|
public init(id: String,
|
||||||
|
typeRaw: String,
|
||||||
|
account: MastodonUser,
|
||||||
|
status: Status?,
|
||||||
|
createdAt: Date
|
||||||
|
) {
|
||||||
|
self.id = id
|
||||||
|
self.typeRaw = typeRaw
|
||||||
|
self.account = account
|
||||||
|
self.status = status
|
||||||
|
self.createdAt = createdAt
|
||||||
|
}
|
||||||
|
|
||||||
|
public let id: String
|
||||||
|
public let typeRaw: String
|
||||||
|
public let account: MastodonUser
|
||||||
|
public let status: Status?
|
||||||
|
public let createdAt: Date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MastodonNotification {
|
||||||
|
static func predicate(domain: String) -> NSPredicate {
|
||||||
|
return NSPredicate(format: "%K == %@", #keyPath(MastodonNotification.domain), domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func predicate(userID: String) -> NSPredicate {
|
||||||
|
return NSPredicate(format: "%K == %@", #keyPath(MastodonNotification.userID), userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func predicate(typeRaw: String) -> NSPredicate {
|
||||||
|
return NSPredicate(format: "%K == %@", #keyPath(MastodonNotification.typeRaw), typeRaw)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func predicate(domain: String, userID: String, typeRaw: String? = nil) -> NSPredicate {
|
||||||
|
if let typeRaw = typeRaw {
|
||||||
|
return NSCompoundPredicate(andPredicateWithSubpredicates: [
|
||||||
|
MastodonNotification.predicate(domain: domain),
|
||||||
|
MastodonNotification.predicate(typeRaw: typeRaw),
|
||||||
|
MastodonNotification.predicate(userID: userID),
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
return NSCompoundPredicate(andPredicateWithSubpredicates: [
|
||||||
|
MastodonNotification.predicate(domain: domain),
|
||||||
|
MastodonNotification.predicate(userID: userID)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MastodonNotification: Managed {
|
||||||
|
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
||||||
|
return [NSSortDescriptor(keyPath: \MastodonNotification.createAt, ascending: false)]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
//
|
||||||
|
// Setting.swift
|
||||||
|
// CoreDataStack
|
||||||
|
//
|
||||||
|
// Created by ihugo on 2021/4/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
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 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
|
||||||
|
setting.userID = property.userID
|
||||||
|
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 let userID: String
|
||||||
|
|
||||||
|
public init(appearance: String, triggerBy: String, domain: String, userID: String) {
|
||||||
|
self.appearance = appearance
|
||||||
|
self.triggerBy = triggerBy
|
||||||
|
self.domain = domain
|
||||||
|
self.userID = userID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Setting: Managed {
|
||||||
|
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
||||||
|
return [NSSortDescriptor(keyPath: \Setting.createdAt, ascending: false)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Setting {
|
||||||
|
public static func predicate(domain: String, userID: String) -> NSPredicate {
|
||||||
|
return NSPredicate(format: "%K == %@ AND %K == %@",
|
||||||
|
#keyPath(Setting.domain), domain,
|
||||||
|
#keyPath(Setting.userID), userID
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
//
|
||||||
|
// SettingNotification+CoreDataClass.swift
|
||||||
|
// CoreDataStack
|
||||||
|
//
|
||||||
|
// Created by ihugo on 2021/4/9.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
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 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
|
||||||
|
setting.type = property.type
|
||||||
|
|
||||||
|
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(type: String) -> NSPredicate {
|
||||||
|
return NSPredicate(format: "%K == %@", #keyPath(Subscription.type), type)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
//
|
||||||
|
// PushSubscriptionAlerts+CoreDataClass.swift
|
||||||
|
// CoreDataStack
|
||||||
|
//
|
||||||
|
// Created by ihugo on 2021/4/9.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
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 private(set) var createdAt: Date
|
||||||
|
@NSManaged public private(set) var updatedAt: Date
|
||||||
|
|
||||||
|
// MARK: - relationships
|
||||||
|
@NSManaged public var subscription: 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: NSNumber?) {
|
||||||
|
guard self.favourite != favourite else { return }
|
||||||
|
self.favourite = favourite
|
||||||
|
|
||||||
|
didUpdate(at: Date())
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(follow: NSNumber?) {
|
||||||
|
guard self.follow != follow else { return }
|
||||||
|
self.follow = follow
|
||||||
|
|
||||||
|
didUpdate(at: Date())
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(mention: NSNumber?) {
|
||||||
|
guard self.mention != mention else { return }
|
||||||
|
self.mention = mention
|
||||||
|
|
||||||
|
didUpdate(at: Date())
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(poll: NSNumber?) {
|
||||||
|
guard self.poll != poll else { return }
|
||||||
|
self.poll = poll
|
||||||
|
|
||||||
|
didUpdate(at: Date())
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(reblog: NSNumber?) {
|
||||||
|
guard self.reblog != reblog else { return }
|
||||||
|
self.reblog = reblog
|
||||||
|
|
||||||
|
didUpdate(at: Date())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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?
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)]
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,6 +22,11 @@
|
||||||
"publish_post_failure": {
|
"publish_post_failure": {
|
||||||
"title": "Publish Failure",
|
"title": "Publish Failure",
|
||||||
"message": "Failed to publish the post.\nPlease check your internet connection."
|
"message": "Failed to publish the post.\nPlease check your internet connection."
|
||||||
|
},
|
||||||
|
"sign_out": {
|
||||||
|
"title": "Sign out",
|
||||||
|
"message": "Are you sure you want to sign out?",
|
||||||
|
"confirm": "Sign Out"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"controls": {
|
"controls": {
|
||||||
|
@ -325,6 +330,18 @@
|
||||||
},
|
},
|
||||||
"favorite": {
|
"favorite": {
|
||||||
"title": "Your Favorites"
|
"title": "Your Favorites"
|
||||||
|
},
|
||||||
|
"notification": {
|
||||||
|
"title": {
|
||||||
|
"Everything": "Everything",
|
||||||
|
"Mentions": "Mentions"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"follow": "followed you",
|
||||||
|
"favourite": "favorited your post",
|
||||||
|
"reblog": "rebloged your post",
|
||||||
|
"poll": "Your poll has ended",
|
||||||
|
"mention": "mentioned you"
|
||||||
},
|
},
|
||||||
"thread": {
|
"thread": {
|
||||||
"back_title": "Post",
|
"back_title": "Post",
|
||||||
|
@ -337,6 +354,41 @@
|
||||||
"single": "%s favorite",
|
"single": "%s favorite",
|
||||||
"multiple": "%s favorites"
|
"multiple": "%s favorites"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "Settings",
|
||||||
|
"section": {
|
||||||
|
"appearance": {
|
||||||
|
"title": "Appearance",
|
||||||
|
"automatic": "Automatic",
|
||||||
|
"light": "Always Light",
|
||||||
|
"dark": "Always Dark"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"title": "Notifications",
|
||||||
|
"favorites": "Favorites my post",
|
||||||
|
"follows": "Follows me",
|
||||||
|
"boosts": "Reblogs my post",
|
||||||
|
"mentions": "Mentions me",
|
||||||
|
"trigger": {
|
||||||
|
"anyone": "anyone",
|
||||||
|
"follower": "a follower",
|
||||||
|
"follow": "anyone I follow",
|
||||||
|
"noone": "no one",
|
||||||
|
"title": "Notify me when"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"boringzone": {
|
||||||
|
"title": "The Boring zone",
|
||||||
|
"terms": "Terms of Service",
|
||||||
|
"privacy": "Privacy Policy"
|
||||||
|
},
|
||||||
|
"spicyzone": {
|
||||||
|
"title": "The spicy zone",
|
||||||
|
"clear": "Clear Media Cache",
|
||||||
|
"signout": "Sign Out"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,18 +30,22 @@
|
||||||
0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D33725E6401400AAD544 /* PickServerCell.swift */; };
|
0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D33725E6401400AAD544 /* PickServerCell.swift */; };
|
||||||
18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */; };
|
18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */; };
|
||||||
2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */; };
|
2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */; };
|
||||||
|
2D084B8D26258EA3003AA3AF /* NotificationViewModel+diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D084B8C26258EA3003AA3AF /* NotificationViewModel+diffable.swift */; };
|
||||||
|
2D084B9326259545003AA3AF /* NotificationViewModel+LoadLatestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D084B9226259545003AA3AF /* NotificationViewModel+LoadLatestState.swift */; };
|
||||||
2D0B7A1D261D839600B44727 /* SearchHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D0B7A1C261D839600B44727 /* SearchHistory.swift */; };
|
2D0B7A1D261D839600B44727 /* SearchHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D0B7A1C261D839600B44727 /* SearchHistory.swift */; };
|
||||||
2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A8B25C295CC009AA50C /* StatusView.swift */; };
|
2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A8B25C295CC009AA50C /* StatusView.swift */; };
|
||||||
2D152A9225C2980C009AA50C /* UIFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A9125C2980C009AA50C /* UIFont.swift */; };
|
2D152A9225C2980C009AA50C /* UIFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A9125C2980C009AA50C /* UIFont.swift */; };
|
||||||
2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198642261BF09500F0B013 /* SearchResultItem.swift */; };
|
2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198642261BF09500F0B013 /* SearchResultItem.swift */; };
|
||||||
2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198648261C0B8500F0B013 /* SearchResultSection.swift */; };
|
2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198648261C0B8500F0B013 /* SearchResultSection.swift */; };
|
||||||
2D19864F261C372A00F0B013 /* SearchBottomLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D19864E261C372A00F0B013 /* SearchBottomLoader.swift */; };
|
|
||||||
2D198655261C3C4300F0B013 /* SearchViewModel+LoadOldestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198654261C3C4300F0B013 /* SearchViewModel+LoadOldestState.swift */; };
|
2D198655261C3C4300F0B013 /* SearchViewModel+LoadOldestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198654261C3C4300F0B013 /* SearchViewModel+LoadOldestState.swift */; };
|
||||||
2D206B7225F5D27F00143C56 /* AudioContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B7125F5D27F00143C56 /* AudioContainerView.swift */; };
|
2D206B7225F5D27F00143C56 /* AudioContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B7125F5D27F00143C56 /* AudioContainerView.swift */; };
|
||||||
2D206B8025F5F45E00143C56 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B7F25F5F45E00143C56 /* UIImage.swift */; };
|
2D206B8025F5F45E00143C56 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B7F25F5F45E00143C56 /* UIImage.swift */; };
|
||||||
2D206B8625F5FB0900143C56 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8525F5FB0900143C56 /* Double.swift */; };
|
2D206B8625F5FB0900143C56 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8525F5FB0900143C56 /* Double.swift */; };
|
||||||
2D206B8C25F6015000143C56 /* AudioPlaybackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */; };
|
2D206B8C25F6015000143C56 /* AudioPlaybackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */; };
|
||||||
2D206B9225F60EA700143C56 /* UIControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B9125F60EA700143C56 /* UIControl.swift */; };
|
2D206B9225F60EA700143C56 /* UIControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B9125F60EA700143C56 /* UIControl.swift */; };
|
||||||
|
2D24E11D2626D8B100A59D4F /* NotificationStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D24E11C2626D8B100A59D4F /* NotificationStatusTableViewCell.swift */; };
|
||||||
|
2D24E1232626ED9D00A59D4F /* UIView+Gesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D24E1222626ED9D00A59D4F /* UIView+Gesture.swift */; };
|
||||||
|
2D24E12D2626FD2E00A59D4F /* NotificationViewModel+LoadOldestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D24E12C2626FD2E00A59D4F /* NotificationViewModel+LoadOldestState.swift */; };
|
||||||
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */; };
|
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */; };
|
||||||
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAB925CB9B0500C9ED86 /* UIView.swift */; };
|
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAB925CB9B0500C9ED86 /* UIView.swift */; };
|
||||||
2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAD925CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift */; };
|
2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAD925CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift */; };
|
||||||
|
@ -49,6 +53,8 @@
|
||||||
2D34D9D126148D9E0081BFC0 /* APIService+Recommend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */; };
|
2D34D9D126148D9E0081BFC0 /* APIService+Recommend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */; };
|
||||||
2D34D9DB261494120081BFC0 /* APIService+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9DA261494120081BFC0 /* APIService+Search.swift */; };
|
2D34D9DB261494120081BFC0 /* APIService+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9DA261494120081BFC0 /* APIService+Search.swift */; };
|
||||||
2D34D9E226149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9E126149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift */; };
|
2D34D9E226149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9E126149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift */; };
|
||||||
|
2D35237A26256D920031AF25 /* NotificationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D35237926256D920031AF25 /* NotificationSection.swift */; };
|
||||||
|
2D35238126256F690031AF25 /* NotificationTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D35238026256F690031AF25 /* NotificationTableViewCell.swift */; };
|
||||||
2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D364F7125E66D7500204FDC /* MastodonResendEmailViewController.swift */; };
|
2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D364F7125E66D7500204FDC /* MastodonResendEmailViewController.swift */; };
|
||||||
2D364F7825E66D8300204FDC /* MastodonResendEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D364F7725E66D8300204FDC /* MastodonResendEmailViewModel.swift */; };
|
2D364F7825E66D8300204FDC /* MastodonResendEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D364F7725E66D8300204FDC /* MastodonResendEmailViewModel.swift */; };
|
||||||
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */; };
|
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */; };
|
||||||
|
@ -76,6 +82,9 @@
|
||||||
2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D2725CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift */; };
|
2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D2725CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift */; };
|
||||||
2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */; };
|
2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */; };
|
||||||
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */; };
|
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */; };
|
||||||
|
2D607AD826242FC500B70763 /* NotificationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D607AD726242FC500B70763 /* NotificationViewModel.swift */; };
|
||||||
|
2D6125472625436B00299647 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D6125462625436B00299647 /* Notification.swift */; };
|
||||||
|
2D61254D262547C200299647 /* APIService+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61254C262547C200299647 /* APIService+Notification.swift */; };
|
||||||
2D61335825C188A000CAE157 /* APIService+Persist+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335725C188A000CAE157 /* APIService+Persist+Status.swift */; };
|
2D61335825C188A000CAE157 /* APIService+Persist+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335725C188A000CAE157 /* APIService+Persist+Status.swift */; };
|
||||||
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; };
|
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; };
|
||||||
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */; };
|
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */; };
|
||||||
|
@ -91,6 +100,7 @@
|
||||||
2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76319E25C1521200929FB9 /* StatusSection.swift */; };
|
2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76319E25C1521200929FB9 /* StatusSection.swift */; };
|
||||||
2D7631A825C1535600929FB9 /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7631A725C1535600929FB9 /* StatusTableViewCell.swift */; };
|
2D7631A825C1535600929FB9 /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7631A725C1535600929FB9 /* StatusTableViewCell.swift */; };
|
||||||
2D7631B325C159F700929FB9 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7631B225C159F700929FB9 /* Item.swift */; };
|
2D7631B325C159F700929FB9 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7631B225C159F700929FB9 /* Item.swift */; };
|
||||||
|
2D7867192625B77500211898 /* NotificationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7867182625B77500211898 /* NotificationItem.swift */; };
|
||||||
2D79E701261EA5550011E398 /* APIService+CoreData+Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D79E700261EA5550011E398 /* APIService+CoreData+Tag.swift */; };
|
2D79E701261EA5550011E398 /* APIService+CoreData+Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D79E700261EA5550011E398 /* APIService+CoreData+Tag.swift */; };
|
||||||
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */; };
|
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */; };
|
||||||
2D82BA0525E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D82BA0425E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift */; };
|
2D82BA0525E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D82BA0425E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift */; };
|
||||||
|
@ -110,6 +120,7 @@
|
||||||
2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */; };
|
2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */; };
|
||||||
2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */; };
|
2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */; };
|
||||||
2DA7D05725CA693F00804E11 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D05625CA693F00804E11 /* Application.swift */; };
|
2DA7D05725CA693F00804E11 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D05625CA693F00804E11 /* Application.swift */; };
|
||||||
|
2DB72C8C262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DB72C8B262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift */; };
|
||||||
2DCB73FD2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DCB73FC2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift */; };
|
2DCB73FD2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DCB73FC2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift */; };
|
||||||
2DE0FAC12615F04D00CDF649 /* RecommendHashTagSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */; };
|
2DE0FAC12615F04D00CDF649 /* RecommendHashTagSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */; };
|
||||||
2DE0FAC82615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE0FAC72615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift */; };
|
2DE0FAC82615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE0FAC72615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift */; };
|
||||||
|
@ -122,10 +133,21 @@
|
||||||
2DF75BC725D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BC625D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift */; };
|
2DF75BC725D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BC625D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift */; };
|
||||||
2DFAD5272616F9D300F9EE7C /* SearchViewController+Searching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFAD5262616F9D300F9EE7C /* SearchViewController+Searching.swift */; };
|
2DFAD5272616F9D300F9EE7C /* SearchViewController+Searching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFAD5262616F9D300F9EE7C /* SearchViewController+Searching.swift */; };
|
||||||
2DFAD5372617010500F9EE7C /* SearchingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFAD5362617010500F9EE7C /* SearchingTableViewCell.swift */; };
|
2DFAD5372617010500F9EE7C /* SearchingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFAD5362617010500F9EE7C /* SearchingTableViewCell.swift */; };
|
||||||
2DFF41892614A4DC00F776A4 /* UIView+Constraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFF41882614A4DC00F776A4 /* UIView+Constraint.swift */; };
|
5B90C45E262599800002E742 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C456262599800002E742 /* SettingsViewModel.swift */; };
|
||||||
|
5B90C45F262599800002E742 /* SettingsToggleTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C459262599800002E742 /* SettingsToggleTableViewCell.swift */; };
|
||||||
|
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 */; };
|
||||||
|
5B90C48526259BF10002E742 /* APIService+Subscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C48426259BF10002E742 /* APIService+Subscriptions.swift */; };
|
||||||
|
5B90C48B26259C120002E742 /* APIService+CoreData+Subscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C48A26259C120002E742 /* APIService+CoreData+Subscriptions.swift */; };
|
||||||
5D0393902612D259007FE196 /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D03938F2612D259007FE196 /* WebViewController.swift */; };
|
5D0393902612D259007FE196 /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D03938F2612D259007FE196 /* WebViewController.swift */; };
|
||||||
5D0393962612D266007FE196 /* WebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D0393952612D266007FE196 /* WebViewModel.swift */; };
|
5D0393962612D266007FE196 /* WebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D0393952612D266007FE196 /* WebViewModel.swift */; };
|
||||||
5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* MastodonSDK */; };
|
5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* MastodonSDK */; };
|
||||||
|
5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DA732CB2629CEF500A92342 /* UIView+Remove.swift */; };
|
||||||
5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */; };
|
5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */; };
|
||||||
5DDDF1992617447F00311060 /* Mastodon+Entity+Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */; };
|
5DDDF1992617447F00311060 /* Mastodon+Entity+Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */; };
|
||||||
5DDDF1A92617489F00311060 /* Mastodon+Entity+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */; };
|
5DDDF1A92617489F00311060 /* Mastodon+Entity+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */; };
|
||||||
|
@ -423,18 +445,22 @@
|
||||||
0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerSearchCell.swift; sourceTree = "<group>"; };
|
0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerSearchCell.swift; sourceTree = "<group>"; };
|
||||||
0FB3D33725E6401400AAD544 /* PickServerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCell.swift; sourceTree = "<group>"; };
|
0FB3D33725E6401400AAD544 /* PickServerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCell.swift; sourceTree = "<group>"; };
|
||||||
2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+PublicTimeline.swift"; sourceTree = "<group>"; };
|
2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+PublicTimeline.swift"; sourceTree = "<group>"; };
|
||||||
|
2D084B8C26258EA3003AA3AF /* NotificationViewModel+diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationViewModel+diffable.swift"; sourceTree = "<group>"; };
|
||||||
|
2D084B9226259545003AA3AF /* NotificationViewModel+LoadLatestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationViewModel+LoadLatestState.swift"; sourceTree = "<group>"; };
|
||||||
2D0B7A1C261D839600B44727 /* SearchHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistory.swift; sourceTree = "<group>"; };
|
2D0B7A1C261D839600B44727 /* SearchHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistory.swift; sourceTree = "<group>"; };
|
||||||
2D152A8B25C295CC009AA50C /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = "<group>"; };
|
2D152A8B25C295CC009AA50C /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = "<group>"; };
|
||||||
2D152A9125C2980C009AA50C /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = "<group>"; };
|
2D152A9125C2980C009AA50C /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = "<group>"; };
|
||||||
2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = "<group>"; };
|
2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = "<group>"; };
|
||||||
2D198648261C0B8500F0B013 /* SearchResultSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultSection.swift; sourceTree = "<group>"; };
|
2D198648261C0B8500F0B013 /* SearchResultSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultSection.swift; sourceTree = "<group>"; };
|
||||||
2D19864E261C372A00F0B013 /* SearchBottomLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBottomLoader.swift; sourceTree = "<group>"; };
|
|
||||||
2D198654261C3C4300F0B013 /* SearchViewModel+LoadOldestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewModel+LoadOldestState.swift"; sourceTree = "<group>"; };
|
2D198654261C3C4300F0B013 /* SearchViewModel+LoadOldestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewModel+LoadOldestState.swift"; sourceTree = "<group>"; };
|
||||||
2D206B7125F5D27F00143C56 /* AudioContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioContainerView.swift; sourceTree = "<group>"; };
|
2D206B7125F5D27F00143C56 /* AudioContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioContainerView.swift; sourceTree = "<group>"; };
|
||||||
2D206B7F25F5F45E00143C56 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = "<group>"; };
|
2D206B7F25F5F45E00143C56 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = "<group>"; };
|
||||||
2D206B8525F5FB0900143C56 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = "<group>"; };
|
2D206B8525F5FB0900143C56 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = "<group>"; };
|
||||||
2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlaybackService.swift; sourceTree = "<group>"; };
|
2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlaybackService.swift; sourceTree = "<group>"; };
|
||||||
2D206B9125F60EA700143C56 /* UIControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControl.swift; sourceTree = "<group>"; };
|
2D206B9125F60EA700143C56 /* UIControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControl.swift; sourceTree = "<group>"; };
|
||||||
|
2D24E11C2626D8B100A59D4F /* NotificationStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationStatusTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
2D24E1222626ED9D00A59D4F /* UIView+Gesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Gesture.swift"; sourceTree = "<group>"; };
|
||||||
|
2D24E12C2626FD2E00A59D4F /* NotificationViewModel+LoadOldestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationViewModel+LoadOldestState.swift"; sourceTree = "<group>"; };
|
||||||
2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderTableViewCell.swift; sourceTree = "<group>"; };
|
2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
2D32EAB925CB9B0500C9ED86 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = "<group>"; };
|
2D32EAB925CB9B0500C9ED86 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = "<group>"; };
|
||||||
2D32EAD925CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewModel+LoadMiddleState.swift"; sourceTree = "<group>"; };
|
2D32EAD925CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewModel+LoadMiddleState.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -442,6 +468,8 @@
|
||||||
2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Recommend.swift"; sourceTree = "<group>"; };
|
2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Recommend.swift"; sourceTree = "<group>"; };
|
||||||
2D34D9DA261494120081BFC0 /* APIService+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Search.swift"; sourceTree = "<group>"; };
|
2D34D9DA261494120081BFC0 /* APIService+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Search.swift"; sourceTree = "<group>"; };
|
||||||
2D34D9E126149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendTagsCollectionViewCell.swift; sourceTree = "<group>"; };
|
2D34D9E126149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendTagsCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
2D35237926256D920031AF25 /* NotificationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSection.swift; sourceTree = "<group>"; };
|
||||||
|
2D35238026256F690031AF25 /* NotificationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
2D364F7125E66D7500204FDC /* MastodonResendEmailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewController.swift; sourceTree = "<group>"; };
|
2D364F7125E66D7500204FDC /* MastodonResendEmailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewController.swift; sourceTree = "<group>"; };
|
||||||
2D364F7725E66D8300204FDC /* MastodonResendEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewModel.swift; sourceTree = "<group>"; };
|
2D364F7725E66D8300204FDC /* MastodonResendEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewModel.swift; sourceTree = "<group>"; };
|
||||||
2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentOffsetAdjustableTimelineViewControllerDelegate.swift; sourceTree = "<group>"; };
|
2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentOffsetAdjustableTimelineViewControllerDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
@ -467,6 +495,9 @@
|
||||||
2D5A3D2725CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
2D5A3D2725CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||||
2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewContainer.swift; sourceTree = "<group>"; };
|
2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewContainer.swift; sourceTree = "<group>"; };
|
||||||
2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewController+DebugAction.swift"; sourceTree = "<group>"; };
|
2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewController+DebugAction.swift"; sourceTree = "<group>"; };
|
||||||
|
2D607AD726242FC500B70763 /* NotificationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
2D6125462625436B00299647 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = "<group>"; };
|
||||||
|
2D61254C262547C200299647 /* APIService+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Notification.swift"; sourceTree = "<group>"; };
|
||||||
2D61335725C188A000CAE157 /* APIService+Persist+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+Status.swift"; sourceTree = "<group>"; };
|
2D61335725C188A000CAE157 /* APIService+Persist+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+Status.swift"; sourceTree = "<group>"; };
|
||||||
2D61335D25C1894B00CAE157 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = "<group>"; };
|
2D61335D25C1894B00CAE157 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = "<group>"; };
|
||||||
2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error+Detail.swift"; sourceTree = "<group>"; };
|
2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error+Detail.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -481,6 +512,7 @@
|
||||||
2D76319E25C1521200929FB9 /* StatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusSection.swift; sourceTree = "<group>"; };
|
2D76319E25C1521200929FB9 /* StatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusSection.swift; sourceTree = "<group>"; };
|
||||||
2D7631A725C1535600929FB9 /* StatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = "<group>"; };
|
2D7631A725C1535600929FB9 /* StatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
2D7631B225C159F700929FB9 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = "<group>"; };
|
2D7631B225C159F700929FB9 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = "<group>"; };
|
||||||
|
2D7867182625B77500211898 /* NotificationItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItem.swift; sourceTree = "<group>"; };
|
||||||
2D79E700261EA5550011E398 /* APIService+CoreData+Tag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Tag.swift"; sourceTree = "<group>"; };
|
2D79E700261EA5550011E398 /* APIService+CoreData+Tag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Tag.swift"; sourceTree = "<group>"; };
|
||||||
2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewControllerAppearance.swift; sourceTree = "<group>"; };
|
2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewControllerAppearance.swift; sourceTree = "<group>"; };
|
||||||
2D82BA0425E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewModelNavigationDelegateShim.swift; sourceTree = "<group>"; };
|
2D82BA0425E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewModelNavigationDelegateShim.swift; sourceTree = "<group>"; };
|
||||||
|
@ -500,6 +532,7 @@
|
||||||
2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBottomLoaderTableViewCell.swift; sourceTree = "<group>"; };
|
2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBottomLoaderTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
2DA7D05025CA545E00804E11 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = "<group>"; };
|
2DA7D05025CA545E00804E11 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = "<group>"; };
|
||||||
2DA7D05625CA693F00804E11 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
|
2DA7D05625CA693F00804E11 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
|
||||||
|
2DB72C8B262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Notification+Type.swift"; sourceTree = "<group>"; };
|
||||||
2DCB73FC2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendCollectionHeader.swift; sourceTree = "<group>"; };
|
2DCB73FC2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendCollectionHeader.swift; sourceTree = "<group>"; };
|
||||||
2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendHashTagSection.swift; sourceTree = "<group>"; };
|
2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendHashTagSection.swift; sourceTree = "<group>"; };
|
||||||
2DE0FAC72615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendAccountsCollectionViewCell.swift; sourceTree = "<group>"; };
|
2DE0FAC72615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendAccountsCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
@ -512,13 +545,24 @@
|
||||||
2DF75BC625D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedObjectContextObjectsDidChange.swift; sourceTree = "<group>"; };
|
2DF75BC625D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedObjectContextObjectsDidChange.swift; sourceTree = "<group>"; };
|
||||||
2DFAD5262616F9D300F9EE7C /* SearchViewController+Searching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewController+Searching.swift"; sourceTree = "<group>"; };
|
2DFAD5262616F9D300F9EE7C /* SearchViewController+Searching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewController+Searching.swift"; sourceTree = "<group>"; };
|
||||||
2DFAD5362617010500F9EE7C /* SearchingTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingTableViewCell.swift; sourceTree = "<group>"; };
|
2DFAD5362617010500F9EE7C /* SearchingTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
2DFF41882614A4DC00F776A4 /* UIView+Constraint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Constraint.swift"; sourceTree = "<group>"; };
|
|
||||||
2E1F6A67FDF9771D3E064FDC /* Pods-Mastodon.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.debug.xcconfig"; sourceTree = "<group>"; };
|
2E1F6A67FDF9771D3E064FDC /* Pods-Mastodon.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
459EA4F43058CAB47719E963 /* Pods-Mastodon-MastodonUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.debug.xcconfig"; sourceTree = "<group>"; };
|
459EA4F43058CAB47719E963 /* Pods-Mastodon-MastodonUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
5B90C456262599800002E742 /* SettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
5B90C459262599800002E742 /* SettingsToggleTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsToggleTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
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>"; };
|
||||||
|
5B90C48426259BF10002E742 /* APIService+Subscriptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+Subscriptions.swift"; sourceTree = "<group>"; };
|
||||||
|
5B90C48A26259C120002E742 /* APIService+CoreData+Subscriptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Subscriptions.swift"; sourceTree = "<group>"; };
|
||||||
5D03938F2612D259007FE196 /* WebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = "<group>"; };
|
5D03938F2612D259007FE196 /* WebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = "<group>"; };
|
||||||
5D0393952612D266007FE196 /* WebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewModel.swift; sourceTree = "<group>"; };
|
5D0393952612D266007FE196 /* WebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
5DA732CB2629CEF500A92342 /* UIView+Remove.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Remove.swift"; sourceTree = "<group>"; };
|
||||||
5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Account.swift"; sourceTree = "<group>"; };
|
5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Account.swift"; sourceTree = "<group>"; };
|
||||||
5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Tag.swift"; sourceTree = "<group>"; };
|
5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Tag.swift"; sourceTree = "<group>"; };
|
||||||
5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+History.swift"; sourceTree = "<group>"; };
|
5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+History.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -907,6 +951,15 @@
|
||||||
path = CollectionViewCell;
|
path = CollectionViewCell;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
2D35237F26256F470031AF25 /* TableViewCell */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
2D35238026256F690031AF25 /* NotificationTableViewCell.swift */,
|
||||||
|
2D24E11C2626D8B100A59D4F /* NotificationStatusTableViewCell.swift */,
|
||||||
|
);
|
||||||
|
path = TableViewCell;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
2D364F7025E66D5B00204FDC /* ResendEmail */ = {
|
2D364F7025E66D5B00204FDC /* ResendEmail */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1063,6 +1116,7 @@
|
||||||
DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */,
|
DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */,
|
||||||
2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */,
|
2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */,
|
||||||
2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */,
|
2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */,
|
||||||
|
2D35237926256D920031AF25 /* NotificationSection.swift */,
|
||||||
2D198648261C0B8500F0B013 /* SearchResultSection.swift */,
|
2D198648261C0B8500F0B013 /* SearchResultSection.swift */,
|
||||||
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */,
|
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */,
|
||||||
DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */,
|
DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */,
|
||||||
|
@ -1116,6 +1170,7 @@
|
||||||
children = (
|
children = (
|
||||||
2D7631B225C159F700929FB9 /* Item.swift */,
|
2D7631B225C159F700929FB9 /* Item.swift */,
|
||||||
2D198642261BF09500F0B013 /* SearchResultItem.swift */,
|
2D198642261BF09500F0B013 /* SearchResultItem.swift */,
|
||||||
|
2D7867182625B77500211898 /* NotificationItem.swift */,
|
||||||
DB4481CB25EE2AFE00BEFB67 /* PollItem.swift */,
|
DB4481CB25EE2AFE00BEFB67 /* PollItem.swift */,
|
||||||
DB1E347725F519300079D7DF /* PickServerItem.swift */,
|
DB1E347725F519300079D7DF /* PickServerItem.swift */,
|
||||||
DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */,
|
DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */,
|
||||||
|
@ -1154,7 +1209,6 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
2DFAD5362617010500F9EE7C /* SearchingTableViewCell.swift */,
|
2DFAD5362617010500F9EE7C /* SearchingTableViewCell.swift */,
|
||||||
2D19864E261C372A00F0B013 /* SearchBottomLoader.swift */,
|
|
||||||
);
|
);
|
||||||
path = TableViewCell;
|
path = TableViewCell;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1169,6 +1223,35 @@
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
5B90C455262599800002E742 /* Settings */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
5B90C457262599800002E742 /* View */,
|
||||||
|
5B90C456262599800002E742 /* SettingsViewModel.swift */,
|
||||||
|
5B90C45D262599800002E742 /* SettingsViewController.swift */,
|
||||||
|
);
|
||||||
|
path = Settings;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
5B90C457262599800002E742 /* View */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
5B90C458262599800002E742 /* Cell */,
|
||||||
|
5B90C45C262599800002E742 /* SettingsSectionHeader.swift */,
|
||||||
|
);
|
||||||
|
path = View;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
5B90C458262599800002E742 /* Cell */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
5B90C459262599800002E742 /* SettingsToggleTableViewCell.swift */,
|
||||||
|
5B90C45A262599800002E742 /* SettingsAppearanceTableViewCell.swift */,
|
||||||
|
5B90C45B262599800002E742 /* SettingsLinkTableViewCell.swift */,
|
||||||
|
);
|
||||||
|
path = Cell;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
5D03938E2612D200007FE196 /* Webview */ = {
|
5D03938E2612D200007FE196 /* Webview */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1346,6 +1429,7 @@
|
||||||
DB938F1426241FDF00E5B6C1 /* APIService+Thread.swift */,
|
DB938F1426241FDF00E5B6C1 /* APIService+Thread.swift */,
|
||||||
DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */,
|
DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */,
|
||||||
DB9A488326034BD7008B817C /* APIService+Status.swift */,
|
DB9A488326034BD7008B817C /* APIService+Status.swift */,
|
||||||
|
2D61254C262547C200299647 /* APIService+Notification.swift */,
|
||||||
DB9A488F26035963008B817C /* APIService+Media.swift */,
|
DB9A488F26035963008B817C /* APIService+Media.swift */,
|
||||||
2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */,
|
2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */,
|
||||||
2D34D9DA261494120081BFC0 /* APIService+Search.swift */,
|
2D34D9DA261494120081BFC0 /* APIService+Search.swift */,
|
||||||
|
@ -1354,6 +1438,7 @@
|
||||||
DBAE3F932616E28B004B8251 /* APIService+Follow.swift */,
|
DBAE3F932616E28B004B8251 /* APIService+Follow.swift */,
|
||||||
DBAE3F8D2616E0B1004B8251 /* APIService+Block.swift */,
|
DBAE3F8D2616E0B1004B8251 /* APIService+Block.swift */,
|
||||||
DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */,
|
DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */,
|
||||||
|
5B90C48426259BF10002E742 /* APIService+Subscriptions.swift */,
|
||||||
);
|
);
|
||||||
path = APIService;
|
path = APIService;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1365,6 +1450,7 @@
|
||||||
DB45FADC25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift */,
|
DB45FADC25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift */,
|
||||||
DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */,
|
DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */,
|
||||||
2D79E700261EA5550011E398 /* APIService+CoreData+Tag.swift */,
|
2D79E700261EA5550011E398 /* APIService+CoreData+Tag.swift */,
|
||||||
|
5B90C48A26259C120002E742 /* APIService+CoreData+Subscriptions.swift */,
|
||||||
);
|
);
|
||||||
path = CoreData;
|
path = CoreData;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1425,6 +1511,7 @@
|
||||||
5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */,
|
5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */,
|
||||||
5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */,
|
5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */,
|
||||||
2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */,
|
2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */,
|
||||||
|
2DB72C8B262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift */,
|
||||||
);
|
);
|
||||||
path = MastodonSDK;
|
path = MastodonSDK;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1521,6 +1608,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
DB89BA2625C110B4008580ED /* Status.swift */,
|
DB89BA2625C110B4008580ED /* Status.swift */,
|
||||||
|
2D6125462625436B00299647 /* Notification.swift */,
|
||||||
2D0B7A1C261D839600B44727 /* SearchHistory.swift */,
|
2D0B7A1C261D839600B44727 /* SearchHistory.swift */,
|
||||||
DB8AF52425C131D1002E6C99 /* MastodonUser.swift */,
|
DB8AF52425C131D1002E6C99 /* MastodonUser.swift */,
|
||||||
DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */,
|
DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */,
|
||||||
|
@ -1534,6 +1622,9 @@
|
||||||
DB4481AC25EE155900BEFB67 /* Poll.swift */,
|
DB4481AC25EE155900BEFB67 /* Poll.swift */,
|
||||||
DB4481B225EE16D000BEFB67 /* PollOption.swift */,
|
DB4481B225EE16D000BEFB67 /* PollOption.swift */,
|
||||||
DBCC3B9A2615849F0045B23D /* PrivateNote.swift */,
|
DBCC3B9A2615849F0045B23D /* PrivateNote.swift */,
|
||||||
|
5B90C46D26259B2C0002E742 /* Setting.swift */,
|
||||||
|
5B90C46C26259B2C0002E742 /* Subscription.swift */,
|
||||||
|
5B90C47E26259BA90002E742 /* SubscriptionAlerts.swift */,
|
||||||
);
|
);
|
||||||
path = Entity;
|
path = Entity;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1585,6 +1676,7 @@
|
||||||
2D76316325C14BAC00929FB9 /* PublicTimeline */,
|
2D76316325C14BAC00929FB9 /* PublicTimeline */,
|
||||||
0F2021F5261325ED000C64BF /* HashtagTimeline */,
|
0F2021F5261325ED000C64BF /* HashtagTimeline */,
|
||||||
DB9D6BEE25E4F5370051B173 /* Search */,
|
DB9D6BEE25E4F5370051B173 /* Search */,
|
||||||
|
5B90C455262599800002E742 /* Settings */,
|
||||||
DB9D6BFD25E4F57B0051B173 /* Notification */,
|
DB9D6BFD25E4F57B0051B173 /* Notification */,
|
||||||
DB9D6C0825E4F5A60051B173 /* Profile */,
|
DB9D6C0825E4F5A60051B173 /* Profile */,
|
||||||
DB789A1025F9F29B0071ACA0 /* Compose */,
|
DB789A1025F9F29B0071ACA0 /* Compose */,
|
||||||
|
@ -1617,8 +1709,9 @@
|
||||||
5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */,
|
5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */,
|
||||||
DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */,
|
DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */,
|
||||||
2D32EAB925CB9B0500C9ED86 /* UIView.swift */,
|
2D32EAB925CB9B0500C9ED86 /* UIView.swift */,
|
||||||
2DFF41882614A4DC00F776A4 /* UIView+Constraint.swift */,
|
5DA732CB2629CEF500A92342 /* UIView+Remove.swift */,
|
||||||
DB8AF55C25C138B7002E6C99 /* UIViewController.swift */,
|
DB8AF55C25C138B7002E6C99 /* UIViewController.swift */,
|
||||||
|
2D24E1222626ED9D00A59D4F /* UIView+Gesture.swift */,
|
||||||
2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */,
|
2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */,
|
||||||
2D84350425FF858100EECE90 /* UIScrollView.swift */,
|
2D84350425FF858100EECE90 /* UIScrollView.swift */,
|
||||||
DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */,
|
DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */,
|
||||||
|
@ -1691,6 +1784,11 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */,
|
DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */,
|
||||||
|
2D607AD726242FC500B70763 /* NotificationViewModel.swift */,
|
||||||
|
2D084B8C26258EA3003AA3AF /* NotificationViewModel+diffable.swift */,
|
||||||
|
2D084B9226259545003AA3AF /* NotificationViewModel+LoadLatestState.swift */,
|
||||||
|
2D24E12C2626FD2E00A59D4F /* NotificationViewModel+LoadOldestState.swift */,
|
||||||
|
2D35237F26256F470031AF25 /* TableViewCell */,
|
||||||
);
|
);
|
||||||
path = Notification;
|
path = Notification;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -2262,6 +2360,7 @@
|
||||||
DB938F0F2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift in Sources */,
|
DB938F0F2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift in Sources */,
|
||||||
5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */,
|
5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */,
|
||||||
DBAE3F882615DDF4004B8251 /* UserProviderFacade.swift in Sources */,
|
DBAE3F882615DDF4004B8251 /* UserProviderFacade.swift in Sources */,
|
||||||
|
2D607AD826242FC500B70763 /* NotificationViewModel.swift in Sources */,
|
||||||
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */,
|
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */,
|
||||||
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */,
|
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */,
|
||||||
DB71FD4625F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift in Sources */,
|
DB71FD4625F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift in Sources */,
|
||||||
|
@ -2280,9 +2379,10 @@
|
||||||
DBE3CE07261D6A0E00430CC6 /* FavoriteViewModel+Diffable.swift in Sources */,
|
DBE3CE07261D6A0E00430CC6 /* FavoriteViewModel+Diffable.swift in Sources */,
|
||||||
2D61335825C188A000CAE157 /* APIService+Persist+Status.swift in Sources */,
|
2D61335825C188A000CAE157 /* APIService+Persist+Status.swift in Sources */,
|
||||||
2D34D9DB261494120081BFC0 /* APIService+Search.swift in Sources */,
|
2D34D9DB261494120081BFC0 /* APIService+Search.swift in Sources */,
|
||||||
|
5B90C48B26259C120002E742 /* APIService+CoreData+Subscriptions.swift in Sources */,
|
||||||
DBB5256E2612D5A1002F1F29 /* ProfileStatusDashboardView.swift in Sources */,
|
DBB5256E2612D5A1002F1F29 /* ProfileStatusDashboardView.swift in Sources */,
|
||||||
|
2D24E1232626ED9D00A59D4F /* UIView+Gesture.swift in Sources */,
|
||||||
DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */,
|
DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */,
|
||||||
2D19864F261C372A00F0B013 /* SearchBottomLoader.swift in Sources */,
|
|
||||||
DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */,
|
DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */,
|
||||||
0F202213261351F5000C64BF /* APIService+HashtagTimeline.swift in Sources */,
|
0F202213261351F5000C64BF /* APIService+HashtagTimeline.swift in Sources */,
|
||||||
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */,
|
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */,
|
||||||
|
@ -2294,10 +2394,12 @@
|
||||||
DB49A62B25FF36C700B98345 /* APIService+CustomEmoji.swift in Sources */,
|
DB49A62B25FF36C700B98345 /* APIService+CustomEmoji.swift in Sources */,
|
||||||
DBCBED1D26132E1A00B49291 /* StatusFetchedResultsController.swift in Sources */,
|
DBCBED1D26132E1A00B49291 /* StatusFetchedResultsController.swift in Sources */,
|
||||||
2D79E701261EA5550011E398 /* APIService+CoreData+Tag.swift in Sources */,
|
2D79E701261EA5550011E398 /* APIService+CoreData+Tag.swift in Sources */,
|
||||||
|
5B90C461262599800002E742 /* SettingsLinkTableViewCell.swift in Sources */,
|
||||||
DBE3CDEC261C6B2900430CC6 /* FavoriteViewController.swift in Sources */,
|
DBE3CDEC261C6B2900430CC6 /* FavoriteViewController.swift in Sources */,
|
||||||
DB938EE62623F50700E5B6C1 /* ThreadViewController.swift in Sources */,
|
DB938EE62623F50700E5B6C1 /* ThreadViewController.swift in Sources */,
|
||||||
2D939AB525EDD8A90076FA61 /* String.swift in Sources */,
|
2D939AB525EDD8A90076FA61 /* String.swift in Sources */,
|
||||||
DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */,
|
DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */,
|
||||||
|
2D35238126256F690031AF25 /* NotificationTableViewCell.swift in Sources */,
|
||||||
DBE3CDBB261C427900430CC6 /* TimelineHeaderTableViewCell.swift in Sources */,
|
DBE3CDBB261C427900430CC6 /* TimelineHeaderTableViewCell.swift in Sources */,
|
||||||
0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */,
|
0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */,
|
||||||
2D38F1D525CD465300561493 /* HomeTimelineViewController.swift in Sources */,
|
2D38F1D525CD465300561493 /* HomeTimelineViewController.swift in Sources */,
|
||||||
|
@ -2317,6 +2419,7 @@
|
||||||
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */,
|
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */,
|
||||||
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */,
|
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */,
|
||||||
DBE3CE0D261D767100430CC6 /* FavoriteViewController+StatusProvider.swift in Sources */,
|
DBE3CE0D261D767100430CC6 /* FavoriteViewController+StatusProvider.swift in Sources */,
|
||||||
|
2D084B9326259545003AA3AF /* NotificationViewModel+LoadLatestState.swift in Sources */,
|
||||||
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
|
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
|
||||||
DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */,
|
DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */,
|
||||||
DBB9759C262462E1004620BD /* ThreadMetaView.swift in Sources */,
|
DBB9759C262462E1004620BD /* ThreadMetaView.swift in Sources */,
|
||||||
|
@ -2324,6 +2427,7 @@
|
||||||
5DDDF1A92617489F00311060 /* Mastodon+Entity+History.swift in Sources */,
|
5DDDF1A92617489F00311060 /* Mastodon+Entity+History.swift in Sources */,
|
||||||
DB59F11825EFA35B001F1DAB /* StripProgressView.swift in Sources */,
|
DB59F11825EFA35B001F1DAB /* StripProgressView.swift in Sources */,
|
||||||
DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */,
|
DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */,
|
||||||
|
5B90C463262599800002E742 /* SettingsViewController.swift in Sources */,
|
||||||
DB73B490261F030A002E9E9F /* SafariActivity.swift in Sources */,
|
DB73B490261F030A002E9E9F /* SafariActivity.swift in Sources */,
|
||||||
DB9A48962603685D008B817C /* MastodonAttachmentService+UploadState.swift in Sources */,
|
DB9A48962603685D008B817C /* MastodonAttachmentService+UploadState.swift in Sources */,
|
||||||
2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */,
|
2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */,
|
||||||
|
@ -2331,6 +2435,7 @@
|
||||||
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */,
|
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */,
|
||||||
2DF75B9B25D0E27500694EC8 /* StatusProviderFacade.swift in Sources */,
|
2DF75B9B25D0E27500694EC8 /* StatusProviderFacade.swift in Sources */,
|
||||||
DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */,
|
DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */,
|
||||||
|
5B90C45E262599800002E742 /* SettingsViewModel.swift in Sources */,
|
||||||
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */,
|
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */,
|
||||||
5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */,
|
5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */,
|
||||||
2DE0FAC12615F04D00CDF649 /* RecommendHashTagSection.swift in Sources */,
|
2DE0FAC12615F04D00CDF649 /* RecommendHashTagSection.swift in Sources */,
|
||||||
|
@ -2343,6 +2448,7 @@
|
||||||
DB71FD3C25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift in Sources */,
|
DB71FD3C25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift in Sources */,
|
||||||
2DA6055125F74407006356F9 /* AudioContainerViewModel.swift in Sources */,
|
2DA6055125F74407006356F9 /* AudioContainerViewModel.swift in Sources */,
|
||||||
0FB3D2FE25E4CB6400AAD544 /* PickServerTitleCell.swift in Sources */,
|
0FB3D2FE25E4CB6400AAD544 /* PickServerTitleCell.swift in Sources */,
|
||||||
|
5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */,
|
||||||
2D38F1DF25CD46A400561493 /* HomeTimelineViewController+StatusProvider.swift in Sources */,
|
2D38F1DF25CD46A400561493 /* HomeTimelineViewController+StatusProvider.swift in Sources */,
|
||||||
2D206B9225F60EA700143C56 /* UIControl.swift in Sources */,
|
2D206B9225F60EA700143C56 /* UIControl.swift in Sources */,
|
||||||
2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */,
|
2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */,
|
||||||
|
@ -2351,6 +2457,7 @@
|
||||||
DB7F48452620241000796008 /* ProfileHeaderViewModel.swift in Sources */,
|
DB7F48452620241000796008 /* ProfileHeaderViewModel.swift in Sources */,
|
||||||
2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */,
|
2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */,
|
||||||
5DDDF1992617447F00311060 /* Mastodon+Entity+Tag.swift in Sources */,
|
5DDDF1992617447F00311060 /* Mastodon+Entity+Tag.swift in Sources */,
|
||||||
|
5B90C45F262599800002E742 /* SettingsToggleTableViewCell.swift in Sources */,
|
||||||
2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */,
|
2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */,
|
||||||
DBAE3F682615DD60004B8251 /* UserProvider.swift in Sources */,
|
DBAE3F682615DD60004B8251 /* UserProvider.swift in Sources */,
|
||||||
DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */,
|
DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */,
|
||||||
|
@ -2360,6 +2467,7 @@
|
||||||
0F20222D261457EE000C64BF /* HashtagTimelineViewModel+LoadOldestState.swift in Sources */,
|
0F20222D261457EE000C64BF /* HashtagTimelineViewModel+LoadOldestState.swift in Sources */,
|
||||||
DB4563BD25E11A24004DA0B9 /* KeyboardResponderService.swift in Sources */,
|
DB4563BD25E11A24004DA0B9 /* KeyboardResponderService.swift in Sources */,
|
||||||
DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */,
|
DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */,
|
||||||
|
5B90C462262599800002E742 /* SettingsSectionHeader.swift in Sources */,
|
||||||
DB44768B260B3F2100B66B82 /* CustomEmojiPickerItem.swift in Sources */,
|
DB44768B260B3F2100B66B82 /* CustomEmojiPickerItem.swift in Sources */,
|
||||||
2DF75BA125D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift in Sources */,
|
2DF75BA125D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift in Sources */,
|
||||||
5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */,
|
5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */,
|
||||||
|
@ -2376,14 +2484,17 @@
|
||||||
DB789A1C25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift in Sources */,
|
DB789A1C25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift in Sources */,
|
||||||
DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */,
|
DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */,
|
||||||
2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */,
|
2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */,
|
||||||
|
2D084B8D26258EA3003AA3AF /* NotificationViewModel+diffable.swift in Sources */,
|
||||||
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
|
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
|
||||||
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */,
|
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */,
|
||||||
DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */,
|
DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */,
|
||||||
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */,
|
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */,
|
||||||
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */,
|
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */,
|
||||||
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */,
|
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */,
|
||||||
|
2D24E11D2626D8B100A59D4F /* NotificationStatusTableViewCell.swift in Sources */,
|
||||||
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */,
|
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */,
|
||||||
DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */,
|
DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */,
|
||||||
|
2D61254D262547C200299647 /* APIService+Notification.swift in Sources */,
|
||||||
DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */,
|
DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */,
|
||||||
DBAE3F822615DDA3004B8251 /* ProfileViewController+UserProvider.swift in Sources */,
|
DBAE3F822615DDA3004B8251 /* ProfileViewController+UserProvider.swift in Sources */,
|
||||||
DB938EED2623F79B00E5B6C1 /* ThreadViewModel.swift in Sources */,
|
DB938EED2623F79B00E5B6C1 /* ThreadViewModel.swift in Sources */,
|
||||||
|
@ -2415,8 +2526,11 @@
|
||||||
DB35FC252612FD7A006193C9 /* ProfileFieldView.swift in Sources */,
|
DB35FC252612FD7A006193C9 /* ProfileFieldView.swift in Sources */,
|
||||||
DB938F0326240EA300E5B6C1 /* CachedThreadViewModel.swift in Sources */,
|
DB938F0326240EA300E5B6C1 /* CachedThreadViewModel.swift in Sources */,
|
||||||
2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.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 */,
|
||||||
DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */,
|
DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */,
|
||||||
DBAE3FA92617106E004B8251 /* MastodonMetricFormatter.swift in Sources */,
|
DBAE3FA92617106E004B8251 /* MastodonMetricFormatter.swift in Sources */,
|
||||||
|
2D35237A26256D920031AF25 /* NotificationSection.swift in Sources */,
|
||||||
DB084B5725CBC56C00F898ED /* Status.swift in Sources */,
|
DB084B5725CBC56C00F898ED /* Status.swift in Sources */,
|
||||||
DB447691260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift in Sources */,
|
DB447691260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift in Sources */,
|
||||||
DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */,
|
DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */,
|
||||||
|
@ -2429,14 +2543,16 @@
|
||||||
DB71FD3625F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift in Sources */,
|
DB71FD3625F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift in Sources */,
|
||||||
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */,
|
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */,
|
||||||
DB98338725C945ED00AD9700 /* Strings.swift in Sources */,
|
DB98338725C945ED00AD9700 /* Strings.swift in Sources */,
|
||||||
|
2D7867192625B77500211898 /* NotificationItem.swift in Sources */,
|
||||||
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */,
|
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */,
|
||||||
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */,
|
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */,
|
||||||
2DFF41892614A4DC00F776A4 /* UIView+Constraint.swift in Sources */,
|
|
||||||
2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */,
|
2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */,
|
||||||
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */,
|
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */,
|
||||||
DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */,
|
DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */,
|
||||||
2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */,
|
2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */,
|
||||||
|
5B90C48526259BF10002E742 /* APIService+Subscriptions.swift in Sources */,
|
||||||
0F20223926146553000C64BF /* Array+removeDuplicates.swift in Sources */,
|
0F20223926146553000C64BF /* Array+removeDuplicates.swift in Sources */,
|
||||||
|
5B90C460262599800002E742 /* SettingsAppearanceTableViewCell.swift in Sources */,
|
||||||
DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */,
|
DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */,
|
||||||
5DF1058525F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift in Sources */,
|
5DF1058525F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift in Sources */,
|
||||||
DB447697260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift in Sources */,
|
DB447697260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift in Sources */,
|
||||||
|
@ -2524,6 +2640,7 @@
|
||||||
2D927F1425C7EDD9004F19B8 /* Emoji.swift in Sources */,
|
2D927F1425C7EDD9004F19B8 /* Emoji.swift in Sources */,
|
||||||
2DF75BC725D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift in Sources */,
|
2DF75BC725D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift in Sources */,
|
||||||
DB89BA1225C1105C008580ED /* CoreDataStack.swift in Sources */,
|
DB89BA1225C1105C008580ED /* CoreDataStack.swift in Sources */,
|
||||||
|
5B90C46F26259B2C0002E742 /* Setting.swift in Sources */,
|
||||||
DB89BA1C25C1107F008580ED /* NSManagedObjectContext.swift in Sources */,
|
DB89BA1C25C1107F008580ED /* NSManagedObjectContext.swift in Sources */,
|
||||||
DB9D6C2E25E504AC0051B173 /* Attachment.swift in Sources */,
|
DB9D6C2E25E504AC0051B173 /* Attachment.swift in Sources */,
|
||||||
2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */,
|
2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */,
|
||||||
|
@ -2536,6 +2653,7 @@
|
||||||
2D152A9225C2980C009AA50C /* UIFont.swift in Sources */,
|
2D152A9225C2980C009AA50C /* UIFont.swift in Sources */,
|
||||||
DB4481B325EE16D000BEFB67 /* PollOption.swift in Sources */,
|
DB4481B325EE16D000BEFB67 /* PollOption.swift in Sources */,
|
||||||
DB89BA4425C1165F008580ED /* Managed.swift in Sources */,
|
DB89BA4425C1165F008580ED /* Managed.swift in Sources */,
|
||||||
|
2D6125472625436B00299647 /* Notification.swift in Sources */,
|
||||||
DB89BA4325C1165F008580ED /* NetworkUpdatable.swift in Sources */,
|
DB89BA4325C1165F008580ED /* NetworkUpdatable.swift in Sources */,
|
||||||
DB8AF56825C13E2A002E6C99 /* HomeTimelineIndex.swift in Sources */,
|
DB8AF56825C13E2A002E6C99 /* HomeTimelineIndex.swift in Sources */,
|
||||||
DB45FAED25CA7A9A005A8AC7 /* MastodonAuthentication.swift in Sources */,
|
DB45FAED25CA7A9A005A8AC7 /* MastodonAuthentication.swift in Sources */,
|
||||||
|
@ -2544,6 +2662,8 @@
|
||||||
DB89BA1D25C1107F008580ED /* URL.swift in Sources */,
|
DB89BA1D25C1107F008580ED /* URL.swift in Sources */,
|
||||||
2D0B7A1D261D839600B44727 /* SearchHistory.swift in Sources */,
|
2D0B7A1D261D839600B44727 /* SearchHistory.swift in Sources */,
|
||||||
2D927F0825C7E9A8004F19B8 /* Tag.swift in Sources */,
|
2D927F0825C7E9A8004F19B8 /* Tag.swift in Sources */,
|
||||||
|
5B90C46E26259B2C0002E742 /* Subscription.swift in Sources */,
|
||||||
|
5B90C47F26259BA90002E742 /* SubscriptionAlerts.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
|
@ -68,6 +68,7 @@ extension SceneCoordinator {
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
case publicTimeline
|
case publicTimeline
|
||||||
|
case settings
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
var isOnboarding: Bool {
|
var isOnboarding: Bool {
|
||||||
|
@ -269,6 +270,10 @@ private extension SceneCoordinator {
|
||||||
let _viewController = PublicTimelineViewController()
|
let _viewController = PublicTimelineViewController()
|
||||||
_viewController.viewModel = PublicTimelineViewModel(context: appContext)
|
_viewController.viewModel = PublicTimelineViewModel(context: appContext)
|
||||||
viewController = _viewController
|
viewController = _viewController
|
||||||
|
case .settings:
|
||||||
|
let _viewController = SettingsViewController()
|
||||||
|
_viewController.viewModel = SettingsViewModel(context: appContext, coordinator: self)
|
||||||
|
viewController = _viewController
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
//
|
||||||
|
// NotificationItem.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/4/13.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum NotificationItem {
|
||||||
|
case notification(objectID: NSManagedObjectID)
|
||||||
|
|
||||||
|
case bottomLoader
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationItem: Equatable {
|
||||||
|
static func == (lhs: NotificationItem, rhs: NotificationItem) -> Bool {
|
||||||
|
switch (lhs, rhs) {
|
||||||
|
case (.notification(let idLeft), .notification(let idRight)):
|
||||||
|
return idLeft == idRight
|
||||||
|
case (.bottomLoader, .bottomLoader):
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationItem: Hashable {
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
switch self {
|
||||||
|
case .notification(let id):
|
||||||
|
hasher.combine(id)
|
||||||
|
case .bottomLoader:
|
||||||
|
hasher.combine(String(describing: NotificationItem.bottomLoader.self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
//
|
||||||
|
// NotificationSection.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/4/13.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import Foundation
|
||||||
|
import MastodonSDK
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
enum NotificationSection: Equatable, Hashable {
|
||||||
|
case main
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationSection {
|
||||||
|
static func tableViewDiffableDataSource(
|
||||||
|
for tableView: UITableView,
|
||||||
|
timestampUpdatePublisher: AnyPublisher<Date, Never>,
|
||||||
|
managedObjectContext: NSManagedObjectContext,
|
||||||
|
delegate: NotificationTableViewCellDelegate,
|
||||||
|
dependency: NeedsDependency,
|
||||||
|
requestUserID: String
|
||||||
|
) -> UITableViewDiffableDataSource<NotificationSection, NotificationItem> {
|
||||||
|
UITableViewDiffableDataSource(tableView: tableView) {
|
||||||
|
[weak delegate, weak dependency]
|
||||||
|
(tableView, indexPath, notificationItem) -> UITableViewCell? in
|
||||||
|
guard let dependency = dependency else { return nil }
|
||||||
|
switch notificationItem {
|
||||||
|
case .notification(let objectID):
|
||||||
|
|
||||||
|
let notification = managedObjectContext.object(with: objectID) as! MastodonNotification
|
||||||
|
guard let type = Mastodon.Entity.Notification.NotificationType(rawValue: notification.typeRaw) else {
|
||||||
|
assertionFailure()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let timeText = notification.createAt.shortTimeAgoSinceNow
|
||||||
|
|
||||||
|
let actionText = type.actionText
|
||||||
|
let actionImageName = type.actionImageName
|
||||||
|
let color = type.color
|
||||||
|
|
||||||
|
if let status = notification.status {
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationStatusTableViewCell.self), for: indexPath) as! NotificationStatusTableViewCell
|
||||||
|
cell.delegate = delegate
|
||||||
|
let frame = CGRect(x: 0, y: 0, width: tableView.readableContentGuide.layoutFrame.width - NotificationStatusTableViewCell.statusPadding.left - NotificationStatusTableViewCell.statusPadding.right, height: tableView.readableContentGuide.layoutFrame.height)
|
||||||
|
StatusSection.configure(cell: cell,
|
||||||
|
dependency: dependency,
|
||||||
|
readableLayoutFrame: frame,
|
||||||
|
timestampUpdatePublisher: timestampUpdatePublisher,
|
||||||
|
status: status,
|
||||||
|
requestUserID: requestUserID,
|
||||||
|
statusItemAttribute: Item.StatusAttribute(isStatusTextSensitive: false, isStatusSensitive: false))
|
||||||
|
timestampUpdatePublisher
|
||||||
|
.sink { _ in
|
||||||
|
let timeText = notification.createAt.shortTimeAgoSinceNow
|
||||||
|
cell.actionLabel.text = actionText + " · " + timeText
|
||||||
|
}
|
||||||
|
.store(in: &cell.disposeBag)
|
||||||
|
cell.actionImageBackground.backgroundColor = color
|
||||||
|
cell.actionLabel.text = actionText + " · " + timeText
|
||||||
|
cell.nameLabel.text = notification.account.displayName.isEmpty ? notification.account.username : notification.account.displayName
|
||||||
|
if let url = notification.account.avatarImageURL() {
|
||||||
|
cell.avatatImageView.af.setImage(
|
||||||
|
withURL: url,
|
||||||
|
placeholderImage: UIImage.placeholder(color: .systemFill),
|
||||||
|
imageTransition: .crossDissolve(0.2)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
cell.avatatImageView.gesture().sink { [weak cell] _ in
|
||||||
|
cell?.delegate?.userAvatarDidPressed(notification: notification)
|
||||||
|
}
|
||||||
|
.store(in: &cell.disposeBag)
|
||||||
|
if let actionImage = UIImage(systemName: actionImageName, withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .semibold))?.withRenderingMode(.alwaysTemplate) {
|
||||||
|
cell.actionImageView.image = actionImage
|
||||||
|
}
|
||||||
|
return cell
|
||||||
|
|
||||||
|
} else {
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationTableViewCell.self), for: indexPath) as! NotificationTableViewCell
|
||||||
|
cell.delegate = delegate
|
||||||
|
timestampUpdatePublisher
|
||||||
|
.sink { _ in
|
||||||
|
let timeText = notification.createAt.shortTimeAgoSinceNow
|
||||||
|
cell.actionLabel.text = actionText + " · " + timeText
|
||||||
|
}
|
||||||
|
.store(in: &cell.disposeBag)
|
||||||
|
cell.actionImageBackground.backgroundColor = color
|
||||||
|
cell.actionLabel.text = actionText + " · " + timeText
|
||||||
|
cell.nameLabel.text = notification.account.displayName.isEmpty ? notification.account.username : notification.account.displayName
|
||||||
|
if let url = notification.account.avatarImageURL() {
|
||||||
|
cell.avatatImageView.af.setImage(
|
||||||
|
withURL: url,
|
||||||
|
placeholderImage: UIImage.placeholder(color: .systemFill),
|
||||||
|
imageTransition: .crossDissolve(0.2)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
cell.avatatImageView.gesture().sink { [weak cell] _ in
|
||||||
|
cell?.delegate?.userAvatarDidPressed(notification: notification)
|
||||||
|
}
|
||||||
|
.store(in: &cell.disposeBag)
|
||||||
|
if let actionImage = UIImage(systemName: actionImageName, withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .semibold))?.withRenderingMode(.alwaysTemplate) {
|
||||||
|
cell.actionImageView.image = actionImage
|
||||||
|
}
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
case .bottomLoader:
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) as! TimelineBottomLoaderTableViewCell
|
||||||
|
cell.startAnimating()
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@ extension SearchResultSection {
|
||||||
cell.config(with: user)
|
cell.config(with: user)
|
||||||
return cell
|
return cell
|
||||||
case .bottomLoader:
|
case .bottomLoader:
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchBottomLoader.self)) as! SearchBottomLoader
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) as! TimelineBottomLoaderTableViewCell
|
||||||
cell.startAnimating()
|
cell.startAnimating()
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,12 @@ import CoreData
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
import os.log
|
import os.log
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import AVKit
|
||||||
|
|
||||||
|
protocol StatusCell : DisposeBagCollectable {
|
||||||
|
var statusView: StatusView { get }
|
||||||
|
var pollCountdownSubscription: AnyCancellable? { get set }
|
||||||
|
}
|
||||||
|
|
||||||
enum StatusSection: Equatable, Hashable {
|
enum StatusSection: Equatable, Hashable {
|
||||||
case main
|
case main
|
||||||
|
@ -127,7 +133,7 @@ extension StatusSection {
|
||||||
extension StatusSection {
|
extension StatusSection {
|
||||||
|
|
||||||
static func configure(
|
static func configure(
|
||||||
cell: StatusTableViewCell,
|
cell: StatusCell,
|
||||||
dependency: NeedsDependency,
|
dependency: NeedsDependency,
|
||||||
readableLayoutFrame: CGRect?,
|
readableLayoutFrame: CGRect?,
|
||||||
timestampUpdatePublisher: AnyPublisher<Date, Never>,
|
timestampUpdatePublisher: AnyPublisher<Date, Never>,
|
||||||
|
@ -283,14 +289,27 @@ extension StatusSection {
|
||||||
if let videoAttachment = mediaAttachments.filter({ $0.type == .gifv || $0.type == .video }).first,
|
if let videoAttachment = mediaAttachments.filter({ $0.type == .gifv || $0.type == .video }).first,
|
||||||
let videoPlayerViewModel = dependency.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: videoAttachment)
|
let videoPlayerViewModel = dependency.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: videoAttachment)
|
||||||
{
|
{
|
||||||
let parent = cell.delegate?.parent()
|
var parent: UIViewController?
|
||||||
|
var playerViewControllerDelegate: AVPlayerViewControllerDelegate? = nil
|
||||||
|
switch cell {
|
||||||
|
case is StatusTableViewCell:
|
||||||
|
let statusTableViewCell = cell as! StatusTableViewCell
|
||||||
|
parent = statusTableViewCell.delegate?.parent()
|
||||||
|
playerViewControllerDelegate = statusTableViewCell.delegate?.playerViewControllerDelegate
|
||||||
|
case is NotificationStatusTableViewCell:
|
||||||
|
let notificationTableViewCell = cell as! NotificationStatusTableViewCell
|
||||||
|
parent = notificationTableViewCell.delegate?.parent()
|
||||||
|
default:
|
||||||
|
parent = nil
|
||||||
|
assertionFailure("unknown cell")
|
||||||
|
}
|
||||||
let playerContainerView = cell.statusView.playerContainerView
|
let playerContainerView = cell.statusView.playerContainerView
|
||||||
let playerViewController = playerContainerView.setupPlayer(
|
let playerViewController = playerContainerView.setupPlayer(
|
||||||
aspectRatio: videoPlayerViewModel.videoSize,
|
aspectRatio: videoPlayerViewModel.videoSize,
|
||||||
maxSize: playerViewMaxSize,
|
maxSize: playerViewMaxSize,
|
||||||
parent: parent
|
parent: parent
|
||||||
)
|
)
|
||||||
playerViewController.delegate = cell.delegate?.playerViewControllerDelegate
|
playerViewController.delegate = playerViewControllerDelegate
|
||||||
playerViewController.player = videoPlayerViewModel.player
|
playerViewController.player = videoPlayerViewModel.player
|
||||||
playerViewController.showsPlaybackControls = videoPlayerViewModel.videoKind != .gif
|
playerViewController.showsPlaybackControls = videoPlayerViewModel.videoKind != .gif
|
||||||
playerContainerView.setMediaKind(kind: videoPlayerViewModel.videoKind)
|
playerContainerView.setMediaKind(kind: videoPlayerViewModel.videoKind)
|
||||||
|
@ -376,7 +395,9 @@ extension StatusSection {
|
||||||
StatusSection.configureActionToolBar(cell: cell, status: status, requestUserID: requestUserID)
|
StatusSection.configureActionToolBar(cell: cell, status: status, requestUserID: requestUserID)
|
||||||
|
|
||||||
// separator line
|
// separator line
|
||||||
cell.separatorLine.isHidden = statusItemAttribute.isSeparatorLineHidden
|
if let statusTableViewCell = cell as? StatusTableViewCell {
|
||||||
|
statusTableViewCell.separatorLine.isHidden = statusItemAttribute.isSeparatorLineHidden
|
||||||
|
}
|
||||||
|
|
||||||
// set date
|
// set date
|
||||||
let createdAt = (status.reblog ?? status).createdAt
|
let createdAt = (status.reblog ?? status).createdAt
|
||||||
|
@ -520,7 +541,7 @@ extension StatusSection {
|
||||||
|
|
||||||
|
|
||||||
static func configureHeader(
|
static func configureHeader(
|
||||||
cell: StatusTableViewCell,
|
cell: StatusCell,
|
||||||
status: Status
|
status: Status
|
||||||
) {
|
) {
|
||||||
if status.reblog != nil {
|
if status.reblog != nil {
|
||||||
|
@ -548,7 +569,7 @@ extension StatusSection {
|
||||||
}
|
}
|
||||||
|
|
||||||
static func configureActionToolBar(
|
static func configureActionToolBar(
|
||||||
cell: StatusTableViewCell,
|
cell: StatusCell,
|
||||||
status: Status,
|
status: Status,
|
||||||
requestUserID: String
|
requestUserID: String
|
||||||
) {
|
) {
|
||||||
|
@ -579,7 +600,7 @@ extension StatusSection {
|
||||||
}
|
}
|
||||||
|
|
||||||
static func configurePoll(
|
static func configurePoll(
|
||||||
cell: StatusTableViewCell,
|
cell: StatusCell,
|
||||||
poll: Poll?,
|
poll: Poll?,
|
||||||
requestUserID: String,
|
requestUserID: String,
|
||||||
updateProgressAnimated: Bool,
|
updateProgressAnimated: Bool,
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
//
|
||||||
|
// Mastodon+Entity+Notification+Type.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/4/19.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import MastodonSDK
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension Mastodon.Entity.Notification.NotificationType {
|
||||||
|
public var color: UIColor {
|
||||||
|
get {
|
||||||
|
var color: UIColor
|
||||||
|
switch self {
|
||||||
|
case .follow:
|
||||||
|
color = Asset.Colors.brandBlue.color
|
||||||
|
case .favourite:
|
||||||
|
color = Asset.Colors.Notification.favourite.color
|
||||||
|
case .reblog:
|
||||||
|
color = Asset.Colors.Notification.reblog.color
|
||||||
|
case .mention:
|
||||||
|
color = Asset.Colors.Notification.mention.color
|
||||||
|
case .poll:
|
||||||
|
color = Asset.Colors.brandBlue.color
|
||||||
|
default:
|
||||||
|
color = .clear
|
||||||
|
}
|
||||||
|
return color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var actionText: String {
|
||||||
|
get {
|
||||||
|
var actionText: String
|
||||||
|
switch self {
|
||||||
|
case .follow:
|
||||||
|
actionText = L10n.Scene.Notification.Action.follow
|
||||||
|
case .favourite:
|
||||||
|
actionText = L10n.Scene.Notification.Action.favourite
|
||||||
|
case .reblog:
|
||||||
|
actionText = L10n.Scene.Notification.Action.reblog
|
||||||
|
case .mention:
|
||||||
|
actionText = L10n.Scene.Notification.Action.mention
|
||||||
|
case .poll:
|
||||||
|
actionText = L10n.Scene.Notification.Action.poll
|
||||||
|
default:
|
||||||
|
actionText = ""
|
||||||
|
}
|
||||||
|
return actionText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var actionImageName: String {
|
||||||
|
get {
|
||||||
|
var actionImageName: String
|
||||||
|
switch self {
|
||||||
|
case .follow:
|
||||||
|
actionImageName = "person.crop.circle.badge.checkmark"
|
||||||
|
case .favourite:
|
||||||
|
actionImageName = "star.fill"
|
||||||
|
case .reblog:
|
||||||
|
actionImageName = "arrow.2.squarepath"
|
||||||
|
case .mention:
|
||||||
|
actionImageName = "at"
|
||||||
|
case .poll:
|
||||||
|
actionImageName = "list.bullet"
|
||||||
|
default:
|
||||||
|
actionImageName = ""
|
||||||
|
}
|
||||||
|
return actionImageName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -43,3 +43,11 @@ extension UIButton {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension UIButton {
|
||||||
|
func setBackgroundColor(_ color: UIColor, for state: UIControl.State) {
|
||||||
|
self.setBackgroundImage(
|
||||||
|
UIImage.placeholder(color: color),
|
||||||
|
for: state
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,261 +0,0 @@
|
||||||
//
|
|
||||||
// UIView+Constraint.swift
|
|
||||||
// Mastodon
|
|
||||||
//
|
|
||||||
// Created by sxiaojian on 2021/3/31.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
enum Dimension {
|
|
||||||
case width
|
|
||||||
case height
|
|
||||||
|
|
||||||
var layoutAttribute: NSLayoutConstraint.Attribute {
|
|
||||||
switch self {
|
|
||||||
case .width:
|
|
||||||
return .width
|
|
||||||
case .height:
|
|
||||||
return .height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension UIView {
|
|
||||||
|
|
||||||
func constrain(toSuperviewEdges: UIEdgeInsets?) {
|
|
||||||
guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return}
|
|
||||||
translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
NSLayoutConstraint(item: self,
|
|
||||||
attribute: .leading,
|
|
||||||
relatedBy: .equal,
|
|
||||||
toItem: view,
|
|
||||||
attribute: .leading,
|
|
||||||
multiplier: 1.0,
|
|
||||||
constant: toSuperviewEdges?.left ?? 0.0),
|
|
||||||
NSLayoutConstraint(item: self,
|
|
||||||
attribute: .top,
|
|
||||||
relatedBy: .equal,
|
|
||||||
toItem: view,
|
|
||||||
attribute: .top,
|
|
||||||
multiplier: 1.0,
|
|
||||||
constant: toSuperviewEdges?.top ?? 0.0),
|
|
||||||
NSLayoutConstraint(item: view,
|
|
||||||
attribute: .trailing,
|
|
||||||
relatedBy: .equal,
|
|
||||||
toItem: self,
|
|
||||||
attribute: .trailing,
|
|
||||||
multiplier: 1.0,
|
|
||||||
constant: toSuperviewEdges?.right ?? 0.0),
|
|
||||||
NSLayoutConstraint(item: view,
|
|
||||||
attribute: .bottom,
|
|
||||||
relatedBy: .equal,
|
|
||||||
toItem: self,
|
|
||||||
attribute: .bottom,
|
|
||||||
multiplier: 1.0,
|
|
||||||
constant: toSuperviewEdges?.bottom ?? 0.0)
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
func constrain(_ constraints: [NSLayoutConstraint?]) {
|
|
||||||
guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return }
|
|
||||||
translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
NSLayoutConstraint.activate(constraints.compactMap { $0 })
|
|
||||||
}
|
|
||||||
|
|
||||||
func constraint(_ attribute: NSLayoutConstraint.Attribute, toView: UIView, constant: CGFloat?) -> NSLayoutConstraint? {
|
|
||||||
guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return nil}
|
|
||||||
translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
return NSLayoutConstraint(item: self, attribute: attribute, relatedBy: .equal, toItem: toView, attribute: attribute, multiplier: 1.0, constant: constant ?? 0.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func constraint(_ attribute: NSLayoutConstraint.Attribute, toView: UIView) -> NSLayoutConstraint? {
|
|
||||||
guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return nil}
|
|
||||||
translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
return NSLayoutConstraint(item: self, attribute: attribute, relatedBy: .equal, toItem: toView, attribute: attribute, multiplier: 1.0, constant: 0.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func constraint(_ dimension: Dimension, constant: CGFloat) -> NSLayoutConstraint? {
|
|
||||||
guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return nil }
|
|
||||||
translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
return NSLayoutConstraint(item: self,
|
|
||||||
attribute: dimension.layoutAttribute,
|
|
||||||
relatedBy: .equal,
|
|
||||||
toItem: nil,
|
|
||||||
attribute: .notAnAttribute,
|
|
||||||
multiplier: 1.0,
|
|
||||||
constant: constant)
|
|
||||||
}
|
|
||||||
|
|
||||||
func constrainTopCorners(sidePadding: CGFloat, topPadding: CGFloat, topLayoutGuide: UILayoutSupport) {
|
|
||||||
guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return }
|
|
||||||
translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
constrain([
|
|
||||||
constraint(.leading, toView: view, constant: sidePadding),
|
|
||||||
NSLayoutConstraint(item: self, attribute: .top, relatedBy: .equal, toItem: topLayoutGuide, attribute: .bottom, multiplier: 1.0, constant: topPadding),
|
|
||||||
constraint(.trailing, toView: view, constant: -sidePadding)
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
func constrainTopCorners(sidePadding: CGFloat, topPadding: CGFloat) {
|
|
||||||
guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return }
|
|
||||||
translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
constrain([
|
|
||||||
constraint(.leading, toView: view, constant: sidePadding),
|
|
||||||
constraint(.top, toView: view, constant: topPadding),
|
|
||||||
constraint(.trailing, toView: view, constant: -sidePadding)
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
func constrainTopCorners(height: CGFloat) {
|
|
||||||
guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return }
|
|
||||||
translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
constrain([
|
|
||||||
constraint(.leading, toView: view),
|
|
||||||
constraint(.top, toView: view),
|
|
||||||
constraint(.trailing, toView: view),
|
|
||||||
constraint(.height, constant: height)
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
func constrainBottomCorners(sidePadding: CGFloat, bottomPadding: CGFloat) {
|
|
||||||
guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return }
|
|
||||||
translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
constrain([
|
|
||||||
constraint(.leading, toView: view, constant: sidePadding),
|
|
||||||
constraint(.bottom, toView: view, constant: -bottomPadding),
|
|
||||||
constraint(.trailing, toView: view, constant: -sidePadding)
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
func constrainBottomCorners(height: CGFloat) {
|
|
||||||
guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return }
|
|
||||||
translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
constrain([
|
|
||||||
constraint(.leading, toView: view),
|
|
||||||
constraint(.bottom, toView: view),
|
|
||||||
constraint(.trailing, toView: view),
|
|
||||||
constraint(.height, constant: height)
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
func constrainLeadingCorners() {
|
|
||||||
guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return }
|
|
||||||
translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
constrain([
|
|
||||||
constraint(.top, toView: view),
|
|
||||||
constraint(.leading, toView: view),
|
|
||||||
constraint(.bottom, toView: view)
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
func constrainTrailingCorners() {
|
|
||||||
guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return }
|
|
||||||
translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
constrain([
|
|
||||||
constraint(.top, toView: view),
|
|
||||||
constraint(.trailing, toView: view),
|
|
||||||
constraint(.bottom, toView: view)
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
func constrainToCenter() {
|
|
||||||
guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return }
|
|
||||||
translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
constrain([
|
|
||||||
constraint(.centerX, toView: view),
|
|
||||||
constraint(.centerY, toView: view)
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
func pin(toSize: CGSize) {
|
|
||||||
guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return }
|
|
||||||
translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
constrain([
|
|
||||||
widthAnchor.constraint(equalToConstant: toSize.width),
|
|
||||||
heightAnchor.constraint(equalToConstant: toSize.height)])
|
|
||||||
}
|
|
||||||
|
|
||||||
func pin(top: CGFloat?,left: CGFloat?,bottom: CGFloat?, right: CGFloat?) {
|
|
||||||
guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return }
|
|
||||||
translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
var constraints = [NSLayoutConstraint]()
|
|
||||||
if let topConstant = top {
|
|
||||||
constraints.append(topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant))
|
|
||||||
}
|
|
||||||
if let leftConstant = left {
|
|
||||||
constraints.append(leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: leftConstant))
|
|
||||||
}
|
|
||||||
if let bottomConstant = bottom {
|
|
||||||
constraints.append(view.bottomAnchor.constraint(equalTo: bottomAnchor, constant: bottomConstant))
|
|
||||||
}
|
|
||||||
if let rightConstant = right {
|
|
||||||
constraints.append(view.trailingAnchor.constraint(equalTo: trailingAnchor, constant: rightConstant))
|
|
||||||
}
|
|
||||||
constrain(constraints)
|
|
||||||
|
|
||||||
}
|
|
||||||
func pinTopLeft(padding: CGFloat) {
|
|
||||||
guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return }
|
|
||||||
translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
constrain([
|
|
||||||
leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: padding),
|
|
||||||
topAnchor.constraint(equalTo: view.topAnchor, constant: padding)])
|
|
||||||
}
|
|
||||||
|
|
||||||
func pinTopLeft(top: CGFloat, left: CGFloat) {
|
|
||||||
guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return }
|
|
||||||
translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
constrain([
|
|
||||||
leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: left),
|
|
||||||
topAnchor.constraint(equalTo: view.topAnchor, constant: top)])
|
|
||||||
}
|
|
||||||
|
|
||||||
func pinTopRight(padding: CGFloat) {
|
|
||||||
guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return }
|
|
||||||
translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
constrain([
|
|
||||||
view.trailingAnchor.constraint(equalTo: trailingAnchor, constant: padding),
|
|
||||||
topAnchor.constraint(equalTo: view.topAnchor, constant: padding)])
|
|
||||||
}
|
|
||||||
|
|
||||||
func pinTopRight(top: CGFloat, right: CGFloat) {
|
|
||||||
guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return }
|
|
||||||
translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
constrain([
|
|
||||||
view.trailingAnchor.constraint(equalTo: trailingAnchor, constant: right),
|
|
||||||
topAnchor.constraint(equalTo: view.topAnchor, constant: top)])
|
|
||||||
}
|
|
||||||
|
|
||||||
func pinTopLeft(toView: UIView, topPadding: CGFloat) {
|
|
||||||
guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return }
|
|
||||||
translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
constrain([
|
|
||||||
leadingAnchor.constraint(equalTo: toView.leadingAnchor),
|
|
||||||
topAnchor.constraint(equalTo: toView.bottomAnchor, constant: topPadding)])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Cross-fades between two views by animating their alpha then setting one or the other hidden.
|
|
||||||
/// - parameters:
|
|
||||||
/// - lhs: left view
|
|
||||||
/// - rhs: right view
|
|
||||||
/// - toRight: fade to the right view if true, fade to the left view if false
|
|
||||||
/// - duration: animation duration
|
|
||||||
///
|
|
||||||
static func crossfade(_ lhs: UIView, _ rhs: UIView, toRight: Bool, duration: TimeInterval) {
|
|
||||||
lhs.alpha = toRight ? 1.0 : 0.0
|
|
||||||
rhs.alpha = toRight ? 0.0 : 1.0
|
|
||||||
lhs.isHidden = false
|
|
||||||
rhs.isHidden = false
|
|
||||||
|
|
||||||
UIView.animate(withDuration: duration, animations: {
|
|
||||||
lhs.alpha = toRight ? 0.0 : 1.0
|
|
||||||
rhs.alpha = toRight ? 1.0 : 0.0
|
|
||||||
}, completion: { _ in
|
|
||||||
lhs.isHidden = toRight
|
|
||||||
rhs.isHidden = !toRight
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
//
|
||||||
|
// UIView+Gesture.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/4/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
struct GesturePublisher: Publisher {
|
||||||
|
typealias Output = GestureType
|
||||||
|
typealias Failure = Never
|
||||||
|
private let view: UIView
|
||||||
|
private let gestureType: GestureType
|
||||||
|
init(view: UIView, gestureType: GestureType) {
|
||||||
|
self.view = view
|
||||||
|
self.gestureType = gestureType
|
||||||
|
}
|
||||||
|
|
||||||
|
func receive<S>(subscriber: S) where S: Subscriber,
|
||||||
|
GesturePublisher.Failure == S.Failure, GesturePublisher.Output
|
||||||
|
== S.Input
|
||||||
|
{
|
||||||
|
let subscription = GestureSubscription(
|
||||||
|
subscriber: subscriber,
|
||||||
|
view: view,
|
||||||
|
gestureType: gestureType
|
||||||
|
)
|
||||||
|
subscriber.receive(subscription: subscription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum GestureType {
|
||||||
|
case tap(UITapGestureRecognizer = .init())
|
||||||
|
case swipe(UISwipeGestureRecognizer = .init())
|
||||||
|
case longPress(UILongPressGestureRecognizer = .init())
|
||||||
|
case pan(UIPanGestureRecognizer = .init())
|
||||||
|
case pinch(UIPinchGestureRecognizer = .init())
|
||||||
|
case edge(UIScreenEdgePanGestureRecognizer = .init())
|
||||||
|
func get() -> UIGestureRecognizer {
|
||||||
|
switch self {
|
||||||
|
case let .tap(tapGesture):
|
||||||
|
return tapGesture
|
||||||
|
case let .swipe(swipeGesture):
|
||||||
|
return swipeGesture
|
||||||
|
case let .longPress(longPressGesture):
|
||||||
|
return longPressGesture
|
||||||
|
case let .pan(panGesture):
|
||||||
|
return panGesture
|
||||||
|
case let .pinch(pinchGesture):
|
||||||
|
return pinchGesture
|
||||||
|
case let .edge(edgePanGesture):
|
||||||
|
return edgePanGesture
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GestureSubscription<S: Subscriber>: Subscription where S.Input == GestureType, S.Failure == Never {
|
||||||
|
private var subscriber: S?
|
||||||
|
private var gestureType: GestureType
|
||||||
|
private var view: UIView
|
||||||
|
init(subscriber: S, view: UIView, gestureType: GestureType) {
|
||||||
|
self.subscriber = subscriber
|
||||||
|
self.view = view
|
||||||
|
self.gestureType = gestureType
|
||||||
|
configureGesture(gestureType)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureGesture(_ gestureType: GestureType) {
|
||||||
|
let gesture = gestureType.get()
|
||||||
|
gesture.addTarget(self, action: #selector(handler))
|
||||||
|
view.addGestureRecognizer(gesture)
|
||||||
|
}
|
||||||
|
|
||||||
|
func request(_ demand: Subscribers.Demand) {}
|
||||||
|
func cancel() {
|
||||||
|
subscriber = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
private func handler() {
|
||||||
|
_ = subscriber?.receive(gestureType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UIView {
|
||||||
|
func gesture(_ gestureType: GestureType = .tap()) -> GesturePublisher {
|
||||||
|
isUserInteractionEnabled = true
|
||||||
|
return GesturePublisher(view: self, gestureType: gestureType)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
//
|
||||||
|
// UIView+Remove.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by xiaojian sun on 2021/4/16.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UIView {
|
||||||
|
func removeFromStackView() {
|
||||||
|
if let stackView = self.superview as? UIStackView {
|
||||||
|
stackView.removeArrangedSubview(self)
|
||||||
|
}
|
||||||
|
self.removeFromSuperview()
|
||||||
|
}
|
||||||
|
}
|
|
@ -52,6 +52,7 @@ internal enum Asset {
|
||||||
internal static let tertiarySystemGroupedBackground = ColorAsset(name: "Colors/Background/tertiary.system.grouped.background")
|
internal static let tertiarySystemGroupedBackground = ColorAsset(name: "Colors/Background/tertiary.system.grouped.background")
|
||||||
}
|
}
|
||||||
internal enum Border {
|
internal enum Border {
|
||||||
|
internal static let notification = ColorAsset(name: "Colors/Border/notification")
|
||||||
internal static let searchCard = ColorAsset(name: "Colors/Border/searchCard")
|
internal static let searchCard = ColorAsset(name: "Colors/Border/searchCard")
|
||||||
}
|
}
|
||||||
internal enum Button {
|
internal enum Button {
|
||||||
|
@ -69,6 +70,11 @@ internal enum Asset {
|
||||||
internal static let primary = ColorAsset(name: "Colors/Label/primary")
|
internal static let primary = ColorAsset(name: "Colors/Label/primary")
|
||||||
internal static let secondary = ColorAsset(name: "Colors/Label/secondary")
|
internal static let secondary = ColorAsset(name: "Colors/Label/secondary")
|
||||||
}
|
}
|
||||||
|
internal enum Notification {
|
||||||
|
internal static let favourite = ColorAsset(name: "Colors/Notification/favourite")
|
||||||
|
internal static let mention = ColorAsset(name: "Colors/Notification/mention")
|
||||||
|
internal static let reblog = ColorAsset(name: "Colors/Notification/reblog")
|
||||||
|
}
|
||||||
internal enum Shadow {
|
internal enum Shadow {
|
||||||
internal static let searchCard = ColorAsset(name: "Colors/Shadow/SearchCard")
|
internal static let searchCard = ColorAsset(name: "Colors/Shadow/SearchCard")
|
||||||
}
|
}
|
||||||
|
@ -80,6 +86,7 @@ internal enum Asset {
|
||||||
internal static let invalid = ColorAsset(name: "Colors/TextField/invalid")
|
internal static let invalid = ColorAsset(name: "Colors/TextField/invalid")
|
||||||
internal static let valid = ColorAsset(name: "Colors/TextField/valid")
|
internal static let valid = ColorAsset(name: "Colors/TextField/valid")
|
||||||
}
|
}
|
||||||
|
internal static let battleshipGrey = ColorAsset(name: "Colors/battleshipGrey")
|
||||||
internal static let brandBlue = ColorAsset(name: "Colors/brand.blue")
|
internal static let brandBlue = ColorAsset(name: "Colors/brand.blue")
|
||||||
internal static let danger = ColorAsset(name: "Colors/danger")
|
internal static let danger = ColorAsset(name: "Colors/danger")
|
||||||
internal static let disabled = ColorAsset(name: "Colors/disabled")
|
internal static let disabled = ColorAsset(name: "Colors/disabled")
|
||||||
|
@ -120,6 +127,11 @@ internal enum Asset {
|
||||||
internal static let mastodonLogoLarge = ImageAsset(name: "Scene/Welcome/mastodon.logo.large")
|
internal static let mastodonLogoLarge = ImageAsset(name: "Scene/Welcome/mastodon.logo.large")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
internal enum Settings {
|
||||||
|
internal static let appearanceAutomatic = ImageAsset(name: "Settings/appearance.automatic")
|
||||||
|
internal static let appearanceDark = ImageAsset(name: "Settings/appearance.dark")
|
||||||
|
internal static let appearanceLight = ImageAsset(name: "Settings/appearance.light")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// swiftlint:enable identifier_name line_length nesting type_body_length type_name
|
// swiftlint:enable identifier_name line_length nesting type_body_length type_name
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,14 @@ internal enum L10n {
|
||||||
/// Server Error
|
/// Server Error
|
||||||
internal static let title = L10n.tr("Localizable", "Common.Alerts.ServerError.Title")
|
internal static let title = L10n.tr("Localizable", "Common.Alerts.ServerError.Title")
|
||||||
}
|
}
|
||||||
|
internal enum SignOut {
|
||||||
|
/// Sign Out
|
||||||
|
internal static let confirm = L10n.tr("Localizable", "Common.Alerts.SignOut.Confirm")
|
||||||
|
/// Are you sure you want to sign out?
|
||||||
|
internal static let message = L10n.tr("Localizable", "Common.Alerts.SignOut.Message")
|
||||||
|
/// Sign out
|
||||||
|
internal static let title = L10n.tr("Localizable", "Common.Alerts.SignOut.Title")
|
||||||
|
}
|
||||||
internal enum SignUpFailure {
|
internal enum SignUpFailure {
|
||||||
/// Sign Up Failure
|
/// Sign Up Failure
|
||||||
internal static let title = L10n.tr("Localizable", "Common.Alerts.SignUpFailure.Title")
|
internal static let title = L10n.tr("Localizable", "Common.Alerts.SignUpFailure.Title")
|
||||||
|
@ -353,6 +361,26 @@ internal enum L10n {
|
||||||
internal static let publishing = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Publishing")
|
internal static let publishing = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Publishing")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
internal enum Notification {
|
||||||
|
internal enum Action {
|
||||||
|
/// favorited your post
|
||||||
|
internal static let favourite = L10n.tr("Localizable", "Scene.Notification.Action.Favourite")
|
||||||
|
/// followed you
|
||||||
|
internal static let follow = L10n.tr("Localizable", "Scene.Notification.Action.Follow")
|
||||||
|
/// mentioned you
|
||||||
|
internal static let mention = L10n.tr("Localizable", "Scene.Notification.Action.Mention")
|
||||||
|
/// Your poll has ended
|
||||||
|
internal static let poll = L10n.tr("Localizable", "Scene.Notification.Action.Poll")
|
||||||
|
/// rebloged your post
|
||||||
|
internal static let reblog = L10n.tr("Localizable", "Scene.Notification.Action.Reblog")
|
||||||
|
}
|
||||||
|
internal enum Title {
|
||||||
|
/// Everything
|
||||||
|
internal static let everything = L10n.tr("Localizable", "Scene.Notification.Title.Everything")
|
||||||
|
/// Mentions
|
||||||
|
internal static let mentions = L10n.tr("Localizable", "Scene.Notification.Title.Mentions")
|
||||||
|
}
|
||||||
|
}
|
||||||
internal enum Profile {
|
internal enum Profile {
|
||||||
/// %@ posts
|
/// %@ posts
|
||||||
internal static func subtitle(_ p1: Any) -> String {
|
internal static func subtitle(_ p1: Any) -> String {
|
||||||
|
@ -595,6 +623,62 @@ internal enum L10n {
|
||||||
internal static let confirm = L10n.tr("Localizable", "Scene.ServerRules.Button.Confirm")
|
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 {
|
||||||
|
/// Reblogs 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 Thread {
|
internal enum Thread {
|
||||||
/// Post
|
/// Post
|
||||||
internal static let backTitle = L10n.tr("Localizable", "Scene.Thread.BackTitle")
|
internal static let backTitle = L10n.tr("Localizable", "Scene.Thread.BackTitle")
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xE8",
|
||||||
|
"green" : "0xE1",
|
||||||
|
"red" : "0xD9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "60",
|
||||||
|
"green" : "58",
|
||||||
|
"red" : "58"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,9 +5,9 @@
|
||||||
"color-space" : "srgb",
|
"color-space" : "srgb",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "0.851",
|
"blue" : "217",
|
||||||
"green" : "0.565",
|
"green" : "144",
|
||||||
"red" : "0.169"
|
"red" : "43"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"provides-namespace" : true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0",
|
||||||
|
"green" : "204",
|
||||||
|
"red" : "255"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "222",
|
||||||
|
"green" : "82",
|
||||||
|
"red" : "175"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "242",
|
||||||
|
"green" : "90",
|
||||||
|
"red" : "191"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "89",
|
||||||
|
"green" : "199",
|
||||||
|
"red" : "52"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "75",
|
||||||
|
"green" : "215",
|
||||||
|
"red" : "20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
@ -6,6 +6,9 @@
|
||||||
Please check your internet connection.";
|
Please check your internet connection.";
|
||||||
"Common.Alerts.PublishPostFailure.Title" = "Publish Failure";
|
"Common.Alerts.PublishPostFailure.Title" = "Publish Failure";
|
||||||
"Common.Alerts.ServerError.Title" = "Server Error";
|
"Common.Alerts.ServerError.Title" = "Server Error";
|
||||||
|
"Common.Alerts.SignOut.Confirm" = "Sign Out";
|
||||||
|
"Common.Alerts.SignOut.Message" = "Are you sure you want to sign out?";
|
||||||
|
"Common.Alerts.SignOut.Title" = "Sign out";
|
||||||
"Common.Alerts.SignUpFailure.Title" = "Sign Up Failure";
|
"Common.Alerts.SignUpFailure.Title" = "Sign Up Failure";
|
||||||
"Common.Alerts.VoteFailure.PollExpired" = "The poll has expired";
|
"Common.Alerts.VoteFailure.PollExpired" = "The poll has expired";
|
||||||
"Common.Alerts.VoteFailure.Title" = "Vote Failure";
|
"Common.Alerts.VoteFailure.Title" = "Vote Failure";
|
||||||
|
@ -118,6 +121,13 @@ tap the link to confirm your account.";
|
||||||
"Scene.HomeTimeline.NavigationBarState.Published" = "Published!";
|
"Scene.HomeTimeline.NavigationBarState.Published" = "Published!";
|
||||||
"Scene.HomeTimeline.NavigationBarState.Publishing" = "Publishing post...";
|
"Scene.HomeTimeline.NavigationBarState.Publishing" = "Publishing post...";
|
||||||
"Scene.HomeTimeline.Title" = "Home";
|
"Scene.HomeTimeline.Title" = "Home";
|
||||||
|
"Scene.Notification.Action.Favourite" = "favorited your post";
|
||||||
|
"Scene.Notification.Action.Follow" = "followed you";
|
||||||
|
"Scene.Notification.Action.Mention" = "mentioned you";
|
||||||
|
"Scene.Notification.Action.Poll" = "Your poll has ended";
|
||||||
|
"Scene.Notification.Action.Reblog" = "rebloged your post";
|
||||||
|
"Scene.Notification.Title.Everything" = "Everything";
|
||||||
|
"Scene.Notification.Title.Mentions" = "Mentions";
|
||||||
"Scene.Profile.Dashboard.Followers" = "followers";
|
"Scene.Profile.Dashboard.Followers" = "followers";
|
||||||
"Scene.Profile.Dashboard.Following" = "following";
|
"Scene.Profile.Dashboard.Following" = "following";
|
||||||
"Scene.Profile.Dashboard.Posts" = "posts";
|
"Scene.Profile.Dashboard.Posts" = "posts";
|
||||||
|
@ -190,6 +200,27 @@ any server.";
|
||||||
"Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@.";
|
"Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@.";
|
||||||
"Scene.ServerRules.TermsOfService" = "terms of service";
|
"Scene.ServerRules.TermsOfService" = "terms of service";
|
||||||
"Scene.ServerRules.Title" = "Some ground rules.";
|
"Scene.ServerRules.Title" = "Some ground rules.";
|
||||||
|
"Scene.Settings.Section.Appearance.Automatic" = "Automatic";
|
||||||
|
"Scene.Settings.Section.Appearance.Dark" = "Always Dark";
|
||||||
|
"Scene.Settings.Section.Appearance.Light" = "Always Light";
|
||||||
|
"Scene.Settings.Section.Appearance.Title" = "Appearance";
|
||||||
|
"Scene.Settings.Section.Boringzone.Privacy" = "Privacy Policy";
|
||||||
|
"Scene.Settings.Section.Boringzone.Terms" = "Terms of Service";
|
||||||
|
"Scene.Settings.Section.Boringzone.Title" = "The Boring zone";
|
||||||
|
"Scene.Settings.Section.Notifications.Boosts" = "Reblogs my post";
|
||||||
|
"Scene.Settings.Section.Notifications.Favorites" = "Favorites my post";
|
||||||
|
"Scene.Settings.Section.Notifications.Follows" = "Follows me";
|
||||||
|
"Scene.Settings.Section.Notifications.Mentions" = "Mentions me";
|
||||||
|
"Scene.Settings.Section.Notifications.Title" = "Notifications";
|
||||||
|
"Scene.Settings.Section.Notifications.Trigger.Anyone" = "anyone";
|
||||||
|
"Scene.Settings.Section.Notifications.Trigger.Follow" = "anyone I follow";
|
||||||
|
"Scene.Settings.Section.Notifications.Trigger.Follower" = "a follower";
|
||||||
|
"Scene.Settings.Section.Notifications.Trigger.Noone" = "no one";
|
||||||
|
"Scene.Settings.Section.Notifications.Trigger.Title" = "Notify me when";
|
||||||
|
"Scene.Settings.Section.Spicyzone.Clear" = "Clear Media Cache";
|
||||||
|
"Scene.Settings.Section.Spicyzone.Signout" = "Sign Out";
|
||||||
|
"Scene.Settings.Section.Spicyzone.Title" = "The spicy zone";
|
||||||
|
"Scene.Settings.Title" = "Settings";
|
||||||
"Scene.Thread.BackTitle" = "Post";
|
"Scene.Thread.BackTitle" = "Post";
|
||||||
"Scene.Thread.Favorite.Multiple" = "%@ favorites";
|
"Scene.Thread.Favorite.Multiple" = "%@ favorites";
|
||||||
"Scene.Thread.Favorite.Single" = "%@ favorite";
|
"Scene.Thread.Favorite.Single" = "%@ favorite";
|
||||||
|
@ -197,4 +228,4 @@ any server.";
|
||||||
"Scene.Thread.Reblog.Single" = "%@ reblog";
|
"Scene.Thread.Reblog.Single" = "%@ reblog";
|
||||||
"Scene.Thread.Title" = "Post from %@";
|
"Scene.Thread.Title" = "Post from %@";
|
||||||
"Scene.Welcome.Slogan" = "Social networking
|
"Scene.Welcome.Slogan" = "Social networking
|
||||||
back in your hands.";
|
back in your hands.";
|
||||||
|
|
|
@ -123,9 +123,11 @@ extension HashtagTimelineViewModel.LoadOldestState {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var snapshot = diffableDataSource.snapshot()
|
DispatchQueue.main.async {
|
||||||
snapshot.deleteItems([.bottomLoader])
|
var snapshot = diffableDataSource.snapshot()
|
||||||
diffableDataSource.apply(snapshot)
|
snapshot.deleteItems([.bottomLoader])
|
||||||
|
diffableDataSource.apply(snapshot)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,10 @@ extension HomeTimelineViewController {
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.showThreadAction(action)
|
self.showThreadAction(action)
|
||||||
},
|
},
|
||||||
|
UIAction(title: "Settings", image: UIImage(systemName: "gear"), attributes: []) { [weak self] action in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.showSettings(action)
|
||||||
|
},
|
||||||
UIAction(title: "Sign Out", image: UIImage(systemName: "escape"), attributes: .destructive) { [weak self] action in
|
UIAction(title: "Sign Out", image: UIImage(systemName: "escape"), attributes: .destructive) { [weak self] action in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.signOutAction(action)
|
self.signOutAction(action)
|
||||||
|
@ -323,5 +327,8 @@ extension HomeTimelineViewController {
|
||||||
coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil))
|
coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func showSettings(_ sender: UIAction) {
|
||||||
|
coordinator.present(scene: .settings, from: self, transition: .modal(animated: true, completion: nil))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -103,9 +103,11 @@ extension HomeTimelineViewModel.LoadOldestState {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var snapshot = diffableDataSource.snapshot()
|
DispatchQueue.main.async {
|
||||||
snapshot.deleteItems([.bottomLoader])
|
var snapshot = diffableDataSource.snapshot()
|
||||||
diffableDataSource.apply(snapshot)
|
snapshot.deleteItems([.bottomLoader])
|
||||||
|
diffableDataSource.apply(snapshot)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,23 +2,227 @@
|
||||||
// NotificationViewController.swift
|
// NotificationViewController.swift
|
||||||
// Mastodon
|
// Mastodon
|
||||||
//
|
//
|
||||||
// Created by MainasuK Cirno on 2021-2-23.
|
// Created by sxiaojian on 2021/4/12.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import GameplayKit
|
||||||
|
import MastodonSDK
|
||||||
|
import OSLog
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
final class NotificationViewController: UIViewController, NeedsDependency {
|
final class NotificationViewController: UIViewController, NeedsDependency {
|
||||||
|
|
||||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
private(set) lazy var viewModel = NotificationViewModel(context: context)
|
||||||
|
|
||||||
|
let segmentControl: UISegmentedControl = {
|
||||||
|
let control = UISegmentedControl(items: [L10n.Scene.Notification.Title.everything, L10n.Scene.Notification.Title.mentions])
|
||||||
|
control.selectedSegmentIndex = NotificationViewModel.NotificationSegment.EveryThing.rawValue
|
||||||
|
return control
|
||||||
|
}()
|
||||||
|
|
||||||
|
let tableView: UITableView = {
|
||||||
|
let tableView = ControlContainableTableView()
|
||||||
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
|
tableView.separatorStyle = .singleLine
|
||||||
|
tableView.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
|
||||||
|
tableView.register(NotificationTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationTableViewCell.self))
|
||||||
|
tableView.register(NotificationStatusTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationStatusTableViewCell.self))
|
||||||
|
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
|
||||||
|
tableView.tableFooterView = UIView()
|
||||||
|
tableView.estimatedRowHeight = UITableView.automaticDimension
|
||||||
|
return tableView
|
||||||
|
}()
|
||||||
|
|
||||||
|
let refreshControl = UIRefreshControl()
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NotificationViewController {
|
extension NotificationViewController {
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
|
||||||
|
navigationItem.titleView = segmentControl
|
||||||
|
segmentControl.addTarget(self, action: #selector(NotificationViewController.segmentedControlValueChanged(_:)), for: .valueChanged)
|
||||||
|
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(tableView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
||||||
|
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
tableView.refreshControl = refreshControl
|
||||||
|
refreshControl.addTarget(self, action: #selector(NotificationViewController.refreshControlValueChanged(_:)), for: .valueChanged)
|
||||||
|
|
||||||
|
tableView.delegate = self
|
||||||
|
viewModel.tableView = tableView
|
||||||
|
viewModel.contentOffsetAdjustableTimelineViewControllerDelegate = self
|
||||||
|
viewModel.setupDiffableDataSource(for: tableView, delegate: self, dependency: self)
|
||||||
|
viewModel.viewDidLoad.send()
|
||||||
|
// bind refresh control
|
||||||
|
viewModel.isFetchingLatestNotification
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] isFetching in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if !isFetching {
|
||||||
|
UIView.animate(withDuration: 0.5) { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.refreshControl.endRefreshing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
// needs trigger manually after onboarding dismiss
|
||||||
|
setNeedsStatusBarAppearanceUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if (self.viewModel.fetchedResultsController.fetchedObjects ?? []).count == 0 {
|
||||||
|
self.viewModel.loadLatestStateMachine.enter(NotificationViewModel.LoadLatestState.Loading.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||||
|
super.viewWillTransition(to: size, with: coordinator)
|
||||||
|
|
||||||
|
coordinator.animate { _ in
|
||||||
|
// do nothing
|
||||||
|
} completion: { _ in
|
||||||
|
self.tableView.reloadData()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationViewController {
|
||||||
|
@objc private func segmentedControlValueChanged(_ sender: UISegmentedControl) {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: select at index: %ld", (#file as NSString).lastPathComponent, #line, #function, sender.selectedSegmentIndex)
|
||||||
|
guard let domain = viewModel.activeMastodonAuthenticationBox.value?.domain, let userID = viewModel.activeMastodonAuthenticationBox.value?.userID else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if sender.selectedSegmentIndex == NotificationViewModel.NotificationSegment.EveryThing.rawValue {
|
||||||
|
viewModel.notificationPredicate.value = MastodonNotification.predicate(domain: domain, userID: userID)
|
||||||
|
} else {
|
||||||
|
viewModel.notificationPredicate.value = MastodonNotification.predicate(domain: domain, userID: userID, typeRaw: Mastodon.Entity.Notification.NotificationType.mention.rawValue)
|
||||||
|
}
|
||||||
|
viewModel.selectedIndex.value = NotificationViewModel.NotificationSegment(rawValue: sender.selectedSegmentIndex)!
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func refreshControlValueChanged(_ sender: UIRefreshControl) {
|
||||||
|
guard viewModel.loadLatestStateMachine.enter(NotificationViewModel.LoadLatestState.Loading.self) else {
|
||||||
|
sender.endRefreshing()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationViewController {
|
||||||
|
func cacheTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||||
|
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||||
|
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
|
let key = item.hashValue
|
||||||
|
let frame = cell.frame
|
||||||
|
viewModel.cellFrameCache.setObject(NSValue(cgRect: frame), forKey: NSNumber(value: key))
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||||
|
guard let diffableDataSource = viewModel.diffableDataSource else { return UITableView.automaticDimension }
|
||||||
|
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return UITableView.automaticDimension }
|
||||||
|
guard let frame = viewModel.cellFrameCache.object(forKey: NSNumber(value: item.hashValue))?.cgRectValue else {
|
||||||
|
if case .bottomLoader = item {
|
||||||
|
return TimelineLoaderTableViewCell.cellHeight
|
||||||
|
} else {
|
||||||
|
return UITableView.automaticDimension
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ceil(frame.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// MARK: - UITableViewDelegate
|
||||||
|
|
||||||
|
extension NotificationViewController: UITableViewDelegate {
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
tableView.deselectRow(at: indexPath, animated: true)
|
||||||
|
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||||
|
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
|
switch item {
|
||||||
|
case .notification(let objectID):
|
||||||
|
let notification = context.managedObjectContext.object(with: objectID) as! MastodonNotification
|
||||||
|
if let status = notification.status {
|
||||||
|
let viewModel = ThreadViewModel(context: context, optionalStatus: status)
|
||||||
|
coordinator.present(scene: .thread(viewModel: viewModel), from: self, transition: .show)
|
||||||
|
} else {
|
||||||
|
let viewModel = ProfileViewModel(context: context, optionalMastodonUser: notification.account)
|
||||||
|
coordinator.present(scene: .profile(viewModel: viewModel), from: self, transition: .show)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||||
|
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||||
|
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
|
switch item {
|
||||||
|
case .bottomLoader:
|
||||||
|
if !tableView.isDragging, !tableView.isDecelerating {
|
||||||
|
viewModel.loadoldestStateMachine.enter(NotificationViewModel.LoadOldestState.Loading.self)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate
|
||||||
|
|
||||||
|
extension NotificationViewController: ContentOffsetAdjustableTimelineViewControllerDelegate {
|
||||||
|
func navigationBar() -> UINavigationBar? {
|
||||||
|
navigationController?.navigationBar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationViewController: NotificationTableViewCellDelegate {
|
||||||
|
func userAvatarDidPressed(notification: MastodonNotification) {
|
||||||
|
let viewModel = ProfileViewModel(context: context, optionalMastodonUser: notification.account)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.coordinator.present(scene: .profile(viewModel: viewModel), from: self, transition: .show)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parent() -> UIViewController {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UIScrollViewDelegate
|
||||||
|
|
||||||
|
extension NotificationViewController {
|
||||||
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
handleScrollViewDidScroll(scrollView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationViewController: LoadMoreConfigurableTableViewContainer {
|
||||||
|
typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell
|
||||||
|
typealias LoadingState = NotificationViewModel.LoadOldestState.Loading
|
||||||
|
var loadMoreConfigurableTableView: UITableView { tableView }
|
||||||
|
var loadMoreConfigurableStateMachine: GKStateMachine { viewModel.loadoldestStateMachine }
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
//
|
||||||
|
// NotificationViewModel+LoadLatestState.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/4/13.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import Foundation
|
||||||
|
import GameplayKit
|
||||||
|
import MastodonSDK
|
||||||
|
import os.log
|
||||||
|
import func QuartzCore.CACurrentMediaTime
|
||||||
|
|
||||||
|
extension NotificationViewModel {
|
||||||
|
class LoadLatestState: GKState {
|
||||||
|
weak var viewModel: NotificationViewModel?
|
||||||
|
|
||||||
|
init(viewModel: NotificationViewModel) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", (#file as NSString).lastPathComponent, #line, #function, debugDescription, previousState.debugDescription)
|
||||||
|
viewModel?.loadLatestStateMachinePublisher.send(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationViewModel.LoadLatestState {
|
||||||
|
class Initial: NotificationViewModel.LoadLatestState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
stateClass == Loading.self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Loading: NotificationViewModel.LoadLatestState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
stateClass == Fail.self || stateClass == Idle.self
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
super.didEnter(from: previousState)
|
||||||
|
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||||
|
guard let activeMastodonAuthenticationBox = viewModel.activeMastodonAuthenticationBox.value else {
|
||||||
|
// sign out when loading will enter here
|
||||||
|
stateMachine.enter(Fail.self)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let query = Mastodon.API.Notifications.Query(
|
||||||
|
maxID: nil,
|
||||||
|
sinceID: nil,
|
||||||
|
minID: nil,
|
||||||
|
limit: nil,
|
||||||
|
excludeTypes: [.followRequest],
|
||||||
|
accountID: nil
|
||||||
|
)
|
||||||
|
viewModel.context.apiService.allNotifications(
|
||||||
|
domain: activeMastodonAuthenticationBox.domain,
|
||||||
|
query: query,
|
||||||
|
mastodonAuthenticationBox: activeMastodonAuthenticationBox
|
||||||
|
)
|
||||||
|
.sink { completion in
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
viewModel.isFetchingLatestNotification.value = false
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch notification failed. %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription)
|
||||||
|
case .finished:
|
||||||
|
// handle isFetchingLatestTimeline in fetch controller delegate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
stateMachine.enter(Idle.self)
|
||||||
|
} receiveValue: { response in
|
||||||
|
if response.value.isEmpty {
|
||||||
|
viewModel.isFetchingLatestNotification.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &viewModel.disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Fail: NotificationViewModel.LoadLatestState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
stateClass == Loading.self || stateClass == Idle.self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Idle: NotificationViewModel.LoadLatestState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
stateClass == Loading.self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,145 @@
|
||||||
|
//
|
||||||
|
// NotificationViewModel+LoadOldestState.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/4/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreDataStack
|
||||||
|
import Foundation
|
||||||
|
import GameplayKit
|
||||||
|
import MastodonSDK
|
||||||
|
import os.log
|
||||||
|
|
||||||
|
extension NotificationViewModel {
|
||||||
|
class LoadOldestState: GKState {
|
||||||
|
weak var viewModel: NotificationViewModel?
|
||||||
|
|
||||||
|
init(viewModel: NotificationViewModel) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", (#file as NSString).lastPathComponent, #line, #function, debugDescription, previousState.debugDescription)
|
||||||
|
viewModel?.loadOldestStateMachinePublisher.send(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationViewModel.LoadOldestState {
|
||||||
|
class Initial: NotificationViewModel.LoadOldestState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
guard let viewModel = viewModel else { return false }
|
||||||
|
guard !(viewModel.fetchedResultsController.fetchedObjects ?? []).isEmpty else { return false }
|
||||||
|
return stateClass == Loading.self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Loading: NotificationViewModel.LoadOldestState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
stateClass == Fail.self || stateClass == Idle.self || stateClass == NoMore.self
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
super.didEnter(from: previousState)
|
||||||
|
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||||
|
guard let activeMastodonAuthenticationBox = viewModel.activeMastodonAuthenticationBox.value else {
|
||||||
|
assertionFailure()
|
||||||
|
stateMachine.enter(Fail.self)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let notifications: [MastodonNotification]? = {
|
||||||
|
let request = MastodonNotification.sortedFetchRequest
|
||||||
|
request.predicate = MastodonNotification.predicate(domain: activeMastodonAuthenticationBox.domain, userID: activeMastodonAuthenticationBox.userID)
|
||||||
|
request.returnsObjectsAsFaults = false
|
||||||
|
do {
|
||||||
|
return try self.viewModel?.context.managedObjectContext.fetch(request)
|
||||||
|
} catch {
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
guard let last = notifications?.last else {
|
||||||
|
stateMachine.enter(Idle.self)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let maxID = last.id
|
||||||
|
let query = Mastodon.API.Notifications.Query(
|
||||||
|
maxID: maxID,
|
||||||
|
sinceID: nil,
|
||||||
|
minID: nil,
|
||||||
|
limit: nil,
|
||||||
|
excludeTypes: [.followRequest],
|
||||||
|
accountID: nil)
|
||||||
|
viewModel.context.apiService.allNotifications(
|
||||||
|
domain: activeMastodonAuthenticationBox.domain,
|
||||||
|
query: query,
|
||||||
|
mastodonAuthenticationBox: activeMastodonAuthenticationBox
|
||||||
|
)
|
||||||
|
.sink { completion in
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch notification failed. %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription)
|
||||||
|
case .finished:
|
||||||
|
// handle isFetchingLatestTimeline in fetch controller delegate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
stateMachine.enter(Idle.self)
|
||||||
|
} receiveValue: { [weak viewModel] response in
|
||||||
|
guard let viewModel = viewModel else { return }
|
||||||
|
switch viewModel.selectedIndex.value {
|
||||||
|
case .EveryThing:
|
||||||
|
if response.value.isEmpty {
|
||||||
|
stateMachine.enter(NoMore.self)
|
||||||
|
} else {
|
||||||
|
stateMachine.enter(Idle.self)
|
||||||
|
}
|
||||||
|
case .Mentions:
|
||||||
|
viewModel.noMoreNotification.value = response.value.isEmpty
|
||||||
|
let list = response.value.filter { $0.type == Mastodon.Entity.Notification.NotificationType.mention }
|
||||||
|
if list.isEmpty {
|
||||||
|
stateMachine.enter(NoMore.self)
|
||||||
|
} else {
|
||||||
|
stateMachine.enter(Idle.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &viewModel.disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Fail: NotificationViewModel.LoadOldestState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
stateClass == Loading.self || stateClass == Idle.self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Idle: NotificationViewModel.LoadOldestState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
stateClass == Loading.self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NoMore: NotificationViewModel.LoadOldestState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
// reset state if needs
|
||||||
|
stateClass == Idle.self
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
guard let viewModel = viewModel else { return }
|
||||||
|
guard let diffableDataSource = viewModel.diffableDataSource else {
|
||||||
|
assertionFailure()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
var snapshot = diffableDataSource.snapshot()
|
||||||
|
snapshot.deleteItems([.bottomLoader])
|
||||||
|
diffableDataSource.apply(snapshot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
//
|
||||||
|
// NotificationViewModel+diffable.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/4/13.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension NotificationViewModel {
|
||||||
|
func setupDiffableDataSource(
|
||||||
|
for tableView: UITableView,
|
||||||
|
delegate: NotificationTableViewCellDelegate,
|
||||||
|
dependency: NeedsDependency
|
||||||
|
) {
|
||||||
|
let timestampUpdatePublisher = Timer.publish(every: 30.0, on: .main, in: .common)
|
||||||
|
.autoconnect()
|
||||||
|
.share()
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
guard let userid = activeMastodonAuthenticationBox.value?.userID else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
diffableDataSource = NotificationSection.tableViewDiffableDataSource(
|
||||||
|
for: tableView,
|
||||||
|
timestampUpdatePublisher: timestampUpdatePublisher,
|
||||||
|
managedObjectContext: context.managedObjectContext,
|
||||||
|
delegate: delegate,
|
||||||
|
dependency: dependency,
|
||||||
|
requestUserID: userid
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationViewModel: NSFetchedResultsControllerDelegate {
|
||||||
|
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function)
|
||||||
|
}
|
||||||
|
|
||||||
|
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function)
|
||||||
|
|
||||||
|
guard let tableView = self.tableView else { return }
|
||||||
|
guard let navigationBar = contentOffsetAdjustableTimelineViewControllerDelegate?.navigationBar() else { return }
|
||||||
|
|
||||||
|
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||||
|
|
||||||
|
let predicate = fetchedResultsController.fetchRequest.predicate
|
||||||
|
let parentManagedObjectContext = fetchedResultsController.managedObjectContext
|
||||||
|
let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
|
||||||
|
managedObjectContext.parent = parentManagedObjectContext
|
||||||
|
|
||||||
|
managedObjectContext.perform {
|
||||||
|
let notifications: [MastodonNotification] = {
|
||||||
|
let request = MastodonNotification.sortedFetchRequest
|
||||||
|
request.returnsObjectsAsFaults = false
|
||||||
|
request.predicate = predicate
|
||||||
|
do {
|
||||||
|
return try managedObjectContext.fetch(request)
|
||||||
|
} catch {
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let oldSnapshot = diffableDataSource.snapshot()
|
||||||
|
var newSnapshot = NSDiffableDataSourceSnapshot<NotificationSection, NotificationItem>()
|
||||||
|
newSnapshot.appendSections([.main])
|
||||||
|
newSnapshot.appendItems(notifications.map { NotificationItem.notification(objectID: $0.objectID) }, toSection: .main)
|
||||||
|
if !notifications.isEmpty, self.noMoreNotification.value == false {
|
||||||
|
newSnapshot.appendItems([.bottomLoader], toSection: .main)
|
||||||
|
}
|
||||||
|
guard let difference = self.calculateReloadSnapshotDifference(navigationBar: navigationBar, tableView: tableView, oldSnapshot: oldSnapshot, newSnapshot: newSnapshot) else {
|
||||||
|
diffableDataSource.apply(newSnapshot, animatingDifferences: false)
|
||||||
|
self.isFetchingLatestNotification.value = false
|
||||||
|
tableView.reloadData()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
diffableDataSource.apply(newSnapshot, animatingDifferences: false) {
|
||||||
|
tableView.scrollToRow(at: difference.targetIndexPath, at: .top, animated: false)
|
||||||
|
tableView.contentOffset.y = tableView.contentOffset.y - difference.offset
|
||||||
|
self.isFetchingLatestNotification.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct Difference<T> {
|
||||||
|
let item: T
|
||||||
|
let sourceIndexPath: IndexPath
|
||||||
|
let targetIndexPath: IndexPath
|
||||||
|
let offset: CGFloat
|
||||||
|
}
|
||||||
|
|
||||||
|
private func calculateReloadSnapshotDifference<T: Hashable>(
|
||||||
|
navigationBar: UINavigationBar,
|
||||||
|
tableView: UITableView,
|
||||||
|
oldSnapshot: NSDiffableDataSourceSnapshot<NotificationSection, T>,
|
||||||
|
newSnapshot: NSDiffableDataSourceSnapshot<NotificationSection, T>
|
||||||
|
) -> Difference<T>? {
|
||||||
|
guard oldSnapshot.numberOfItems != 0 else { return nil }
|
||||||
|
|
||||||
|
// old snapshot not empty. set source index path to first item if not match
|
||||||
|
let sourceIndexPath = UIViewController.topVisibleTableViewCellIndexPath(in: tableView, navigationBar: navigationBar) ?? IndexPath(row: 0, section: 0)
|
||||||
|
|
||||||
|
guard sourceIndexPath.row < oldSnapshot.itemIdentifiers(inSection: .main).count else { return nil }
|
||||||
|
|
||||||
|
if oldSnapshot.itemIdentifiers.elementsEqual(newSnapshot.itemIdentifiers) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let timelineItem = oldSnapshot.itemIdentifiers(inSection: .main)[sourceIndexPath.row]
|
||||||
|
guard let itemIndex = newSnapshot.itemIdentifiers(inSection: .main).firstIndex(of: timelineItem) else { return nil }
|
||||||
|
let targetIndexPath = IndexPath(row: itemIndex, section: 0)
|
||||||
|
|
||||||
|
let offset = UIViewController.tableViewCellOriginOffsetToWindowTop(in: tableView, at: sourceIndexPath, navigationBar: navigationBar)
|
||||||
|
return Difference(
|
||||||
|
item: timelineItem,
|
||||||
|
sourceIndexPath: sourceIndexPath,
|
||||||
|
targetIndexPath: targetIndexPath,
|
||||||
|
offset: offset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
//
|
||||||
|
// NotificationViewModel.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/4/12.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import Foundation
|
||||||
|
import GameplayKit
|
||||||
|
import MastodonSDK
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class NotificationViewModel: NSObject {
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
// input
|
||||||
|
let context: AppContext
|
||||||
|
weak var tableView: UITableView?
|
||||||
|
weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate?
|
||||||
|
|
||||||
|
let viewDidLoad = PassthroughSubject<Void, Never>()
|
||||||
|
let selectedIndex = CurrentValueSubject<NotificationSegment, Never>(.EveryThing)
|
||||||
|
let noMoreNotification = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
|
||||||
|
let activeMastodonAuthenticationBox: CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>
|
||||||
|
let fetchedResultsController: NSFetchedResultsController<MastodonNotification>!
|
||||||
|
let notificationPredicate = CurrentValueSubject<NSPredicate?, Never>(nil)
|
||||||
|
let cellFrameCache = NSCache<NSNumber, NSValue>()
|
||||||
|
|
||||||
|
let isFetchingLatestNotification = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
|
||||||
|
// output
|
||||||
|
var diffableDataSource: UITableViewDiffableDataSource<NotificationSection, NotificationItem>!
|
||||||
|
// top loader
|
||||||
|
private(set) lazy var loadLatestStateMachine: GKStateMachine = {
|
||||||
|
// exclude timeline middle fetcher state
|
||||||
|
let stateMachine = GKStateMachine(states: [
|
||||||
|
LoadLatestState.Initial(viewModel: self),
|
||||||
|
LoadLatestState.Loading(viewModel: self),
|
||||||
|
LoadLatestState.Fail(viewModel: self),
|
||||||
|
LoadLatestState.Idle(viewModel: self),
|
||||||
|
])
|
||||||
|
stateMachine.enter(LoadLatestState.Initial.self)
|
||||||
|
return stateMachine
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var loadLatestStateMachinePublisher = CurrentValueSubject<LoadLatestState?, Never>(nil)
|
||||||
|
|
||||||
|
// bottom loader
|
||||||
|
private(set) lazy var loadoldestStateMachine: GKStateMachine = {
|
||||||
|
// exclude timeline middle fetcher state
|
||||||
|
let stateMachine = GKStateMachine(states: [
|
||||||
|
LoadOldestState.Initial(viewModel: self),
|
||||||
|
LoadOldestState.Loading(viewModel: self),
|
||||||
|
LoadOldestState.Fail(viewModel: self),
|
||||||
|
LoadOldestState.Idle(viewModel: self),
|
||||||
|
LoadOldestState.NoMore(viewModel: self),
|
||||||
|
])
|
||||||
|
stateMachine.enter(LoadOldestState.Initial.self)
|
||||||
|
return stateMachine
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var loadOldestStateMachinePublisher = CurrentValueSubject<LoadOldestState?, Never>(nil)
|
||||||
|
|
||||||
|
init(context: AppContext) {
|
||||||
|
self.context = context
|
||||||
|
self.activeMastodonAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value)
|
||||||
|
self.fetchedResultsController = {
|
||||||
|
let fetchRequest = MastodonNotification.sortedFetchRequest
|
||||||
|
fetchRequest.returnsObjectsAsFaults = false
|
||||||
|
fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(MastodonNotification.status), #keyPath(MastodonNotification.account)]
|
||||||
|
let controller = NSFetchedResultsController(
|
||||||
|
fetchRequest: fetchRequest,
|
||||||
|
managedObjectContext: context.managedObjectContext,
|
||||||
|
sectionNameKeyPath: nil,
|
||||||
|
cacheName: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
return controller
|
||||||
|
}()
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
fetchedResultsController.delegate = self
|
||||||
|
context.authenticationService.activeMastodonAuthenticationBox
|
||||||
|
.sink(receiveValue: { [weak self] box in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.activeMastodonAuthenticationBox.value = box
|
||||||
|
if let domain = box?.domain, let userID = box?.userID {
|
||||||
|
self.notificationPredicate.value = MastodonNotification.predicate(domain: domain, userID: userID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
notificationPredicate
|
||||||
|
.compactMap { $0 }
|
||||||
|
.sink { [weak self] predicate in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.fetchedResultsController.fetchRequest.predicate = predicate
|
||||||
|
do {
|
||||||
|
self.diffableDataSource?.defaultRowAnimation = .fade
|
||||||
|
try self.fetchedResultsController.performFetch()
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.diffableDataSource?.defaultRowAnimation = .automatic
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
viewDidLoad
|
||||||
|
.sink { [weak self] in
|
||||||
|
|
||||||
|
guard let domain = self?.activeMastodonAuthenticationBox.value?.domain, let userID = self?.activeMastodonAuthenticationBox.value?.userID else { return }
|
||||||
|
self?.notificationPredicate.value = MastodonNotification.predicate(domain: domain, userID: userID)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationViewModel {
|
||||||
|
enum NotificationSegment: Int {
|
||||||
|
case EveryThing
|
||||||
|
case Mentions
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,209 @@
|
||||||
|
//
|
||||||
|
// NotificationStatusTableViewCell.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/4/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class NotificationStatusTableViewCell: UITableViewCell, StatusCell {
|
||||||
|
static let actionImageBorderWidth: CGFloat = 2
|
||||||
|
static let statusPadding = UIEdgeInsets(top: 50, left: 73, bottom: 24, right: 24)
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
var pollCountdownSubscription: AnyCancellable?
|
||||||
|
var delegate: NotificationTableViewCellDelegate?
|
||||||
|
|
||||||
|
let avatatImageView: UIImageView = {
|
||||||
|
let imageView = UIImageView()
|
||||||
|
imageView.layer.cornerRadius = 4
|
||||||
|
imageView.layer.cornerCurve = .continuous
|
||||||
|
imageView.clipsToBounds = true
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
|
||||||
|
let actionImageView: UIImageView = {
|
||||||
|
let imageView = UIImageView()
|
||||||
|
imageView.tintColor = Asset.Colors.Background.systemBackground.color
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
|
||||||
|
let actionImageBackground: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.layer.cornerRadius = (24 + NotificationStatusTableViewCell.actionImageBorderWidth) / 2
|
||||||
|
view.layer.cornerCurve = .continuous
|
||||||
|
view.clipsToBounds = true
|
||||||
|
view.layer.borderWidth = NotificationStatusTableViewCell.actionImageBorderWidth
|
||||||
|
view.layer.borderColor = Asset.Colors.Background.systemBackground.color.cgColor
|
||||||
|
view.tintColor = Asset.Colors.Background.systemBackground.color
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
let avatarContainer: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
let actionLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.textColor = Asset.Colors.Label.secondary.color
|
||||||
|
label.font = UIFont.preferredFont(forTextStyle: .body)
|
||||||
|
label.lineBreakMode = .byTruncatingTail
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
let nameLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.textColor = Asset.Colors.brandBlue.color
|
||||||
|
label.font = .systemFont(ofSize: 15, weight: .semibold)
|
||||||
|
label.lineBreakMode = .byTruncatingTail
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
let statusBorder: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
view.layer.cornerRadius = 6
|
||||||
|
view.layer.borderWidth = 2
|
||||||
|
view.layer.cornerCurve = .continuous
|
||||||
|
view.layer.borderColor = Asset.Colors.Border.notification.color.cgColor
|
||||||
|
view.clipsToBounds = true
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
let statusView = StatusView()
|
||||||
|
|
||||||
|
override func prepareForReuse() {
|
||||||
|
super.prepareForReuse()
|
||||||
|
avatatImageView.af.cancelImageRequest()
|
||||||
|
statusView.isStatusTextSensitive = false
|
||||||
|
statusView.cleanUpContentWarning()
|
||||||
|
statusView.pollTableView.dataSource = nil
|
||||||
|
statusView.playerContainerView.reset()
|
||||||
|
statusView.playerContainerView.isHidden = true
|
||||||
|
|
||||||
|
disposeBag.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
|
configure()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
configure()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.statusView.drawContentWarningImageView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationStatusTableViewCell {
|
||||||
|
func configure() {
|
||||||
|
let containerStackView = UIStackView()
|
||||||
|
containerStackView.axis = .horizontal
|
||||||
|
containerStackView.alignment = .top
|
||||||
|
containerStackView.spacing = 4
|
||||||
|
containerStackView.layoutMargins = UIEdgeInsets(top: 14, left: 0, bottom: 12, right: 0)
|
||||||
|
containerStackView.isLayoutMarginsRelativeArrangement = true
|
||||||
|
containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(containerStackView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||||
|
containerStackView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||||
|
contentView.readableContentGuide.trailingAnchor.constraint(equalTo: containerStackView.trailingAnchor),
|
||||||
|
contentView.bottomAnchor.constraint(equalTo: containerStackView.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
containerStackView.addArrangedSubview(avatarContainer)
|
||||||
|
avatarContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
avatarContainer.heightAnchor.constraint(equalToConstant: 47).priority(.required - 1),
|
||||||
|
avatarContainer.widthAnchor.constraint(equalToConstant: 47).priority(.required - 1)
|
||||||
|
])
|
||||||
|
|
||||||
|
avatarContainer.addSubview(avatatImageView)
|
||||||
|
avatatImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
avatatImageView.heightAnchor.constraint(equalToConstant: 35).priority(.required - 1),
|
||||||
|
avatatImageView.widthAnchor.constraint(equalToConstant: 35).priority(.required - 1),
|
||||||
|
avatatImageView.topAnchor.constraint(equalTo: avatarContainer.topAnchor),
|
||||||
|
avatatImageView.leadingAnchor.constraint(equalTo: avatarContainer.leadingAnchor)
|
||||||
|
])
|
||||||
|
|
||||||
|
avatarContainer.addSubview(actionImageBackground)
|
||||||
|
actionImageBackground.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
actionImageBackground.heightAnchor.constraint(equalToConstant: 24 + NotificationTableViewCell.actionImageBorderWidth).priority(.required - 1),
|
||||||
|
actionImageBackground.widthAnchor.constraint(equalToConstant: 24 + NotificationTableViewCell.actionImageBorderWidth).priority(.required - 1),
|
||||||
|
actionImageBackground.bottomAnchor.constraint(equalTo: avatarContainer.bottomAnchor),
|
||||||
|
actionImageBackground.trailingAnchor.constraint(equalTo: avatarContainer.trailingAnchor)
|
||||||
|
])
|
||||||
|
|
||||||
|
avatarContainer.addSubview(actionImageView)
|
||||||
|
actionImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
actionImageView.centerXAnchor.constraint(equalTo: actionImageBackground.centerXAnchor),
|
||||||
|
actionImageView.centerYAnchor.constraint(equalTo: actionImageBackground.centerYAnchor)
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
let actionStackView = UIStackView()
|
||||||
|
actionStackView.axis = .horizontal
|
||||||
|
actionStackView.distribution = .fill
|
||||||
|
actionStackView.spacing = 4
|
||||||
|
actionStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
nameLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
actionStackView.addArrangedSubview(nameLabel)
|
||||||
|
actionLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
actionStackView.addArrangedSubview(actionLabel)
|
||||||
|
nameLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical)
|
||||||
|
nameLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||||
|
actionLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||||
|
let statusStackView = UIStackView()
|
||||||
|
statusStackView.axis = .vertical
|
||||||
|
|
||||||
|
statusStackView.distribution = .fill
|
||||||
|
statusStackView.spacing = 4
|
||||||
|
statusStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
statusView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
statusStackView.addArrangedSubview(actionStackView)
|
||||||
|
|
||||||
|
statusBorder.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
statusView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
statusBorder.addSubview(statusView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
statusView.topAnchor.constraint(equalTo: statusBorder.topAnchor, constant: 12),
|
||||||
|
statusView.leadingAnchor.constraint(equalTo: statusBorder.leadingAnchor, constant: 12),
|
||||||
|
statusBorder.bottomAnchor.constraint(equalTo: statusView.bottomAnchor, constant: 12),
|
||||||
|
statusBorder.trailingAnchor.constraint(equalTo: statusView.trailingAnchor, constant: 12),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
statusStackView.addArrangedSubview(statusBorder)
|
||||||
|
|
||||||
|
containerStackView.addArrangedSubview(statusStackView)
|
||||||
|
|
||||||
|
statusView.contentWarningBlurContentImageView.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color
|
||||||
|
statusView.isUserInteractionEnabled = false
|
||||||
|
// remove item don't display
|
||||||
|
statusView.actionToolbarContainer.removeFromStackView()
|
||||||
|
// it affect stackView's height,need remove
|
||||||
|
statusView.avatarView.removeFromStackView()
|
||||||
|
statusView.usernameLabel.removeFromStackView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||||
|
super.traitCollectionDidChange(previousTraitCollection)
|
||||||
|
statusBorder.layer.borderColor = Asset.Colors.Border.notification.color.cgColor
|
||||||
|
actionImageBackground.layer.borderColor = Asset.Colors.Background.systemBackground.color.cgColor
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,155 @@
|
||||||
|
//
|
||||||
|
// NotificationTableViewCell.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/4/13.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import CoreDataStack
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
protocol NotificationTableViewCellDelegate: AnyObject {
|
||||||
|
var context: AppContext! { get }
|
||||||
|
|
||||||
|
func parent() -> UIViewController
|
||||||
|
|
||||||
|
func userAvatarDidPressed(notification: MastodonNotification)
|
||||||
|
}
|
||||||
|
|
||||||
|
final class NotificationTableViewCell: UITableViewCell {
|
||||||
|
static let actionImageBorderWidth: CGFloat = 2
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
var delegate: NotificationTableViewCellDelegate?
|
||||||
|
|
||||||
|
let avatatImageView: UIImageView = {
|
||||||
|
let imageView = UIImageView()
|
||||||
|
imageView.layer.cornerRadius = 4
|
||||||
|
imageView.layer.cornerCurve = .continuous
|
||||||
|
imageView.clipsToBounds = true
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
|
||||||
|
let actionImageView: UIImageView = {
|
||||||
|
let imageView = UIImageView()
|
||||||
|
imageView.tintColor = Asset.Colors.Background.systemBackground.color
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
|
||||||
|
let actionImageBackground: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.layer.cornerRadius = (24 + NotificationTableViewCell.actionImageBorderWidth) / 2
|
||||||
|
view.layer.cornerCurve = .continuous
|
||||||
|
view.clipsToBounds = true
|
||||||
|
view.layer.borderWidth = NotificationTableViewCell.actionImageBorderWidth
|
||||||
|
view.layer.borderColor = Asset.Colors.Background.systemBackground.color.cgColor
|
||||||
|
view.tintColor = Asset.Colors.Background.systemBackground.color
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
let avatarContainer: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
let actionLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.textColor = Asset.Colors.Label.secondary.color
|
||||||
|
label.font = UIFont.preferredFont(forTextStyle: .body)
|
||||||
|
label.lineBreakMode = .byTruncatingTail
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
let nameLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.textColor = Asset.Colors.brandBlue.color
|
||||||
|
label.font = .systemFont(ofSize: 15, weight: .semibold)
|
||||||
|
label.lineBreakMode = .byTruncatingTail
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
override func prepareForReuse() {
|
||||||
|
super.prepareForReuse()
|
||||||
|
avatatImageView.af.cancelImageRequest()
|
||||||
|
disposeBag.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
|
configure()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
configure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationTableViewCell {
|
||||||
|
func configure() {
|
||||||
|
|
||||||
|
let containerStackView = UIStackView()
|
||||||
|
containerStackView.axis = .horizontal
|
||||||
|
containerStackView.alignment = .center
|
||||||
|
containerStackView.spacing = 4
|
||||||
|
containerStackView.layoutMargins = UIEdgeInsets(top: 14, left: 0, bottom: 12, right: 0)
|
||||||
|
containerStackView.isLayoutMarginsRelativeArrangement = true
|
||||||
|
containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(containerStackView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||||
|
containerStackView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||||
|
contentView.readableContentGuide.trailingAnchor.constraint(equalTo: containerStackView.trailingAnchor),
|
||||||
|
contentView.bottomAnchor.constraint(equalTo: containerStackView.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
containerStackView.addArrangedSubview(avatarContainer)
|
||||||
|
avatarContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
avatarContainer.heightAnchor.constraint(equalToConstant: 47).priority(.required - 1),
|
||||||
|
avatarContainer.widthAnchor.constraint(equalToConstant: 47).priority(.required - 1)
|
||||||
|
])
|
||||||
|
|
||||||
|
avatarContainer.addSubview(avatatImageView)
|
||||||
|
avatatImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
avatatImageView.heightAnchor.constraint(equalToConstant: 35).priority(.required - 1),
|
||||||
|
avatatImageView.widthAnchor.constraint(equalToConstant: 35).priority(.required - 1),
|
||||||
|
avatatImageView.topAnchor.constraint(equalTo: avatarContainer.topAnchor),
|
||||||
|
avatatImageView.leadingAnchor.constraint(equalTo: avatarContainer.leadingAnchor)
|
||||||
|
])
|
||||||
|
|
||||||
|
avatarContainer.addSubview(actionImageBackground)
|
||||||
|
actionImageBackground.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
actionImageBackground.heightAnchor.constraint(equalToConstant: 24 + NotificationTableViewCell.actionImageBorderWidth).priority(.required - 1),
|
||||||
|
actionImageBackground.widthAnchor.constraint(equalToConstant: 24 + NotificationTableViewCell.actionImageBorderWidth).priority(.required - 1),
|
||||||
|
actionImageBackground.bottomAnchor.constraint(equalTo: avatarContainer.bottomAnchor),
|
||||||
|
actionImageBackground.trailingAnchor.constraint(equalTo: avatarContainer.trailingAnchor)
|
||||||
|
])
|
||||||
|
|
||||||
|
avatarContainer.addSubview(actionImageView)
|
||||||
|
actionImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
actionImageView.centerXAnchor.constraint(equalTo: actionImageBackground.centerXAnchor),
|
||||||
|
actionImageView.centerYAnchor.constraint(equalTo: actionImageBackground.centerYAnchor)
|
||||||
|
])
|
||||||
|
|
||||||
|
nameLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
containerStackView.addArrangedSubview(nameLabel)
|
||||||
|
actionLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
containerStackView.addArrangedSubview(actionLabel)
|
||||||
|
nameLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical)
|
||||||
|
nameLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||||
|
actionLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||||
|
super.traitCollectionDidChange(previousTraitCollection)
|
||||||
|
actionImageBackground.layer.borderColor = Asset.Colors.Background.systemBackground.color.cgColor
|
||||||
|
}
|
||||||
|
}
|
|
@ -62,6 +62,7 @@ class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell {
|
||||||
|
|
||||||
let followButton: HighlightDimmableButton = {
|
let followButton: HighlightDimmableButton = {
|
||||||
let button = HighlightDimmableButton(type: .custom)
|
let button = HighlightDimmableButton(type: .custom)
|
||||||
|
button.setInsets(forContentPadding: UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16), imageTitlePadding: 0)
|
||||||
button.setTitleColor(.white, for: .normal)
|
button.setTitleColor(.white, for: .normal)
|
||||||
button.setTitle(L10n.Scene.Search.Recommend.Accounts.follow, for: .normal)
|
button.setTitle(L10n.Scene.Search.Recommend.Accounts.follow, for: .normal)
|
||||||
button.titleLabel?.font = .systemFont(ofSize: 14, weight: .semibold)
|
button.titleLabel?.font = .systemFont(ofSize: 14, weight: .semibold)
|
||||||
|
@ -97,50 +98,70 @@ extension SearchRecommendAccountsCollectionViewCell {
|
||||||
headerImageView.layer.borderColor = Asset.Colors.Border.searchCard.color.cgColor
|
headerImageView.layer.borderColor = Asset.Colors.Border.searchCard.color.cgColor
|
||||||
applyShadow(color: Asset.Colors.Shadow.searchCard.color, alpha: 0.1, x: 0, y: 3, blur: 12, spread: 0)
|
applyShadow(color: Asset.Colors.Shadow.searchCard.color, alpha: 0.1, x: 0, y: 3, blur: 12, spread: 0)
|
||||||
}
|
}
|
||||||
|
override open func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
followButton.layer.cornerRadius = followButton.frame.height/2
|
||||||
|
}
|
||||||
private func configure() {
|
private func configure() {
|
||||||
headerImageView.backgroundColor = Asset.Colors.brandBlue.color
|
headerImageView.backgroundColor = Asset.Colors.brandBlue.color
|
||||||
layer.cornerRadius = 10
|
layer.cornerRadius = 10
|
||||||
layer.cornerCurve = .continuous
|
layer.cornerCurve = .continuous
|
||||||
clipsToBounds = false
|
clipsToBounds = false
|
||||||
applyShadow(color: Asset.Colors.Shadow.searchCard.color, alpha: 0.1, x: 0, y: 3, blur: 12, spread: 0)
|
applyShadow(color: Asset.Colors.Shadow.searchCard.color, alpha: 0.1, x: 0, y: 3, blur: 12, spread: 0)
|
||||||
|
|
||||||
|
headerImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
contentView.addSubview(headerImageView)
|
contentView.addSubview(headerImageView)
|
||||||
headerImageView.pin(top: 16, left: 0, bottom: 0, right: 0)
|
NSLayoutConstraint.activate([
|
||||||
|
headerImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16),
|
||||||
|
headerImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||||
|
headerImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||||
|
headerImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
|
||||||
|
])
|
||||||
|
|
||||||
|
let containerStackView = UIStackView()
|
||||||
|
containerStackView.axis = .vertical
|
||||||
|
containerStackView.distribution = .fill
|
||||||
|
containerStackView.alignment = .center
|
||||||
|
containerStackView.spacing = 6
|
||||||
|
containerStackView.layoutMargins = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)
|
||||||
|
containerStackView.isLayoutMarginsRelativeArrangement = true
|
||||||
|
containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
contentView.addSubview(containerStackView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||||
|
containerStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||||
|
containerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
contentView.addSubview(avatarImageView)
|
contentView.addSubview(avatarImageView)
|
||||||
avatarImageView.pin(toSize: CGSize(width: 88, height: 88))
|
NSLayoutConstraint.activate([
|
||||||
avatarImageView.constrain([
|
avatarImageView.widthAnchor.constraint(equalToConstant: 88),
|
||||||
avatarImageView.constraint(.top, toView: contentView),
|
avatarImageView.heightAnchor.constraint(equalToConstant: 88)
|
||||||
avatarImageView.constraint(.centerX, toView: contentView)
|
|
||||||
])
|
])
|
||||||
|
containerStackView.addArrangedSubview(avatarImageView)
|
||||||
|
containerStackView.setCustomSpacing(20, after: avatarImageView)
|
||||||
|
displayNameLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
containerStackView.addArrangedSubview(displayNameLabel)
|
||||||
|
containerStackView.setCustomSpacing(0, after: displayNameLabel)
|
||||||
|
|
||||||
contentView.addSubview(displayNameLabel)
|
acctLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
displayNameLabel.constrain([
|
containerStackView.addArrangedSubview(acctLabel)
|
||||||
displayNameLabel.constraint(.top, toView: contentView, constant: 108),
|
containerStackView.setCustomSpacing(7, after: acctLabel)
|
||||||
displayNameLabel.constraint(.leading, toView: contentView),
|
|
||||||
displayNameLabel.constraint(.trailing, toView: contentView),
|
|
||||||
displayNameLabel.constraint(.centerX, toView: contentView)
|
|
||||||
])
|
|
||||||
|
|
||||||
contentView.addSubview(acctLabel)
|
followButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
acctLabel.constrain([
|
containerStackView.addArrangedSubview(followButton)
|
||||||
acctLabel.constraint(.top, toView: contentView, constant: 132),
|
NSLayoutConstraint.activate([
|
||||||
acctLabel.constraint(.leading, toView: contentView),
|
followButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 76),
|
||||||
acctLabel.constraint(.trailing, toView: contentView),
|
followButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 24)
|
||||||
acctLabel.constraint(.centerX, toView: contentView)
|
|
||||||
])
|
|
||||||
|
|
||||||
contentView.addSubview(followButton)
|
|
||||||
followButton.pin(toSize: CGSize(width: 76, height: 24))
|
|
||||||
followButton.constrain([
|
|
||||||
followButton.constraint(.top, toView: contentView, constant: 159),
|
|
||||||
followButton.constraint(.centerX, toView: contentView)
|
|
||||||
])
|
])
|
||||||
|
containerStackView.addArrangedSubview(followButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
func config(with mastodonUser: MastodonUser) {
|
func config(with mastodonUser: MastodonUser) {
|
||||||
displayNameLabel.text = mastodonUser.displayName.isEmpty ? mastodonUser.username : mastodonUser.displayName
|
displayNameLabel.text = mastodonUser.displayName.isEmpty ? mastodonUser.username : mastodonUser.displayName
|
||||||
acctLabel.text = mastodonUser.acct
|
acctLabel.text = "@" + mastodonUser.acct
|
||||||
avatarImageView.af.setImage(
|
avatarImageView.af.setImage(
|
||||||
withURL: URL(string: mastodonUser.avatar)!,
|
withURL: URL(string: mastodonUser.avatar)!,
|
||||||
placeholderImage: UIImage.placeholder(color: .systemFill),
|
placeholderImage: UIImage.placeholder(color: .systemFill),
|
||||||
|
@ -153,7 +174,13 @@ extension SearchRecommendAccountsCollectionViewCell {
|
||||||
) { [weak self] _ in
|
) { [weak self] _ in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.headerImageView.addSubview(self.visualEffectView)
|
self.headerImageView.addSubview(self.visualEffectView)
|
||||||
self.visualEffectView.pin(top: 0, left: 0, bottom: 0, right: 0)
|
self.visualEffectView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
self.visualEffectView.topAnchor.constraint(equalTo: self.headerImageView.topAnchor),
|
||||||
|
self.visualEffectView.leadingAnchor.constraint(equalTo: self.headerImageView.leadingAnchor),
|
||||||
|
self.visualEffectView.trailingAnchor.constraint(equalTo: self.headerImageView.trailingAnchor),
|
||||||
|
self.visualEffectView.bottomAnchor.constraint(equalTo: self.headerImageView.bottomAnchor)
|
||||||
|
])
|
||||||
}
|
}
|
||||||
delegate?.configFollowButton(with: mastodonUser, followButton: followButton)
|
delegate?.configFollowButton(with: mastodonUser, followButton: followButton)
|
||||||
followButton.publisher(for: .touchUpInside)
|
followButton.publisher(for: .touchUpInside)
|
||||||
|
|
|
@ -12,7 +12,6 @@ import UIKit
|
||||||
class SearchRecommendTagsCollectionViewCell: UICollectionViewCell {
|
class SearchRecommendTagsCollectionViewCell: UICollectionViewCell {
|
||||||
let backgroundImageView: UIImageView = {
|
let backgroundImageView: UIImageView = {
|
||||||
let imageView = UIImageView()
|
let imageView = UIImageView()
|
||||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
return imageView
|
return imageView
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -20,7 +19,6 @@ class SearchRecommendTagsCollectionViewCell: UICollectionViewCell {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.textColor = .white
|
label.textColor = .white
|
||||||
label.font = .systemFont(ofSize: 20, weight: .semibold)
|
label.font = .systemFont(ofSize: 20, weight: .semibold)
|
||||||
label.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
label.lineBreakMode = .byTruncatingTail
|
label.lineBreakMode = .byTruncatingTail
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
@ -29,7 +27,6 @@ class SearchRecommendTagsCollectionViewCell: UICollectionViewCell {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.textColor = .white
|
label.textColor = .white
|
||||||
label.font = .preferredFont(forTextStyle: .body)
|
label.font = .preferredFont(forTextStyle: .body)
|
||||||
label.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -38,7 +35,6 @@ class SearchRecommendTagsCollectionViewCell: UICollectionViewCell {
|
||||||
let image = UIImage(systemName: "flame.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .semibold))!.withRenderingMode(.alwaysTemplate)
|
let image = UIImage(systemName: "flame.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .semibold))!.withRenderingMode(.alwaysTemplate)
|
||||||
imageView.image = image
|
imageView.image = image
|
||||||
imageView.tintColor = .white
|
imageView.tintColor = .white
|
||||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
return imageView
|
return imageView
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -74,17 +70,49 @@ extension SearchRecommendTagsCollectionViewCell {
|
||||||
layer.borderColor = Asset.Colors.Border.searchCard.color.cgColor
|
layer.borderColor = Asset.Colors.Border.searchCard.color.cgColor
|
||||||
applyShadow(color: Asset.Colors.Shadow.searchCard.color, alpha: 0.1, x: 0, y: 3, blur: 12, spread: 0)
|
applyShadow(color: Asset.Colors.Shadow.searchCard.color, alpha: 0.1, x: 0, y: 3, blur: 12, spread: 0)
|
||||||
|
|
||||||
|
backgroundImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
contentView.addSubview(backgroundImageView)
|
contentView.addSubview(backgroundImageView)
|
||||||
backgroundImageView.constrain(toSuperviewEdges: nil)
|
NSLayoutConstraint.activate([
|
||||||
|
backgroundImageView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||||
|
backgroundImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||||
|
backgroundImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||||
|
backgroundImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
|
||||||
|
])
|
||||||
|
|
||||||
contentView.addSubview(hashtagTitleLabel)
|
|
||||||
hashtagTitleLabel.pin(top: 16, left: 16, bottom: nil, right: 42)
|
|
||||||
|
|
||||||
contentView.addSubview(peopleLabel)
|
let containerStackView = UIStackView()
|
||||||
peopleLabel.pinTopLeft(top: 46, left: 16)
|
containerStackView.axis = .vertical
|
||||||
|
containerStackView.distribution = .fill
|
||||||
|
containerStackView.spacing = 6
|
||||||
|
containerStackView.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 0, right: 16)
|
||||||
|
containerStackView.isLayoutMarginsRelativeArrangement = true
|
||||||
|
containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(containerStackView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||||
|
containerStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||||
|
containerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
|
||||||
|
])
|
||||||
|
|
||||||
contentView.addSubview(flameIconView)
|
|
||||||
flameIconView.pinTopRight(padding: 16)
|
let horizontalStackView = UIStackView()
|
||||||
|
horizontalStackView.axis = .horizontal
|
||||||
|
horizontalStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
horizontalStackView.distribution = .fill
|
||||||
|
|
||||||
|
hashtagTitleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
hashtagTitleLabel.setContentHuggingPriority(.defaultLow - 1, for: .horizontal)
|
||||||
|
horizontalStackView.addArrangedSubview(hashtagTitleLabel)
|
||||||
|
horizontalStackView.setContentHuggingPriority(.required - 1, for: .vertical)
|
||||||
|
|
||||||
|
flameIconView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
horizontalStackView.addArrangedSubview(flameIconView)
|
||||||
|
|
||||||
|
|
||||||
|
containerStackView.addArrangedSubview(horizontalStackView)
|
||||||
|
peopleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
peopleLabel.setContentHuggingPriority(.defaultLow - 1, for: .vertical)
|
||||||
|
containerStackView.addArrangedSubview(peopleLabel)
|
||||||
}
|
}
|
||||||
|
|
||||||
func config(with tag: Mastodon.Entity.Tag) {
|
func config(with tag: Mastodon.Entity.Tag) {
|
||||||
|
|
|
@ -23,8 +23,9 @@ extension SearchViewController {
|
||||||
hashtagCollectionView.register(SearchRecommendTagsCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: SearchRecommendTagsCollectionViewCell.self))
|
hashtagCollectionView.register(SearchRecommendTagsCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: SearchRecommendTagsCollectionViewCell.self))
|
||||||
hashtagCollectionView.delegate = self
|
hashtagCollectionView.delegate = self
|
||||||
|
|
||||||
|
hashtagCollectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
stackView.addArrangedSubview(hashtagCollectionView)
|
stackView.addArrangedSubview(hashtagCollectionView)
|
||||||
hashtagCollectionView.constrain([
|
NSLayoutConstraint.activate([
|
||||||
hashtagCollectionView.frameLayoutGuide.heightAnchor.constraint(equalToConstant: 130)
|
hashtagCollectionView.frameLayoutGuide.heightAnchor.constraint(equalToConstant: 130)
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
@ -39,8 +40,9 @@ extension SearchViewController {
|
||||||
accountsCollectionView.register(SearchRecommendAccountsCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: SearchRecommendAccountsCollectionViewCell.self))
|
accountsCollectionView.register(SearchRecommendAccountsCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: SearchRecommendAccountsCollectionViewCell.self))
|
||||||
accountsCollectionView.delegate = self
|
accountsCollectionView.delegate = self
|
||||||
|
|
||||||
|
accountsCollectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
stackView.addArrangedSubview(accountsCollectionView)
|
stackView.addArrangedSubview(accountsCollectionView)
|
||||||
accountsCollectionView.constrain([
|
NSLayoutConstraint.activate([
|
||||||
accountsCollectionView.frameLayoutGuide.heightAnchor.constraint(equalToConstant: 202)
|
accountsCollectionView.frameLayoutGuide.heightAnchor.constraint(equalToConstant: 202)
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,16 +15,18 @@ import UIKit
|
||||||
|
|
||||||
extension SearchViewController {
|
extension SearchViewController {
|
||||||
func setupSearchingTableView() {
|
func setupSearchingTableView() {
|
||||||
searchingTableView.delegate = self
|
|
||||||
searchingTableView.register(SearchingTableViewCell.self, forCellReuseIdentifier: String(describing: SearchingTableViewCell.self))
|
searchingTableView.register(SearchingTableViewCell.self, forCellReuseIdentifier: String(describing: SearchingTableViewCell.self))
|
||||||
searchingTableView.register(SearchBottomLoader.self, forCellReuseIdentifier: String(describing: SearchBottomLoader.self))
|
searchingTableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
|
||||||
|
searchingTableView.estimatedRowHeight = 66
|
||||||
|
searchingTableView.rowHeight = 66
|
||||||
view.addSubview(searchingTableView)
|
view.addSubview(searchingTableView)
|
||||||
searchingTableView.constrain([
|
searchingTableView.delegate = self
|
||||||
searchingTableView.frameLayoutGuide.topAnchor.constraint(equalTo: searchBar.bottomAnchor),
|
searchingTableView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
searchingTableView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
NSLayoutConstraint.activate([
|
||||||
searchingTableView.frameLayoutGuide.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
searchingTableView.topAnchor.constraint(equalTo: searchBar.bottomAnchor),
|
||||||
searchingTableView.frameLayoutGuide.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
|
searchingTableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||||
searchingTableView.contentLayoutGuide.widthAnchor.constraint(equalTo: view.widthAnchor)
|
searchingTableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||||
|
searchingTableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
|
||||||
])
|
])
|
||||||
searchingTableView.tableFooterView = UIView()
|
searchingTableView.tableFooterView = UIView()
|
||||||
viewModel.isSearching
|
viewModel.isSearching
|
||||||
|
@ -50,18 +52,23 @@ extension SearchViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupSearchHeader() {
|
func setupSearchHeader() {
|
||||||
searchHeader.addSubview(recentSearchesLabel)
|
let containerStackView = UIStackView()
|
||||||
recentSearchesLabel.constrain([
|
containerStackView.axis = .horizontal
|
||||||
recentSearchesLabel.constraint(.leading, toView: searchHeader, constant: 16),
|
containerStackView.distribution = .fill
|
||||||
recentSearchesLabel.constraint(.centerY, toView: searchHeader)
|
containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
containerStackView.layoutMargins = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 12)
|
||||||
|
containerStackView.isLayoutMarginsRelativeArrangement = true
|
||||||
|
searchHeader.addSubview(containerStackView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
containerStackView.topAnchor.constraint(equalTo: searchHeader.topAnchor),
|
||||||
|
containerStackView.leadingAnchor.constraint(equalTo: searchHeader.leadingAnchor),
|
||||||
|
containerStackView.trailingAnchor.constraint(equalTo: searchHeader.trailingAnchor),
|
||||||
|
containerStackView.bottomAnchor.constraint(equalTo: searchHeader.bottomAnchor)
|
||||||
])
|
])
|
||||||
|
recentSearchesLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
searchHeader.addSubview(clearSearchHistoryButton)
|
containerStackView.addArrangedSubview(recentSearchesLabel)
|
||||||
recentSearchesLabel.constrain([
|
clearSearchHistoryButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
searchHeader.trailingAnchor.constraint(equalTo: clearSearchHistoryButton.trailingAnchor, constant: 16),
|
containerStackView.addArrangedSubview(clearSearchHistoryButton)
|
||||||
clearSearchHistoryButton.constraint(.centerY, toView: searchHeader)
|
|
||||||
])
|
|
||||||
|
|
||||||
clearSearchHistoryButton.addTarget(self, action: #selector(SearchViewController.clearAction(_:)), for: .touchUpInside)
|
clearSearchHistoryButton.addTarget(self, action: #selector(SearchViewController.clearAction(_:)), for: .touchUpInside)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,15 +82,9 @@ extension SearchViewController {
|
||||||
// MARK: - UITableViewDelegate
|
// MARK: - UITableViewDelegate
|
||||||
|
|
||||||
extension SearchViewController: UITableViewDelegate {
|
extension SearchViewController: UITableViewDelegate {
|
||||||
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
|
||||||
66
|
|
||||||
}
|
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
|
||||||
66
|
|
||||||
}
|
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
tableView.deselectRow(at: indexPath, animated: true)
|
||||||
guard let diffableDataSource = viewModel.searchResultDiffableDataSource else { return }
|
guard let diffableDataSource = viewModel.searchResultDiffableDataSource else { return }
|
||||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
viewModel.searchResultItemDidSelected(item: item, from: self)
|
viewModel.searchResultItemDidSelected(item: item, from: self)
|
||||||
|
|
|
@ -82,7 +82,7 @@ final class SearchViewController: UIViewController, NeedsDependency {
|
||||||
// searching
|
// searching
|
||||||
let searchingTableView: UITableView = {
|
let searchingTableView: UITableView = {
|
||||||
let tableView = UITableView()
|
let tableView = UITableView()
|
||||||
tableView.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
tableView.backgroundColor = Asset.Colors.Background.systemBackground.color
|
||||||
tableView.rowHeight = UITableView.automaticDimension
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
tableView.separatorStyle = .singleLine
|
tableView.separatorStyle = .singleLine
|
||||||
tableView.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
|
tableView.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
|
||||||
|
@ -135,14 +135,16 @@ extension SearchViewController {
|
||||||
func setupSearchBar() {
|
func setupSearchBar() {
|
||||||
searchBar.delegate = self
|
searchBar.delegate = self
|
||||||
view.addSubview(searchBar)
|
view.addSubview(searchBar)
|
||||||
searchBar.constrain([
|
searchBar.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
searchBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
searchBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
||||||
searchBar.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
searchBar.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||||
searchBar.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
searchBar.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||||
])
|
])
|
||||||
view.addSubview(statusBar)
|
|
||||||
|
|
||||||
statusBar.constrain([
|
statusBar.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(statusBar)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
statusBar.topAnchor.constraint(equalTo: view.topAnchor),
|
statusBar.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
statusBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
statusBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
statusBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
statusBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
@ -151,8 +153,9 @@ extension SearchViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupScrollView() {
|
func setupScrollView() {
|
||||||
|
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
view.addSubview(scrollView)
|
view.addSubview(scrollView)
|
||||||
scrollView.constrain([
|
NSLayoutConstraint.activate([
|
||||||
scrollView.frameLayoutGuide.topAnchor.constraint(equalTo: searchBar.bottomAnchor),
|
scrollView.frameLayoutGuide.topAnchor.constraint(equalTo: searchBar.bottomAnchor),
|
||||||
scrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
scrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||||
scrollView.frameLayoutGuide.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
scrollView.frameLayoutGuide.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||||
|
@ -160,8 +163,9 @@ extension SearchViewController {
|
||||||
scrollView.contentLayoutGuide.widthAnchor.constraint(equalTo: view.widthAnchor),
|
scrollView.contentLayoutGuide.widthAnchor.constraint(equalTo: view.widthAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
scrollView.addSubview(stackView)
|
scrollView.addSubview(stackView)
|
||||||
stackView.constrain([
|
NSLayoutConstraint.activate([
|
||||||
stackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
|
stackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
|
||||||
stackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
|
stackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
|
||||||
stackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
|
stackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
|
||||||
|
@ -227,7 +231,7 @@ extension SearchViewController: UISearchBarDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SearchViewController: LoadMoreConfigurableTableViewContainer {
|
extension SearchViewController: LoadMoreConfigurableTableViewContainer {
|
||||||
typealias BottomLoaderTableViewCell = SearchBottomLoader
|
typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell
|
||||||
typealias LoadingState = SearchViewModel.LoadOldestState.Loading
|
typealias LoadingState = SearchViewModel.LoadOldestState.Loading
|
||||||
var loadMoreConfigurableTableView: UITableView { searchingTableView }
|
var loadMoreConfigurableTableView: UITableView { searchingTableView }
|
||||||
var loadMoreConfigurableStateMachine: GKStateMachine { viewModel.loadoldestStateMachine }
|
var loadMoreConfigurableStateMachine: GKStateMachine { viewModel.loadoldestStateMachine }
|
||||||
|
|
|
@ -136,9 +136,11 @@ extension SearchViewModel.LoadOldestState {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var snapshot = diffableDataSource.snapshot()
|
DispatchQueue.main.async {
|
||||||
snapshot.deleteItems([.bottomLoader])
|
var snapshot = diffableDataSource.snapshot()
|
||||||
diffableDataSource.apply(snapshot)
|
snapshot.deleteItems([.bottomLoader])
|
||||||
|
diffableDataSource.apply(snapshot)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -237,10 +237,10 @@ final class SearchViewModel: NSObject {
|
||||||
.sink { completion in
|
.sink { completion in
|
||||||
switch completion {
|
switch completion {
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: recommendHashTags request fail: %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: recommendAccount request fail: %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription)
|
||||||
promise(.failure(error))
|
promise(.failure(error))
|
||||||
case .finished:
|
case .finished:
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: recommendHashTags request success", (#file as NSString).lastPathComponent, #line, #function)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: recommendAccount request success", (#file as NSString).lastPathComponent, #line, #function)
|
||||||
promise(.success(()))
|
promise(.success(()))
|
||||||
}
|
}
|
||||||
} receiveValue: { [weak self] accounts in
|
} receiveValue: { [weak self] accounts in
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
//
|
|
||||||
// SearchBottomLoader.swift
|
|
||||||
// Mastodon
|
|
||||||
//
|
|
||||||
// Created by sxiaojian on 2021/4/6.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
final class SearchBottomLoader: UITableViewCell {
|
|
||||||
let activityIndicatorView: UIActivityIndicatorView = {
|
|
||||||
let activityIndicatorView = UIActivityIndicatorView(style: .medium)
|
|
||||||
activityIndicatorView.tintColor = Asset.Colors.Label.primary.color
|
|
||||||
activityIndicatorView.hidesWhenStopped = true
|
|
||||||
return activityIndicatorView
|
|
||||||
}()
|
|
||||||
|
|
||||||
override func prepareForReuse() {
|
|
||||||
super.prepareForReuse()
|
|
||||||
}
|
|
||||||
|
|
||||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
|
||||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
|
||||||
_init()
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
super.init(coder: coder)
|
|
||||||
_init()
|
|
||||||
}
|
|
||||||
|
|
||||||
func startAnimating() {
|
|
||||||
activityIndicatorView.startAnimating()
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopAnimating() {
|
|
||||||
activityIndicatorView.stopAnimating()
|
|
||||||
}
|
|
||||||
|
|
||||||
func _init() {
|
|
||||||
selectionStyle = .none
|
|
||||||
backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
|
||||||
contentView.addSubview(activityIndicatorView)
|
|
||||||
activityIndicatorView.constrainToCenter()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -55,19 +55,40 @@ final class SearchingTableViewCell: UITableViewCell {
|
||||||
extension SearchingTableViewCell {
|
extension SearchingTableViewCell {
|
||||||
private func configure() {
|
private func configure() {
|
||||||
backgroundColor = .clear
|
backgroundColor = .clear
|
||||||
selectionStyle = .none
|
|
||||||
contentView.addSubview(_imageView)
|
let containerStackView = UIStackView()
|
||||||
_imageView.pin(toSize: CGSize(width: 42, height: 42))
|
containerStackView.axis = .horizontal
|
||||||
_imageView.constrain([
|
containerStackView.distribution = .fill
|
||||||
_imageView.constraint(.leading, toView: contentView, constant: 21),
|
containerStackView.spacing = 12
|
||||||
_imageView.constraint(.centerY, toView: contentView)
|
containerStackView.layoutMargins = UIEdgeInsets(top: 12, left: 21, bottom: 12, right: 12)
|
||||||
|
containerStackView.isLayoutMarginsRelativeArrangement = true
|
||||||
|
containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(containerStackView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||||
|
containerStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||||
|
containerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||||
|
containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
|
||||||
])
|
])
|
||||||
|
|
||||||
contentView.addSubview(_titleLabel)
|
_imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
_titleLabel.pin(top: 12, left: 75, bottom: nil, right: 0)
|
containerStackView.addArrangedSubview(_imageView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
_imageView.widthAnchor.constraint(equalToConstant: 42),
|
||||||
|
_imageView.heightAnchor.constraint(equalToConstant: 42),
|
||||||
|
])
|
||||||
|
|
||||||
contentView.addSubview(_subTitleLabel)
|
let textStackView = UIStackView()
|
||||||
_subTitleLabel.pin(top: 34, left: 75, bottom: nil, right: 0)
|
textStackView.axis = .vertical
|
||||||
|
textStackView.distribution = .fill
|
||||||
|
textStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
_titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
textStackView.addArrangedSubview(_titleLabel)
|
||||||
|
_subTitleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
textStackView.addArrangedSubview(_subTitleLabel)
|
||||||
|
_subTitleLabel.setContentHuggingPriority(.defaultLow - 1, for: .vertical)
|
||||||
|
|
||||||
|
containerStackView.addArrangedSubview(textStackView)
|
||||||
}
|
}
|
||||||
|
|
||||||
func config(with account: Mastodon.Entity.Account) {
|
func config(with account: Mastodon.Entity.Account) {
|
||||||
|
|
|
@ -47,14 +47,35 @@ extension SearchRecommendCollectionHeader {
|
||||||
private func configure() {
|
private func configure() {
|
||||||
backgroundColor = .clear
|
backgroundColor = .clear
|
||||||
translatesAutoresizingMaskIntoConstraints = false
|
translatesAutoresizingMaskIntoConstraints = false
|
||||||
addSubview(titleLabel)
|
|
||||||
titleLabel.pinTopLeft(top: 31, left: 16)
|
|
||||||
|
|
||||||
addSubview(descriptionLabel)
|
let containerStackView = UIStackView()
|
||||||
descriptionLabel.constrain(toSuperviewEdges: UIEdgeInsets(top: 60, left: 16, bottom: 16, right: 16))
|
containerStackView.axis = .vertical
|
||||||
|
containerStackView.layoutMargins = UIEdgeInsets(top: 31, left: 16, bottom: 16, right: 16)
|
||||||
|
containerStackView.isLayoutMarginsRelativeArrangement = true
|
||||||
|
containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(containerStackView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
containerStackView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
containerStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
containerStackView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
containerStackView.trailingAnchor.constraint(equalTo: trailingAnchor)
|
||||||
|
])
|
||||||
|
|
||||||
addSubview(seeAllButton)
|
let horizontalStackView = UIStackView()
|
||||||
seeAllButton.pinTopRight(top: 26, right: 16)
|
horizontalStackView.axis = .horizontal
|
||||||
|
horizontalStackView.alignment = .center
|
||||||
|
horizontalStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
horizontalStackView.distribution = .fill
|
||||||
|
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
titleLabel.setContentHuggingPriority(.defaultLow - 1, for: .horizontal)
|
||||||
|
horizontalStackView.addArrangedSubview(titleLabel)
|
||||||
|
seeAllButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
horizontalStackView.addArrangedSubview(seeAllButton)
|
||||||
|
|
||||||
|
containerStackView.addArrangedSubview(horizontalStackView)
|
||||||
|
descriptionLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
containerStackView.addArrangedSubview(descriptionLabel)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,488 @@
|
||||||
|
//
|
||||||
|
// SettingsViewController.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by ihugo on 2021/4/7.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import ActiveLabel
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import AlamofireImage
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
|
// iTODO: when to ask permission to Use Notifications
|
||||||
|
|
||||||
|
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: nil,
|
||||||
|
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)
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
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: { [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
|
||||||
|
}
|
||||||
|
|
||||||
|
func alertToSignout() {
|
||||||
|
let alertController = UIAlertController(
|
||||||
|
title: L10n.Common.Alerts.SignOut.title,
|
||||||
|
message: L10n.Common.Alerts.SignOut.message,
|
||||||
|
preferredStyle: .alert
|
||||||
|
)
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
alertController.addAction(cancelAction)
|
||||||
|
alertController.addAction(signOutAction)
|
||||||
|
self.coordinator.present(
|
||||||
|
scene: .alertController(alertController: alertController),
|
||||||
|
from: self,
|
||||||
|
transition: .alertController(animated: true, completion: nil)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s:", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
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]
|
||||||
|
|
||||||
|
switch item {
|
||||||
|
case .boringZone:
|
||||||
|
guard let url = viewModel.privacyURL else { break }
|
||||||
|
coordinator.present(
|
||||||
|
scene: .safari(url: url),
|
||||||
|
from: self,
|
||||||
|
transition: .safariPresent(animated: true, completion: nil)
|
||||||
|
)
|
||||||
|
case .spicyZone(let link):
|
||||||
|
// clear media cache
|
||||||
|
if link.title == L10n.Scene.Settings.Section.Spicyzone.clear {
|
||||||
|
// clean image cache for AlamofireImage
|
||||||
|
let diskBytes = ImageDownloader.defaultURLCache().currentDiskUsage
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: diskBytes %d", ((#file as NSString).lastPathComponent), #line, #function, diskBytes)
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: clean image cache", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
// logout
|
||||||
|
if link.title == L10n.Scene.Settings.Section.Spicyzone.signout {
|
||||||
|
alertToSignout()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 }
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SettingsViewController: SettingsAppearanceTableViewCellDelegate {
|
||||||
|
func settingsAppearanceCell(_ view: SettingsAppearanceTableViewCell, didSelect: SettingsItem.AppearanceMode) {
|
||||||
|
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: .safari(url: URL(string: "https://github.com/tootsuite/mastodon")!),
|
||||||
|
from: self,
|
||||||
|
transition: .safariPresent(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, 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
|
||||||
|
|
||||||
|
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,393 @@
|
||||||
|
//
|
||||||
|
// 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>()
|
||||||
|
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)
|
||||||
|
|
||||||
|
/// create a subscription when:
|
||||||
|
/// - does not has one
|
||||||
|
/// - does not find subscription for selected trigger when change trigger
|
||||||
|
let createSubscriptionSubject = PassthroughSubject<(triggerBy: String, values: [Bool?]), Never>()
|
||||||
|
|
||||||
|
/// update a subscription when:
|
||||||
|
/// - 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
self.context = context
|
||||||
|
self.coordinator = coordinator
|
||||||
|
|
||||||
|
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(
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
.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 {
|
||||||
|
|
||||||
|
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(title: 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,205 @@
|
||||||
|
//
|
||||||
|
// 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.Label.secondary.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.Label.highlight.color
|
||||||
|
} else {
|
||||||
|
checkBox.imageView?.tintColor = Asset.Colors.Label.secondary.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
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let delegate = self.delegate else { return }
|
||||||
|
delegate.settingsAppearanceCell(self, didSelect: appearance)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,63 @@
|
||||||
|
//
|
||||||
|
// 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)
|
||||||
|
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
|
||||||
|
accessoryView = switchButton
|
||||||
|
|
||||||
|
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,64 @@
|
||||||
|
//
|
||||||
|
// SettingsSectionHeader.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by ihugo on 2021/4/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
struct GroupedTableViewConstraints {
|
||||||
|
static let topMargin: CGFloat = 40
|
||||||
|
static let bottomMargin: CGFloat = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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: GroupedTableViewConstraints.topMargin,
|
||||||
|
left: 0,
|
||||||
|
bottom: GroupedTableViewConstraints.bottomMargin,
|
||||||
|
right: 0
|
||||||
|
)
|
||||||
|
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.readableContentGuide.leadingAnchor),
|
||||||
|
stackView.trailingAnchor.constraint(lessThanOrEqualTo: self.readableContentGuide.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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -47,7 +47,7 @@ extension StatusTableViewCellDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class StatusTableViewCell: UITableViewCell {
|
final class StatusTableViewCell: UITableViewCell, StatusCell {
|
||||||
|
|
||||||
static let bottomPaddingHeight: CGFloat = 10
|
static let bottomPaddingHeight: CGFloat = 10
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ import UIKit
|
||||||
|
|
||||||
class AudioContainerViewModel {
|
class AudioContainerViewModel {
|
||||||
static func configure(
|
static func configure(
|
||||||
cell: StatusTableViewCell,
|
cell: StatusCell,
|
||||||
audioAttachment: Attachment,
|
audioAttachment: Attachment,
|
||||||
audioService: AudioPlaybackService
|
audioService: AudioPlaybackService
|
||||||
) {
|
) {
|
||||||
|
@ -51,7 +51,7 @@ class AudioContainerViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
static func observePlayer(
|
static func observePlayer(
|
||||||
cell: StatusTableViewCell,
|
cell: StatusCell,
|
||||||
audioAttachment: Attachment,
|
audioAttachment: Attachment,
|
||||||
audioService: AudioPlaybackService
|
audioService: AudioPlaybackService
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
//
|
||||||
|
// APIService+Notification.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/4/13.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import Foundation
|
||||||
|
import MastodonSDK
|
||||||
|
import OSLog
|
||||||
|
|
||||||
|
extension APIService {
|
||||||
|
func allNotifications(
|
||||||
|
domain: String,
|
||||||
|
query: Mastodon.API.Notifications.Query,
|
||||||
|
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Notification]>, Error> {
|
||||||
|
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||||
|
let userID = mastodonAuthenticationBox.userID
|
||||||
|
return Mastodon.API.Notifications.getNotifications(
|
||||||
|
session: session,
|
||||||
|
domain: domain,
|
||||||
|
query: query,
|
||||||
|
authorization: authorization
|
||||||
|
)
|
||||||
|
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Notification]>, Error> in
|
||||||
|
let log = OSLog.api
|
||||||
|
return self.backgroundManagedObjectContext.performChanges {
|
||||||
|
response.value.forEach { notification in
|
||||||
|
let (mastodonUser, _) = APIService.CoreData.createOrMergeMastodonUser(into: self.backgroundManagedObjectContext, for: nil, in: domain, entity: notification.account, userCache: nil, networkDate: Date(), log: log)
|
||||||
|
var status: Status?
|
||||||
|
if let statusEntity = notification.status {
|
||||||
|
let (statusInCoreData, _, _) = APIService.CoreData.createOrMergeStatus(
|
||||||
|
into: self.backgroundManagedObjectContext,
|
||||||
|
for: nil,
|
||||||
|
domain: domain,
|
||||||
|
entity: statusEntity,
|
||||||
|
statusCache: nil,
|
||||||
|
userCache: nil,
|
||||||
|
networkDate: Date(),
|
||||||
|
log: log
|
||||||
|
)
|
||||||
|
status = statusInCoreData
|
||||||
|
}
|
||||||
|
// use constrain to avoid repeated save
|
||||||
|
let property = MastodonNotification.Property(id: notification.id, typeRaw: notification.type.rawValue, account: mastodonUser, status: status, createdAt: notification.createdAt)
|
||||||
|
let notification = MastodonNotification.insert(into: self.backgroundManagedObjectContext, domain: domain, userID: userID, networkDate: response.networkDate, property: property)
|
||||||
|
os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: fetch mastodon user [%s](%s)", (#file as NSString).lastPathComponent, #line, #function, notification.typeRaw, notification.account.username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setFailureType(to: Error.self)
|
||||||
|
.tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Notification]> in
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
return response
|
||||||
|
case .failure(let error):
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,163 @@
|
||||||
|
//
|
||||||
|
// APIService+Settings.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by ihugo on 2021/4/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import Foundation
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
extension APIService {
|
||||||
|
|
||||||
|
func subscription(
|
||||||
|
domain: String,
|
||||||
|
userID: String,
|
||||||
|
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Subscription>, Error> {
|
||||||
|
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||||
|
|
||||||
|
let findSettings: Setting? = {
|
||||||
|
let request = Setting.sortedFetchRequest
|
||||||
|
request.predicate = Setting.predicate(domain: domain, userID: userID)
|
||||||
|
request.fetchLimit = 1
|
||||||
|
request.returnsObjectsAsFaults = false
|
||||||
|
do {
|
||||||
|
return try self.backgroundManagedObjectContext.fetch(request).first
|
||||||
|
} catch {
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
let triggerBy = findSettings?.triggerBy ?? "anyone"
|
||||||
|
let setting = self.createSettingIfNeed(
|
||||||
|
domain: domain,
|
||||||
|
userId: userID,
|
||||||
|
triggerBy: triggerBy
|
||||||
|
)
|
||||||
|
return Mastodon.API.Subscriptions.subscription(
|
||||||
|
session: session,
|
||||||
|
domain: domain,
|
||||||
|
authorization: authorization
|
||||||
|
)
|
||||||
|
.flatMap { response -> AnyPublisher<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 changeSubscription(
|
||||||
|
domain: String,
|
||||||
|
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 setting = self.createSettingIfNeed(domain: domain,
|
||||||
|
userId: userID,
|
||||||
|
triggerBy: triggerBy)
|
||||||
|
return Mastodon.API.Subscriptions.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,
|
||||||
|
setting: setting
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.setFailureType(to: Error.self)
|
||||||
|
.map { _ in return response }
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}.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> {
|
||||||
|
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||||
|
|
||||||
|
let setting = self.createSettingIfNeed(domain: domain,
|
||||||
|
userId: userID,
|
||||||
|
triggerBy: triggerBy)
|
||||||
|
|
||||||
|
return Mastodon.API.Subscriptions.updateSubscription(
|
||||||
|
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,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
//
|
||||||
|
// 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,
|
||||||
|
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) {
|
||||||
|
let oldSubscription: Subscription? = {
|
||||||
|
let request = Subscription.sortedFetchRequest
|
||||||
|
request.predicate = Subscription.predicate(type: triggerBy)
|
||||||
|
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
|
||||||
|
)
|
||||||
|
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 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.setup()
|
||||||
sceneCoordinator.setupOnboardingIfNeeds(animated: false)
|
sceneCoordinator.setupOnboardingIfNeeds(animated: false)
|
||||||
window.makeKeyAndVisible()
|
window.makeKeyAndVisible()
|
||||||
|
|
||||||
|
// update `overrideUserInterfaceStyle` with current setting
|
||||||
|
SettingsViewController.updateOverrideUserInterfaceStyle(window: window)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sceneDidDisconnect(_ scene: UIScene) {
|
func sceneDidDisconnect(_ scene: UIScene) {
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
//
|
//
|
||||||
// File.swift
|
// File.swift
|
||||||
//
|
//
|
||||||
//
|
//
|
||||||
// Created by BradGao on 2021/4/1.
|
// Created by BradGao on 2021/4/1.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Combine
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
|
||||||
extension Mastodon.API.Notifications {
|
extension Mastodon.API.Notifications {
|
||||||
static func notificationsEndpointURL(domain: String) -> URL {
|
internal static func notificationsEndpointURL(domain: String) -> URL {
|
||||||
Mastodon.API.endpointV2URL(domain: domain).appendingPathComponent("notifications")
|
Mastodon.API.endpointURL(domain: domain).appendingPathComponent("notifications")
|
||||||
}
|
}
|
||||||
static func getNotificationEndpointURL(domain: String, notificationID: String) -> URL {
|
|
||||||
|
internal static func getNotificationEndpointURL(domain: String, notificationID: String) -> URL {
|
||||||
notificationsEndpointURL(domain: domain).appendingPathComponent(notificationID)
|
notificationsEndpointURL(domain: domain).appendingPathComponent(notificationID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,15 +28,15 @@ extension Mastodon.API.Notifications {
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - session: `URLSession`
|
/// - session: `URLSession`
|
||||||
/// - domain: Mastodon instance domain. e.g. "example.com"
|
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||||
/// - query: `GetAllNotificationsQuery` with query parameters
|
/// - query: `NotificationsQuery` with query parameters
|
||||||
/// - authorization: User token
|
/// - authorization: User token
|
||||||
/// - Returns: `AnyPublisher` contains `Token` nested in the response
|
/// - Returns: `AnyPublisher` contains `Token` nested in the response
|
||||||
public static func getAll(
|
public static func getNotifications(
|
||||||
session: URLSession,
|
session: URLSession,
|
||||||
domain: String,
|
domain: String,
|
||||||
query: GetAllNotificationsQuery,
|
query: Mastodon.API.Notifications.Query,
|
||||||
authorization: Mastodon.API.OAuth.Authorization?
|
authorization: Mastodon.API.OAuth.Authorization
|
||||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Notification]>, Error> {
|
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Notification]>, Error> {
|
||||||
let request = Mastodon.API.get(
|
let request = Mastodon.API.get(
|
||||||
url: notificationsEndpointURL(domain: domain),
|
url: notificationsEndpointURL(domain: domain),
|
||||||
query: query,
|
query: query,
|
||||||
|
@ -63,12 +64,12 @@ extension Mastodon.API.Notifications {
|
||||||
/// - notificationID: ID of the notification.
|
/// - notificationID: ID of the notification.
|
||||||
/// - authorization: User token
|
/// - authorization: User token
|
||||||
/// - Returns: `AnyPublisher` contains `Token` nested in the response
|
/// - Returns: `AnyPublisher` contains `Token` nested in the response
|
||||||
public static func get(
|
public static func getNotification(
|
||||||
session: URLSession,
|
session: URLSession,
|
||||||
domain: String,
|
domain: String,
|
||||||
notificationID: String,
|
notificationID: String,
|
||||||
authorization: Mastodon.API.OAuth.Authorization?
|
authorization: Mastodon.API.OAuth.Authorization
|
||||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Notification>, Error> {
|
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Notification>, Error> {
|
||||||
let request = Mastodon.API.get(
|
let request = Mastodon.API.get(
|
||||||
url: getNotificationEndpointURL(domain: domain, notificationID: notificationID),
|
url: getNotificationEndpointURL(domain: domain, notificationID: notificationID),
|
||||||
query: nil,
|
query: nil,
|
||||||
|
@ -81,13 +82,15 @@ extension Mastodon.API.Notifications {
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
public struct GetAllNotificationsQuery: Codable, PagedQueryType, GetQuery {
|
|
||||||
|
extension Mastodon.API.Notifications {
|
||||||
|
public struct Query: PagedQueryType, GetQuery {
|
||||||
public let maxID: Mastodon.Entity.Status.ID?
|
public let maxID: Mastodon.Entity.Status.ID?
|
||||||
public let sinceID: Mastodon.Entity.Status.ID?
|
public let sinceID: Mastodon.Entity.Status.ID?
|
||||||
public let minID: Mastodon.Entity.Status.ID?
|
public let minID: Mastodon.Entity.Status.ID?
|
||||||
public let limit: Int?
|
public let limit: Int?
|
||||||
public let excludeTypes: [String]?
|
public let excludeTypes: [Mastodon.Entity.Notification.NotificationType]?
|
||||||
public let accountID: String?
|
public let accountID: String?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
|
@ -95,7 +98,7 @@ extension Mastodon.API.Notifications {
|
||||||
sinceID: Mastodon.Entity.Status.ID? = nil,
|
sinceID: Mastodon.Entity.Status.ID? = nil,
|
||||||
minID: Mastodon.Entity.Status.ID? = nil,
|
minID: Mastodon.Entity.Status.ID? = nil,
|
||||||
limit: Int? = nil,
|
limit: Int? = nil,
|
||||||
excludeTypes: [String]? = nil,
|
excludeTypes: [Mastodon.Entity.Notification.NotificationType]? = nil,
|
||||||
accountID: String? = nil
|
accountID: String? = nil
|
||||||
) {
|
) {
|
||||||
self.maxID = maxID
|
self.maxID = maxID
|
||||||
|
@ -114,7 +117,7 @@ extension Mastodon.API.Notifications {
|
||||||
limit.flatMap { items.append(URLQueryItem(name: "limit", value: String($0))) }
|
limit.flatMap { items.append(URLQueryItem(name: "limit", value: String($0))) }
|
||||||
if let excludeTypes = excludeTypes {
|
if let excludeTypes = excludeTypes {
|
||||||
excludeTypes.forEach {
|
excludeTypes.forEach {
|
||||||
items.append(URLQueryItem(name: "exclude_types[]", value: $0))
|
items.append(URLQueryItem(name: "exclude_types[]", value: $0.rawValue))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
accountID.flatMap { items.append(URLQueryItem(name: "account_id", value: $0)) }
|
accountID.flatMap { items.append(URLQueryItem(name: "account_id", value: $0)) }
|
||||||
|
|
|
@ -0,0 +1,226 @@
|
||||||
|
//
|
||||||
|
// File.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by ihugo on 2021/4/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
extension Mastodon.API.Subscriptions {
|
||||||
|
|
||||||
|
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 `Subscription` 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribe to push notifications
|
||||||
|
///
|
||||||
|
/// Add a Web Push API subscription to receive notifications. Each access token can have one push subscription. If you create a new subscription, the old subscription is deleted.
|
||||||
|
///
|
||||||
|
/// - 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 `Subscription` 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change types of notifications
|
||||||
|
///
|
||||||
|
/// Updates the current push subscription. Only the data part can be updated. To change fundamentals, a new subscription must be created instead.
|
||||||
|
///
|
||||||
|
/// - 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 `Subscription` nested in the response
|
||||||
|
public static func updateSubscription(
|
||||||
|
session: URLSession,
|
||||||
|
domain: String,
|
||||||
|
authorization: Mastodon.API.OAuth.Authorization,
|
||||||
|
query: UpdateSubscriptionQuery
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Subscription>, Error> {
|
||||||
|
let request = Mastodon.API.put(
|
||||||
|
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.Subscriptions {
|
||||||
|
public struct CreateSubscriptionQuery: Codable, PostQuery {
|
||||||
|
let endpoint: String
|
||||||
|
let p256dh: String
|
||||||
|
let auth: String
|
||||||
|
let favourite: Bool?
|
||||||
|
let follow: Bool?
|
||||||
|
let reblog: Bool?
|
||||||
|
let mention: Bool?
|
||||||
|
let poll: Bool?
|
||||||
|
|
||||||
|
var queryItems: [URLQueryItem]? {
|
||||||
|
var items = [URLQueryItem]()
|
||||||
|
|
||||||
|
items.append(URLQueryItem(name: "subscription[endpoint]", value: endpoint))
|
||||||
|
items.append(URLQueryItem(name: "subscription[keys][p256dh]", value: p256dh))
|
||||||
|
items.append(URLQueryItem(name: "subscription[keys][auth]", value: auth))
|
||||||
|
|
||||||
|
if let followValue = follow?.queryItemValue {
|
||||||
|
let followItem = URLQueryItem(name: "data[alerts][follow]", value: followValue)
|
||||||
|
items.append(followItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let favouriteValue = favourite?.queryItemValue {
|
||||||
|
let favouriteItem = URLQueryItem(name: "data[alerts][favourite]", value: favouriteValue)
|
||||||
|
items.append(favouriteItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let reblogValue = reblog?.queryItemValue {
|
||||||
|
let reblogItem = URLQueryItem(name: "data[alerts][reblog]", value: reblogValue)
|
||||||
|
items.append(reblogItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let mentionValue = mention?.queryItemValue {
|
||||||
|
let mentionItem = URLQueryItem(name: "data[alerts][mention]", value: mentionValue)
|
||||||
|
items.append(mentionItem)
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(
|
||||||
|
endpoint: String,
|
||||||
|
p256dh: String,
|
||||||
|
auth: String,
|
||||||
|
favourite: Bool?,
|
||||||
|
follow: Bool?,
|
||||||
|
reblog: Bool?,
|
||||||
|
mention: Bool?,
|
||||||
|
poll: Bool?
|
||||||
|
) {
|
||||||
|
self.endpoint = endpoint
|
||||||
|
self.p256dh = p256dh
|
||||||
|
self.auth = auth
|
||||||
|
self.favourite = favourite
|
||||||
|
self.follow = follow
|
||||||
|
self.reblog = reblog
|
||||||
|
self.mention = mention
|
||||||
|
self.poll = poll
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct UpdateSubscriptionQuery: Codable, PutQuery {
|
||||||
|
let favourite: Bool?
|
||||||
|
let follow: Bool?
|
||||||
|
let reblog: Bool?
|
||||||
|
let mention: Bool?
|
||||||
|
let poll: Bool?
|
||||||
|
|
||||||
|
var queryItems: [URLQueryItem]? {
|
||||||
|
var items = [URLQueryItem]()
|
||||||
|
|
||||||
|
if let followValue = follow?.queryItemValue {
|
||||||
|
let followItem = URLQueryItem(name: "data[alerts][follow]", value: followValue)
|
||||||
|
items.append(followItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let favouriteValue = favourite?.queryItemValue {
|
||||||
|
let favouriteItem = URLQueryItem(name: "data[alerts][favourite]", value: favouriteValue)
|
||||||
|
items.append(favouriteItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let reblogValue = reblog?.queryItemValue {
|
||||||
|
let reblogItem = URLQueryItem(name: "data[alerts][reblog]", value: reblogValue)
|
||||||
|
items.append(reblogItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let mentionValue = mention?.queryItemValue {
|
||||||
|
let mentionItem = URLQueryItem(name: "data[alerts][mention]", value: mentionValue)
|
||||||
|
items.append(mentionItem)
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(
|
||||||
|
favourite: Bool?,
|
||||||
|
follow: Bool?,
|
||||||
|
reblog: Bool?,
|
||||||
|
mention: Bool?,
|
||||||
|
poll: Bool?
|
||||||
|
) {
|
||||||
|
self.favourite = favourite
|
||||||
|
self.follow = follow
|
||||||
|
self.reblog = reblog
|
||||||
|
self.mention = mention
|
||||||
|
self.poll = poll
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -115,6 +115,7 @@ extension Mastodon.API {
|
||||||
public enum Trends { }
|
public enum Trends { }
|
||||||
public enum Suggestions { }
|
public enum Suggestions { }
|
||||||
public enum Notifications { }
|
public enum Notifications { }
|
||||||
|
public enum Subscriptions { }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Mastodon.API {
|
extension Mastodon.API {
|
||||||
|
|
|
@ -37,6 +37,7 @@ extension Mastodon.Entity {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Mastodon.Entity.Notification {
|
extension Mastodon.Entity.Notification {
|
||||||
|
public typealias NotificationType = Type
|
||||||
public enum `Type`: RawRepresentable, Codable {
|
public enum `Type`: RawRepresentable, Codable {
|
||||||
case follow
|
case follow
|
||||||
case followRequest
|
case followRequest
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
//
|
||||||
|
// 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?
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
|
var id = try? container.decode(String.self, forKey: .id)
|
||||||
|
if nil == id, let numId = try? container.decode(Int.self, forKey: .id) {
|
||||||
|
id = String(numId)
|
||||||
|
}
|
||||||
|
self.id = id ?? ""
|
||||||
|
|
||||||
|
endpoint = try container.decode(String.self, forKey: .endpoint)
|
||||||
|
alerts = try container.decode(Alerts.self, forKey: .alerts)
|
||||||
|
serverKey = try container.decode(String.self, forKey: .serverKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue