forked from zelo72/mastodon-ios
Merge pull request #327 from mastodon/feature/v2-timeline
Update Timeline UI
This commit is contained in:
commit
c4c297a3de
|
@ -19,8 +19,8 @@ jobs:
|
|||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: force Xcode 13.1
|
||||
run: sudo xcode-select -switch /Applications/Xcode_13.1.app
|
||||
- name: force Xcode 13.2.1
|
||||
run: sudo xcode-select -switch /Applications/Xcode_13.2.1.app
|
||||
- name: setup
|
||||
run: exec ./.github/scripts/setup.sh
|
||||
- name: build
|
||||
|
|
|
@ -11,6 +11,10 @@ import CryptoKit
|
|||
import KeychainAccess
|
||||
import Keys
|
||||
|
||||
enum AppName {
|
||||
public static let groupID = "group.org.joinmastodon.app"
|
||||
}
|
||||
|
||||
public final class AppSecret {
|
||||
|
||||
public static let keychain = Keychain(service: "org.joinmastodon.app.keychain", accessGroup: AppName.groupID)
|
||||
|
|
|
@ -17,6 +17,6 @@
|
|||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.3.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>90</string>
|
||||
<string>96</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import MastodonCommon
|
||||
|
||||
extension UserDefaults {
|
||||
public static let shared = UserDefaults(suiteName: AppName.groupID)!
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
//
|
||||
// CoreDataStack.h
|
||||
// CoreDataStack
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/27.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
//! Project version number for CoreDataStack.
|
||||
FOUNDATION_EXPORT double CoreDataStackVersionNumber;
|
||||
|
||||
//! Project version string for CoreDataStack.
|
||||
FOUNDATION_EXPORT const unsigned char CoreDataStackVersionString[];
|
||||
|
||||
// In this header, you should import all the public headers of your framework using statements like #import <CoreDataStack/PublicHeader.h>
|
||||
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
//
|
||||
// Attachment.swift
|
||||
// CoreDataStack
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-2-23.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
public final class Attachment: NSManagedObject {
|
||||
public typealias ID = String
|
||||
|
||||
@NSManaged public private(set) var id: ID
|
||||
@NSManaged public private(set) var domain: String
|
||||
@NSManaged public private(set) var typeRaw: String
|
||||
@NSManaged public private(set) var url: String
|
||||
@NSManaged public private(set) var previewURL: String?
|
||||
|
||||
@NSManaged public private(set) var remoteURL: String?
|
||||
@NSManaged public private(set) var metaData: Data?
|
||||
@NSManaged public private(set) var textURL: String?
|
||||
@NSManaged public private(set) var descriptionString: String?
|
||||
@NSManaged public private(set) var blurhash: String?
|
||||
|
||||
@NSManaged public private(set) var createdAt: Date
|
||||
@NSManaged public private(set) var updatedAt: Date
|
||||
@NSManaged public private(set) var index: NSNumber
|
||||
|
||||
// many-to-one relationship
|
||||
@NSManaged public private(set) var status: Status?
|
||||
|
||||
}
|
||||
|
||||
public extension Attachment {
|
||||
|
||||
override func awakeFromInsert() {
|
||||
super.awakeFromInsert()
|
||||
setPrimitiveValue(Date(), forKey: #keyPath(Attachment.createdAt))
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
static func insert(
|
||||
into context: NSManagedObjectContext,
|
||||
property: Property
|
||||
) -> Attachment {
|
||||
let attachment: Attachment = context.insertObject()
|
||||
|
||||
attachment.domain = property.domain
|
||||
attachment.index = property.index
|
||||
|
||||
attachment.id = property.id
|
||||
attachment.typeRaw = property.typeRaw
|
||||
attachment.url = property.url
|
||||
attachment.previewURL = property.previewURL
|
||||
|
||||
attachment.remoteURL = property.remoteURL
|
||||
attachment.metaData = property.metaData
|
||||
attachment.textURL = property.textURL
|
||||
attachment.descriptionString = property.descriptionString
|
||||
attachment.blurhash = property.blurhash
|
||||
|
||||
attachment.updatedAt = property.networkDate
|
||||
|
||||
return attachment
|
||||
}
|
||||
|
||||
func didUpdate(at networkDate: Date) {
|
||||
self.updatedAt = networkDate
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public extension Attachment {
|
||||
struct Property {
|
||||
public let domain: String
|
||||
public let index: NSNumber
|
||||
|
||||
public let id: ID
|
||||
public let typeRaw: String
|
||||
public let url: String
|
||||
|
||||
public let previewURL: String?
|
||||
public let remoteURL: String?
|
||||
public let metaData: Data?
|
||||
public let textURL: String?
|
||||
public let descriptionString: String?
|
||||
public let blurhash: String?
|
||||
|
||||
public let networkDate: Date
|
||||
|
||||
public init(
|
||||
domain: String,
|
||||
index: Int,
|
||||
id: Attachment.ID,
|
||||
typeRaw: String,
|
||||
url: String,
|
||||
previewURL: String?,
|
||||
remoteURL: String?,
|
||||
metaData: Data?,
|
||||
textURL: String?,
|
||||
descriptionString: String?,
|
||||
blurhash: String?,
|
||||
networkDate: Date
|
||||
) {
|
||||
self.domain = domain
|
||||
self.index = NSNumber(value: index)
|
||||
self.id = id
|
||||
self.typeRaw = typeRaw
|
||||
self.url = url
|
||||
self.previewURL = previewURL
|
||||
self.remoteURL = remoteURL
|
||||
self.metaData = metaData
|
||||
self.textURL = textURL
|
||||
self.descriptionString = descriptionString
|
||||
self.blurhash = blurhash
|
||||
self.networkDate = networkDate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Attachment: Managed {
|
||||
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
||||
return [NSSortDescriptor(keyPath: \Attachment.createdAt, ascending: false)]
|
||||
}
|
||||
}
|
|
@ -1,102 +0,0 @@
|
|||
//
|
||||
// HomeTimelineIndex.swift
|
||||
// CoreDataStack
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/27.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
final public class HomeTimelineIndex: NSManagedObject {
|
||||
|
||||
public typealias ID = String
|
||||
@NSManaged public private(set) var identifier: ID
|
||||
@NSManaged public private(set) var domain: String
|
||||
@NSManaged public private(set) var userID: String
|
||||
|
||||
@NSManaged public private(set) var hasMore: Bool // default NO
|
||||
|
||||
@NSManaged public private(set) var createdAt: Date
|
||||
@NSManaged public private(set) var deletedAt: Date?
|
||||
|
||||
|
||||
// many-to-one relationship
|
||||
@NSManaged public private(set) var status: Status
|
||||
|
||||
}
|
||||
|
||||
extension HomeTimelineIndex {
|
||||
|
||||
@discardableResult
|
||||
public static func insert(
|
||||
into context: NSManagedObjectContext,
|
||||
property: Property,
|
||||
status: Status
|
||||
) -> HomeTimelineIndex {
|
||||
let index: HomeTimelineIndex = context.insertObject()
|
||||
|
||||
index.identifier = property.identifier
|
||||
index.domain = property.domain
|
||||
index.userID = property.userID
|
||||
index.createdAt = status.createdAt
|
||||
|
||||
index.status = status
|
||||
|
||||
return index
|
||||
}
|
||||
|
||||
public func update(hasMore: Bool) {
|
||||
if self.hasMore != hasMore {
|
||||
self.hasMore = hasMore
|
||||
}
|
||||
}
|
||||
|
||||
// internal method for status call
|
||||
func softDelete() {
|
||||
deletedAt = Date()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension HomeTimelineIndex {
|
||||
public struct Property {
|
||||
public let identifier: String
|
||||
public let domain: String
|
||||
public let userID: String
|
||||
|
||||
public init(domain: String, userID: String) {
|
||||
self.identifier = UUID().uuidString + "@" + domain
|
||||
self.domain = domain
|
||||
self.userID = userID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension HomeTimelineIndex: Managed {
|
||||
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
||||
return [NSSortDescriptor(keyPath: \HomeTimelineIndex.createdAt, ascending: false)]
|
||||
}
|
||||
}
|
||||
extension HomeTimelineIndex {
|
||||
|
||||
static func predicate(domain: String) -> NSPredicate {
|
||||
return NSPredicate(format: "%K == %@", #keyPath(HomeTimelineIndex.domain), domain)
|
||||
}
|
||||
|
||||
static func predicate(userID: MastodonUser.ID) -> NSPredicate {
|
||||
return NSPredicate(format: "%K == %@", #keyPath(HomeTimelineIndex.userID), userID)
|
||||
}
|
||||
|
||||
public static func predicate(domain: String, userID: MastodonUser.ID) -> NSPredicate {
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [
|
||||
predicate(domain: domain),
|
||||
predicate(userID: userID)
|
||||
])
|
||||
}
|
||||
|
||||
public static func notDeleted() -> NSPredicate {
|
||||
return NSPredicate(format: "%K == nil", #keyPath(HomeTimelineIndex.deletedAt))
|
||||
}
|
||||
|
||||
}
|
|
@ -1,407 +0,0 @@
|
|||
//
|
||||
// MastodonUser.swift
|
||||
// CoreDataStack
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/27.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
final public class MastodonUser: NSManagedObject {
|
||||
|
||||
public typealias ID = String
|
||||
|
||||
@NSManaged public private(set) var identifier: ID
|
||||
@NSManaged public private(set) var domain: String
|
||||
|
||||
@NSManaged public private(set) var id: ID
|
||||
@NSManaged public private(set) var acct: String
|
||||
@NSManaged public private(set) var username: String
|
||||
@NSManaged public private(set) var displayName: String
|
||||
@NSManaged public private(set) var avatar: String
|
||||
@NSManaged public private(set) var avatarStatic: String?
|
||||
@NSManaged public private(set) var header: String
|
||||
@NSManaged public private(set) var headerStatic: String?
|
||||
@NSManaged public private(set) var note: String?
|
||||
@NSManaged public private(set) var url: String?
|
||||
|
||||
@NSManaged public private(set) var emojisData: Data?
|
||||
@NSManaged public private(set) var fieldsData: Data?
|
||||
|
||||
@NSManaged public private(set) var statusesCount: NSNumber
|
||||
@NSManaged public private(set) var followingCount: NSNumber
|
||||
@NSManaged public private(set) var followersCount: NSNumber
|
||||
|
||||
@NSManaged public private(set) var locked: Bool
|
||||
@NSManaged public private(set) var bot: Bool
|
||||
@NSManaged public private(set) var suspended: Bool
|
||||
|
||||
@NSManaged public private(set) var createdAt: Date
|
||||
@NSManaged public private(set) var updatedAt: Date
|
||||
|
||||
// one-to-one relationship
|
||||
@NSManaged public private(set) var pinnedStatus: Status?
|
||||
@NSManaged public private(set) var mastodonAuthentication: MastodonAuthentication?
|
||||
|
||||
// one-to-many relationship
|
||||
@NSManaged public private(set) var statuses: Set<Status>?
|
||||
@NSManaged public private(set) var notifications: Set<MastodonNotification>?
|
||||
@NSManaged public private(set) var searchHistories: Set<SearchHistory>
|
||||
|
||||
// many-to-many relationship
|
||||
@NSManaged public private(set) var favourite: Set<Status>?
|
||||
@NSManaged public private(set) var reblogged: Set<Status>?
|
||||
@NSManaged public private(set) var muted: Set<Status>?
|
||||
@NSManaged public private(set) var bookmarked: Set<Status>?
|
||||
@NSManaged public private(set) var votePollOptions: Set<PollOption>?
|
||||
@NSManaged public private(set) var votePolls: Set<Poll>?
|
||||
// relationships
|
||||
@NSManaged public private(set) var following: Set<MastodonUser>?
|
||||
@NSManaged public private(set) var followingBy: Set<MastodonUser>?
|
||||
@NSManaged public private(set) var followRequested: Set<MastodonUser>?
|
||||
@NSManaged public private(set) var followRequestedBy: Set<MastodonUser>?
|
||||
@NSManaged public private(set) var muting: Set<MastodonUser>?
|
||||
@NSManaged public private(set) var mutingBy: Set<MastodonUser>?
|
||||
@NSManaged public private(set) var blocking: Set<MastodonUser>?
|
||||
@NSManaged public private(set) var blockingBy: Set<MastodonUser>?
|
||||
@NSManaged public private(set) var endorsed: Set<MastodonUser>?
|
||||
@NSManaged public private(set) var endorsedBy: Set<MastodonUser>?
|
||||
@NSManaged public private(set) var domainBlocking: Set<MastodonUser>?
|
||||
@NSManaged public private(set) var domainBlockingBy: Set<MastodonUser>?
|
||||
|
||||
}
|
||||
|
||||
extension MastodonUser {
|
||||
|
||||
@discardableResult
|
||||
public static func insert(
|
||||
into context: NSManagedObjectContext,
|
||||
property: Property
|
||||
) -> MastodonUser {
|
||||
let user: MastodonUser = context.insertObject()
|
||||
|
||||
user.identifier = property.identifier
|
||||
user.domain = property.domain
|
||||
|
||||
user.id = property.id
|
||||
user.acct = property.acct
|
||||
user.username = property.username
|
||||
user.displayName = property.displayName
|
||||
user.avatar = property.avatar
|
||||
user.avatarStatic = property.avatarStatic
|
||||
user.header = property.header
|
||||
user.headerStatic = property.headerStatic
|
||||
user.note = property.note
|
||||
user.url = property.url
|
||||
user.emojisData = property.emojisData
|
||||
user.fieldsData = property.fieldsData
|
||||
|
||||
user.statusesCount = NSNumber(value: property.statusesCount)
|
||||
user.followingCount = NSNumber(value: property.followingCount)
|
||||
user.followersCount = NSNumber(value: property.followersCount)
|
||||
|
||||
user.locked = property.locked
|
||||
user.bot = property.bot ?? false
|
||||
user.suspended = property.suspended ?? false
|
||||
|
||||
// Mastodon do not provide relationship on the `Account`
|
||||
// Update relationship via attribute updating interface
|
||||
|
||||
user.createdAt = property.createdAt
|
||||
user.updatedAt = property.networkDate
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
|
||||
public func update(acct: String) {
|
||||
if self.acct != acct {
|
||||
self.acct = acct
|
||||
}
|
||||
}
|
||||
public func update(username: String) {
|
||||
if self.username != username {
|
||||
self.username = username
|
||||
}
|
||||
}
|
||||
public func update(displayName: String) {
|
||||
if self.displayName != displayName {
|
||||
self.displayName = displayName
|
||||
}
|
||||
}
|
||||
public func update(avatar: String) {
|
||||
if self.avatar != avatar {
|
||||
self.avatar = avatar
|
||||
}
|
||||
}
|
||||
public func update(avatarStatic: String?) {
|
||||
if self.avatarStatic != avatarStatic {
|
||||
self.avatarStatic = avatarStatic
|
||||
}
|
||||
}
|
||||
public func update(header: String) {
|
||||
if self.header != header {
|
||||
self.header = header
|
||||
}
|
||||
}
|
||||
public func update(headerStatic: String?) {
|
||||
if self.headerStatic != headerStatic {
|
||||
self.headerStatic = headerStatic
|
||||
}
|
||||
}
|
||||
public func update(note: String?) {
|
||||
if self.note != note {
|
||||
self.note = note
|
||||
}
|
||||
}
|
||||
public func update(url: String?) {
|
||||
if self.url != url {
|
||||
self.url = url
|
||||
}
|
||||
}
|
||||
public func update(emojisData: Data?) {
|
||||
if self.emojisData != emojisData {
|
||||
self.emojisData = emojisData
|
||||
}
|
||||
}
|
||||
public func update(fieldsData: Data?) {
|
||||
if self.fieldsData != fieldsData {
|
||||
self.fieldsData = fieldsData
|
||||
}
|
||||
}
|
||||
public func update(statusesCount: Int) {
|
||||
if self.statusesCount.intValue != statusesCount {
|
||||
self.statusesCount = NSNumber(value: statusesCount)
|
||||
}
|
||||
}
|
||||
public func update(followingCount: Int) {
|
||||
if self.followingCount.intValue != followingCount {
|
||||
self.followingCount = NSNumber(value: followingCount)
|
||||
}
|
||||
}
|
||||
public func update(followersCount: Int) {
|
||||
if self.followersCount.intValue != followersCount {
|
||||
self.followersCount = NSNumber(value: followersCount)
|
||||
}
|
||||
}
|
||||
public func update(locked: Bool) {
|
||||
if self.locked != locked {
|
||||
self.locked = locked
|
||||
}
|
||||
}
|
||||
public func update(bot: Bool) {
|
||||
if self.bot != bot {
|
||||
self.bot = bot
|
||||
}
|
||||
}
|
||||
public func update(suspended: Bool) {
|
||||
if self.suspended != suspended {
|
||||
self.suspended = suspended
|
||||
}
|
||||
}
|
||||
|
||||
public func update(isFollowing: Bool, by mastodonUser: MastodonUser) {
|
||||
if isFollowing {
|
||||
if !(self.followingBy ?? Set()).contains(mastodonUser) {
|
||||
self.mutableSetValue(forKey: #keyPath(MastodonUser.followingBy)).add(mastodonUser)
|
||||
}
|
||||
} else {
|
||||
if (self.followingBy ?? Set()).contains(mastodonUser) {
|
||||
self.mutableSetValue(forKey: #keyPath(MastodonUser.followingBy)).remove(mastodonUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
public func update(isFollowRequested: Bool, by mastodonUser: MastodonUser) {
|
||||
if isFollowRequested {
|
||||
if !(self.followRequestedBy ?? Set()).contains(mastodonUser) {
|
||||
self.mutableSetValue(forKey: #keyPath(MastodonUser.followRequestedBy)).add(mastodonUser)
|
||||
}
|
||||
} else {
|
||||
if (self.followRequestedBy ?? Set()).contains(mastodonUser) {
|
||||
self.mutableSetValue(forKey: #keyPath(MastodonUser.followRequestedBy)).remove(mastodonUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
public func update(isMuting: Bool, by mastodonUser: MastodonUser) {
|
||||
if isMuting {
|
||||
if !(self.mutingBy ?? Set()).contains(mastodonUser) {
|
||||
self.mutableSetValue(forKey: #keyPath(MastodonUser.mutingBy)).add(mastodonUser)
|
||||
}
|
||||
} else {
|
||||
if (self.mutingBy ?? Set()).contains(mastodonUser) {
|
||||
self.mutableSetValue(forKey: #keyPath(MastodonUser.mutingBy)).remove(mastodonUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
public func update(isBlocking: Bool, by mastodonUser: MastodonUser) {
|
||||
if isBlocking {
|
||||
if !(self.blockingBy ?? Set()).contains(mastodonUser) {
|
||||
self.mutableSetValue(forKey: #keyPath(MastodonUser.blockingBy)).add(mastodonUser)
|
||||
}
|
||||
} else {
|
||||
if (self.blockingBy ?? Set()).contains(mastodonUser) {
|
||||
self.mutableSetValue(forKey: #keyPath(MastodonUser.blockingBy)).remove(mastodonUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
public func update(isEndorsed: Bool, by mastodonUser: MastodonUser) {
|
||||
if isEndorsed {
|
||||
if !(self.endorsedBy ?? Set()).contains(mastodonUser) {
|
||||
self.mutableSetValue(forKey: #keyPath(MastodonUser.endorsedBy)).add(mastodonUser)
|
||||
}
|
||||
} else {
|
||||
if (self.endorsedBy ?? Set()).contains(mastodonUser) {
|
||||
self.mutableSetValue(forKey: #keyPath(MastodonUser.endorsedBy)).remove(mastodonUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
public func update(isDomainBlocking: Bool, by mastodonUser: MastodonUser) {
|
||||
if isDomainBlocking {
|
||||
if !(self.domainBlockingBy ?? Set()).contains(mastodonUser) {
|
||||
self.mutableSetValue(forKey: #keyPath(MastodonUser.domainBlockingBy)).add(mastodonUser)
|
||||
}
|
||||
} else {
|
||||
if (self.domainBlockingBy ?? Set()).contains(mastodonUser) {
|
||||
self.mutableSetValue(forKey: #keyPath(MastodonUser.domainBlockingBy)).remove(mastodonUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func didUpdate(at networkDate: Date) {
|
||||
self.updatedAt = networkDate
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MastodonUser {
|
||||
public func findSearchHistory(domain: String, userID: MastodonUser.ID) -> SearchHistory? {
|
||||
return searchHistories.first { searchHistory in
|
||||
return searchHistory.domain == domain
|
||||
&& searchHistory.userID == userID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MastodonUser {
|
||||
public struct Property {
|
||||
public let identifier: String
|
||||
public let domain: String
|
||||
|
||||
public let id: String
|
||||
public let acct: String
|
||||
public let username: String
|
||||
public let displayName: String
|
||||
public let avatar: String
|
||||
public let avatarStatic: String?
|
||||
public let header: String
|
||||
public let headerStatic: String?
|
||||
public let note: String?
|
||||
public let url: String?
|
||||
public let emojisData: Data?
|
||||
public let fieldsData: Data?
|
||||
public let statusesCount: Int
|
||||
public let followingCount: Int
|
||||
public let followersCount: Int
|
||||
public let locked: Bool
|
||||
public let bot: Bool?
|
||||
public let suspended: Bool?
|
||||
|
||||
public let createdAt: Date
|
||||
public let networkDate: Date
|
||||
|
||||
public init(
|
||||
id: String,
|
||||
domain: String,
|
||||
acct: String,
|
||||
username: String,
|
||||
displayName: String,
|
||||
avatar: String,
|
||||
avatarStatic: String?,
|
||||
header: String,
|
||||
headerStatic: String?,
|
||||
note: String?,
|
||||
url: String?,
|
||||
emojisData: Data?,
|
||||
fieldsData: Data?,
|
||||
statusesCount: Int,
|
||||
followingCount: Int,
|
||||
followersCount: Int,
|
||||
locked: Bool,
|
||||
bot: Bool?,
|
||||
suspended: Bool?,
|
||||
createdAt: Date,
|
||||
networkDate: Date
|
||||
) {
|
||||
self.identifier = id + "@" + domain
|
||||
self.domain = domain
|
||||
self.id = id
|
||||
self.acct = acct
|
||||
self.username = username
|
||||
self.displayName = displayName
|
||||
self.avatar = avatar
|
||||
self.avatarStatic = avatarStatic
|
||||
self.header = header
|
||||
self.headerStatic = headerStatic
|
||||
self.note = note
|
||||
self.url = url
|
||||
self.emojisData = emojisData
|
||||
self.fieldsData = fieldsData
|
||||
self.statusesCount = statusesCount
|
||||
self.followingCount = followingCount
|
||||
self.followersCount = followersCount
|
||||
self.locked = locked
|
||||
self.bot = bot
|
||||
self.suspended = suspended
|
||||
self.createdAt = createdAt
|
||||
self.networkDate = networkDate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MastodonUser: Managed {
|
||||
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
||||
return [NSSortDescriptor(keyPath: \MastodonUser.createdAt, ascending: false)]
|
||||
}
|
||||
}
|
||||
|
||||
extension MastodonUser {
|
||||
|
||||
static func predicate(domain: String) -> NSPredicate {
|
||||
return NSPredicate(format: "%K == %@", #keyPath(MastodonUser.domain), domain)
|
||||
}
|
||||
|
||||
static func predicate(id: String) -> NSPredicate {
|
||||
return NSPredicate(format: "%K == %@", #keyPath(MastodonUser.id), id)
|
||||
}
|
||||
|
||||
public static func predicate(domain: String, id: String) -> NSPredicate {
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [
|
||||
MastodonUser.predicate(domain: domain),
|
||||
MastodonUser.predicate(id: id)
|
||||
])
|
||||
}
|
||||
|
||||
static func predicate(ids: [String]) -> NSPredicate {
|
||||
return NSPredicate(format: "%K IN %@", #keyPath(MastodonUser.id), ids)
|
||||
}
|
||||
|
||||
public static func predicate(domain: String, ids: [String]) -> NSPredicate {
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [
|
||||
MastodonUser.predicate(domain: domain),
|
||||
MastodonUser.predicate(ids: ids)
|
||||
])
|
||||
}
|
||||
|
||||
static func predicate(username: String) -> NSPredicate {
|
||||
return NSPredicate(format: "%K == %@", #keyPath(MastodonUser.username), username)
|
||||
}
|
||||
|
||||
public static func predicate(domain: String, username: String) -> NSPredicate {
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [
|
||||
MastodonUser.predicate(domain: domain),
|
||||
MastodonUser.predicate(username: username)
|
||||
])
|
||||
}
|
||||
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
//
|
||||
// Mention.swift
|
||||
// CoreDataStack
|
||||
//
|
||||
// Created by sxiaojian on 2021/2/1.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
public final class Mention: NSManagedObject {
|
||||
public typealias ID = UUID
|
||||
|
||||
@NSManaged public private(set) var index: NSNumber
|
||||
|
||||
@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 username: String
|
||||
@NSManaged public private(set) var acct: String
|
||||
@NSManaged public private(set) var url: String
|
||||
|
||||
// many-to-one relationship
|
||||
@NSManaged public private(set) var status: Status
|
||||
}
|
||||
|
||||
public extension Mention {
|
||||
override func awakeFromInsert() {
|
||||
super.awakeFromInsert()
|
||||
|
||||
setPrimitiveValue(UUID(), forKey: #keyPath(Mention.identifier))
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
static func insert(
|
||||
into context: NSManagedObjectContext,
|
||||
property: Property,
|
||||
index: Int
|
||||
) -> Mention {
|
||||
let mention: Mention = context.insertObject()
|
||||
mention.index = NSNumber(value: index)
|
||||
mention.id = property.id
|
||||
mention.username = property.username
|
||||
mention.acct = property.acct
|
||||
mention.url = property.url
|
||||
return mention
|
||||
}
|
||||
}
|
||||
|
||||
public extension Mention {
|
||||
struct Property {
|
||||
public let id: String
|
||||
public let username: String
|
||||
public let acct: String
|
||||
public let url: String
|
||||
|
||||
public init(id: String, username: String, acct: String, url: String) {
|
||||
self.id = id
|
||||
self.username = username
|
||||
self.acct = acct
|
||||
self.url = url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Mention: Managed {
|
||||
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
||||
return [NSSortDescriptor(keyPath: \Mention.createAt, ascending: false)]
|
||||
}
|
||||
}
|
|
@ -1,115 +0,0 @@
|
|||
//
|
||||
// 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)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
public static func predicate(validTypesRaws types: [String]) -> NSPredicate {
|
||||
return NSPredicate(format: "%K IN %@", #keyPath(MastodonNotification.typeRaw), types)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MastodonNotification: Managed {
|
||||
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
||||
return [NSSortDescriptor(keyPath: \MastodonNotification.createAt, ascending: false)]
|
||||
}
|
||||
}
|
|
@ -1,145 +0,0 @@
|
|||
//
|
||||
// Poll.swift
|
||||
// CoreDataStack
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-2.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
public final class Poll: NSManagedObject {
|
||||
public typealias ID = String
|
||||
|
||||
@NSManaged public private(set) var id: ID
|
||||
@NSManaged public private(set) var expiresAt: Date?
|
||||
@NSManaged public private(set) var expired: Bool
|
||||
@NSManaged public private(set) var multiple: Bool
|
||||
@NSManaged public private(set) var votesCount: NSNumber
|
||||
@NSManaged public private(set) var votersCount: NSNumber?
|
||||
|
||||
@NSManaged public private(set) var createdAt: Date
|
||||
@NSManaged public private(set) var updatedAt: Date
|
||||
|
||||
// one-to-one relationship
|
||||
@NSManaged public private(set) var status: Status
|
||||
|
||||
// one-to-many relationship
|
||||
@NSManaged public private(set) var options: Set<PollOption>
|
||||
|
||||
// many-to-many relationship
|
||||
@NSManaged public private(set) var votedBy: Set<MastodonUser>?
|
||||
}
|
||||
|
||||
extension Poll {
|
||||
|
||||
public override func awakeFromInsert() {
|
||||
super.awakeFromInsert()
|
||||
setPrimitiveValue(Date(), forKey: #keyPath(Poll.createdAt))
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public static func insert(
|
||||
into context: NSManagedObjectContext,
|
||||
property: Property,
|
||||
votedBy: MastodonUser?,
|
||||
options: [PollOption]
|
||||
) -> Poll {
|
||||
let poll: Poll = context.insertObject()
|
||||
|
||||
poll.id = property.id
|
||||
poll.expiresAt = property.expiresAt
|
||||
poll.expired = property.expired
|
||||
poll.multiple = property.multiple
|
||||
poll.votesCount = property.votesCount
|
||||
poll.votersCount = property.votersCount
|
||||
|
||||
|
||||
poll.updatedAt = property.networkDate
|
||||
|
||||
if let votedBy = votedBy {
|
||||
poll.mutableSetValue(forKey: #keyPath(Poll.votedBy)).add(votedBy)
|
||||
}
|
||||
poll.mutableSetValue(forKey: #keyPath(Poll.options)).addObjects(from: options)
|
||||
|
||||
return poll
|
||||
}
|
||||
|
||||
public func update(expiresAt: Date?) {
|
||||
if self.expiresAt != expiresAt {
|
||||
self.expiresAt = expiresAt
|
||||
}
|
||||
}
|
||||
|
||||
public func update(expired: Bool) {
|
||||
if self.expired != expired {
|
||||
self.expired = expired
|
||||
}
|
||||
}
|
||||
|
||||
public func update(votesCount: Int) {
|
||||
if self.votesCount.intValue != votesCount {
|
||||
self.votesCount = NSNumber(value: votesCount)
|
||||
}
|
||||
}
|
||||
|
||||
public func update(votersCount: Int?) {
|
||||
if self.votersCount?.intValue != votersCount {
|
||||
self.votersCount = votersCount.flatMap { NSNumber(value: $0) }
|
||||
}
|
||||
}
|
||||
|
||||
public func update(voted: Bool, by: MastodonUser) {
|
||||
if voted {
|
||||
if !(votedBy ?? Set()).contains(by) {
|
||||
mutableSetValue(forKey: #keyPath(Poll.votedBy)).add(by)
|
||||
}
|
||||
} else {
|
||||
if (votedBy ?? Set()).contains(by) {
|
||||
mutableSetValue(forKey: #keyPath(Poll.votedBy)).remove(by)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func didUpdate(at networkDate: Date) {
|
||||
self.updatedAt = networkDate
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Poll {
|
||||
public struct Property {
|
||||
public let id: ID
|
||||
public let expiresAt: Date?
|
||||
public let expired: Bool
|
||||
public let multiple: Bool
|
||||
public let votesCount: NSNumber
|
||||
public let votersCount: NSNumber?
|
||||
|
||||
public let networkDate: Date
|
||||
|
||||
public init(
|
||||
id: Poll.ID,
|
||||
expiresAt: Date?,
|
||||
expired: Bool,
|
||||
multiple: Bool,
|
||||
votesCount: Int,
|
||||
votersCount: Int?,
|
||||
networkDate: Date
|
||||
) {
|
||||
self.id = id
|
||||
self.expiresAt = expiresAt
|
||||
self.expired = expired
|
||||
self.multiple = multiple
|
||||
self.votesCount = NSNumber(value: votesCount)
|
||||
self.votersCount = votersCount.flatMap { NSNumber(value: $0) }
|
||||
self.networkDate = networkDate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Poll: Managed {
|
||||
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
||||
return [NSSortDescriptor(keyPath: \Poll.createdAt, ascending: false)]
|
||||
}
|
||||
}
|
|
@ -1,98 +0,0 @@
|
|||
//
|
||||
// PollOption.swift
|
||||
// CoreDataStack
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-2.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
public final class PollOption: NSManagedObject {
|
||||
@NSManaged public private(set) var index: NSNumber
|
||||
@NSManaged public private(set) var title: String
|
||||
@NSManaged public private(set) var votesCount: NSNumber?
|
||||
|
||||
@NSManaged public private(set) var createdAt: Date
|
||||
@NSManaged public private(set) var updatedAt: Date
|
||||
|
||||
// many-to-one relationship
|
||||
@NSManaged public private(set) var poll: Poll
|
||||
|
||||
// many-to-many relationship
|
||||
@NSManaged public private(set) var votedBy: Set<MastodonUser>?
|
||||
}
|
||||
|
||||
extension PollOption {
|
||||
|
||||
public override func awakeFromInsert() {
|
||||
super.awakeFromInsert()
|
||||
setPrimitiveValue(Date(), forKey: #keyPath(PollOption.createdAt))
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public static func insert(
|
||||
into context: NSManagedObjectContext,
|
||||
property: Property,
|
||||
votedBy: MastodonUser?
|
||||
) -> PollOption {
|
||||
let option: PollOption = context.insertObject()
|
||||
|
||||
option.index = property.index
|
||||
option.title = property.title
|
||||
option.votesCount = property.votesCount
|
||||
option.updatedAt = property.networkDate
|
||||
|
||||
if let votedBy = votedBy {
|
||||
option.mutableSetValue(forKey: #keyPath(PollOption.votedBy)).add(votedBy)
|
||||
}
|
||||
|
||||
return option
|
||||
}
|
||||
|
||||
public func update(votesCount: Int?) {
|
||||
if self.votesCount?.intValue != votesCount {
|
||||
self.votesCount = votesCount.flatMap { NSNumber(value: $0) }
|
||||
}
|
||||
}
|
||||
|
||||
public func update(voted: Bool, by: MastodonUser) {
|
||||
if voted {
|
||||
if !(self.votedBy ?? Set()).contains(by) {
|
||||
self.mutableSetValue(forKey: #keyPath(PollOption.votedBy)).add(by)
|
||||
}
|
||||
} else {
|
||||
if (self.votedBy ?? Set()).contains(by) {
|
||||
self.mutableSetValue(forKey: #keyPath(PollOption.votedBy)).remove(by)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func didUpdate(at networkDate: Date) {
|
||||
self.updatedAt = networkDate
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension PollOption {
|
||||
public struct Property {
|
||||
public let index: NSNumber
|
||||
public let title: String
|
||||
public let votesCount: NSNumber?
|
||||
|
||||
public let networkDate: Date
|
||||
|
||||
public init(index: Int, title: String, votesCount: Int?, networkDate: Date) {
|
||||
self.index = NSNumber(value: index)
|
||||
self.title = title
|
||||
self.votesCount = votesCount.flatMap { NSNumber(value: $0) }
|
||||
self.networkDate = networkDate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PollOption: Managed {
|
||||
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
||||
return [NSSortDescriptor(keyPath: \PollOption.createdAt, ascending: false)]
|
||||
}
|
||||
}
|
|
@ -1,118 +0,0 @@
|
|||
//
|
||||
// SearchHistory.swift
|
||||
// CoreDataStack
|
||||
//
|
||||
// Created by sxiaojian on 2021/4/7.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
public final class SearchHistory: NSManagedObject {
|
||||
public typealias ID = UUID
|
||||
@NSManaged public private(set) var identifier: ID
|
||||
@NSManaged public private(set) var domain: String
|
||||
@NSManaged public private(set) var userID: MastodonUser.ID
|
||||
@NSManaged public private(set) var createAt: Date
|
||||
@NSManaged public private(set) var updatedAt: Date
|
||||
|
||||
// many-to-one relationship
|
||||
@NSManaged public private(set) var account: MastodonUser?
|
||||
@NSManaged public private(set) var hashtag: Tag?
|
||||
@NSManaged public private(set) var status: Status?
|
||||
|
||||
}
|
||||
|
||||
extension SearchHistory {
|
||||
public override func awakeFromInsert() {
|
||||
super.awakeFromInsert()
|
||||
setPrimitiveValue(UUID(), forKey: #keyPath(SearchHistory.identifier))
|
||||
setPrimitiveValue(Date(), forKey: #keyPath(SearchHistory.createAt))
|
||||
setPrimitiveValue(Date(), forKey: #keyPath(SearchHistory.updatedAt))
|
||||
}
|
||||
|
||||
// public override func willSave() {
|
||||
// super.willSave()
|
||||
// setPrimitiveValue(Date(), forKey: #keyPath(SearchHistory.updatedAt))
|
||||
// }
|
||||
|
||||
@discardableResult
|
||||
public static func insert(
|
||||
into context: NSManagedObjectContext,
|
||||
property: Property,
|
||||
account: MastodonUser
|
||||
) -> SearchHistory {
|
||||
let searchHistory: SearchHistory = context.insertObject()
|
||||
searchHistory.domain = property.domain
|
||||
searchHistory.userID = property.userID
|
||||
searchHistory.account = account
|
||||
return searchHistory
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public static func insert(
|
||||
into context: NSManagedObjectContext,
|
||||
property: Property,
|
||||
hashtag: Tag
|
||||
) -> SearchHistory {
|
||||
let searchHistory: SearchHistory = context.insertObject()
|
||||
searchHistory.domain = property.domain
|
||||
searchHistory.userID = property.userID
|
||||
searchHistory.hashtag = hashtag
|
||||
return searchHistory
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public static func insert(
|
||||
into context: NSManagedObjectContext,
|
||||
property: Property,
|
||||
status: Status
|
||||
) -> SearchHistory {
|
||||
let searchHistory: SearchHistory = context.insertObject()
|
||||
searchHistory.domain = property.domain
|
||||
searchHistory.userID = property.userID
|
||||
searchHistory.status = status
|
||||
return searchHistory
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchHistory {
|
||||
public func update(updatedAt: Date) {
|
||||
setValue(updatedAt, forKey: #keyPath(SearchHistory.updatedAt))
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchHistory {
|
||||
public struct Property {
|
||||
public let domain: String
|
||||
public let userID: MastodonUser.ID
|
||||
|
||||
public init(domain: String, userID: MastodonUser.ID) {
|
||||
self.domain = domain
|
||||
self.userID = userID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchHistory: Managed {
|
||||
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
||||
return [NSSortDescriptor(keyPath: \SearchHistory.updatedAt, ascending: false)]
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchHistory {
|
||||
static func predicate(domain: String) -> NSPredicate {
|
||||
return NSPredicate(format: "%K == %@", #keyPath(SearchHistory.domain), domain)
|
||||
}
|
||||
|
||||
static func predicate(userID: String) -> NSPredicate {
|
||||
return NSPredicate(format: "%K == %@", #keyPath(SearchHistory.userID), userID)
|
||||
}
|
||||
|
||||
public static func predicate(domain: String, userID: String) -> NSPredicate {
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [
|
||||
predicate(domain: domain),
|
||||
predicate(userID: userID)
|
||||
])
|
||||
}
|
||||
}
|
|
@ -1,355 +0,0 @@
|
|||
//
|
||||
// Status.swift
|
||||
// CoreDataStack
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/27.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
public final class Status: NSManagedObject {
|
||||
public typealias ID = String
|
||||
|
||||
@NSManaged public private(set) var identifier: ID
|
||||
@NSManaged public private(set) var domain: String
|
||||
|
||||
@NSManaged public private(set) var id: String
|
||||
@NSManaged public private(set) var uri: String
|
||||
@NSManaged public private(set) var createdAt: Date
|
||||
@NSManaged public private(set) var content: String
|
||||
|
||||
@NSManaged public private(set) var visibility: String?
|
||||
@NSManaged public private(set) var sensitive: Bool
|
||||
@NSManaged public private(set) var spoilerText: String?
|
||||
@NSManaged public private(set) var application: Application?
|
||||
|
||||
@NSManaged public private(set) var emojisData: Data?
|
||||
|
||||
// Informational
|
||||
@NSManaged public private(set) var reblogsCount: NSNumber
|
||||
@NSManaged public private(set) var favouritesCount: NSNumber
|
||||
@NSManaged public private(set) var repliesCount: NSNumber?
|
||||
|
||||
@NSManaged public private(set) var url: String?
|
||||
@NSManaged public private(set) var inReplyToID: Status.ID?
|
||||
@NSManaged public private(set) var inReplyToAccountID: MastodonUser.ID?
|
||||
|
||||
@NSManaged public private(set) var language: String? // (ISO 639 Part 1 two-letter language code)
|
||||
@NSManaged public private(set) var text: String?
|
||||
|
||||
// many-to-one relationship
|
||||
@NSManaged public private(set) var author: MastodonUser
|
||||
@NSManaged public private(set) var reblog: Status?
|
||||
@NSManaged public private(set) var replyTo: Status?
|
||||
|
||||
// many-to-many relationship
|
||||
@NSManaged public private(set) var favouritedBy: Set<MastodonUser>?
|
||||
@NSManaged public private(set) var rebloggedBy: Set<MastodonUser>?
|
||||
@NSManaged public private(set) var mutedBy: Set<MastodonUser>?
|
||||
@NSManaged public private(set) var bookmarkedBy: Set<MastodonUser>?
|
||||
|
||||
// one-to-one relationship
|
||||
@NSManaged public private(set) var pinnedBy: MastodonUser?
|
||||
@NSManaged public private(set) var poll: Poll?
|
||||
|
||||
// one-to-many relationship
|
||||
@NSManaged public private(set) var reblogFrom: Set<Status>?
|
||||
@NSManaged public private(set) var mentions: Set<Mention>?
|
||||
@NSManaged public private(set) var homeTimelineIndexes: Set<HomeTimelineIndex>?
|
||||
@NSManaged public private(set) var mediaAttachments: Set<Attachment>?
|
||||
@NSManaged public private(set) var replyFrom: Set<Status>?
|
||||
|
||||
@NSManaged public private(set) var inNotifications: Set<MastodonNotification>?
|
||||
|
||||
@NSManaged public private(set) var searchHistories: Set<SearchHistory>
|
||||
|
||||
@NSManaged public private(set) var updatedAt: Date
|
||||
@NSManaged public private(set) var deletedAt: Date?
|
||||
@NSManaged public private(set) var revealedAt: Date?
|
||||
}
|
||||
|
||||
extension Status {
|
||||
|
||||
@discardableResult
|
||||
public static func insert(
|
||||
into context: NSManagedObjectContext,
|
||||
property: Property,
|
||||
author: MastodonUser,
|
||||
reblog: Status?,
|
||||
application: Application?,
|
||||
replyTo: Status?,
|
||||
poll: Poll?,
|
||||
mentions: [Mention]?,
|
||||
mediaAttachments: [Attachment]?,
|
||||
favouritedBy: MastodonUser?,
|
||||
rebloggedBy: MastodonUser?,
|
||||
mutedBy: MastodonUser?,
|
||||
bookmarkedBy: MastodonUser?,
|
||||
pinnedBy: MastodonUser?
|
||||
) -> Status {
|
||||
let status: Status = context.insertObject()
|
||||
|
||||
status.identifier = property.identifier
|
||||
status.domain = property.domain
|
||||
|
||||
status.id = property.id
|
||||
status.uri = property.uri
|
||||
status.createdAt = property.createdAt
|
||||
status.content = property.content
|
||||
|
||||
status.visibility = property.visibility
|
||||
status.sensitive = property.sensitive
|
||||
status.spoilerText = property.spoilerText
|
||||
status.application = application
|
||||
|
||||
status.emojisData = property.emojisData
|
||||
|
||||
status.reblogsCount = property.reblogsCount
|
||||
status.favouritesCount = property.favouritesCount
|
||||
status.repliesCount = property.repliesCount
|
||||
|
||||
status.url = property.url
|
||||
status.inReplyToID = property.inReplyToID
|
||||
status.inReplyToAccountID = property.inReplyToAccountID
|
||||
|
||||
status.language = property.language
|
||||
status.text = property.text
|
||||
|
||||
status.author = author
|
||||
status.reblog = reblog
|
||||
|
||||
status.pinnedBy = pinnedBy
|
||||
status.poll = poll
|
||||
|
||||
if let mentions = mentions {
|
||||
status.mutableSetValue(forKey: #keyPath(Status.mentions)).addObjects(from: mentions)
|
||||
}
|
||||
if let mediaAttachments = mediaAttachments {
|
||||
status.mutableSetValue(forKey: #keyPath(Status.mediaAttachments)).addObjects(from: mediaAttachments)
|
||||
}
|
||||
if let favouritedBy = favouritedBy {
|
||||
status.mutableSetValue(forKey: #keyPath(Status.favouritedBy)).add(favouritedBy)
|
||||
}
|
||||
if let rebloggedBy = rebloggedBy {
|
||||
status.mutableSetValue(forKey: #keyPath(Status.rebloggedBy)).add(rebloggedBy)
|
||||
}
|
||||
if let mutedBy = mutedBy {
|
||||
status.mutableSetValue(forKey: #keyPath(Status.mutedBy)).add(mutedBy)
|
||||
}
|
||||
if let bookmarkedBy = bookmarkedBy {
|
||||
status.mutableSetValue(forKey: #keyPath(Status.bookmarkedBy)).add(bookmarkedBy)
|
||||
}
|
||||
|
||||
status.updatedAt = property.networkDate
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
public func update(emojisData: Data?) {
|
||||
if self.emojisData != emojisData {
|
||||
self.emojisData = emojisData
|
||||
}
|
||||
}
|
||||
|
||||
public func update(reblogsCount: NSNumber) {
|
||||
if self.reblogsCount.intValue != reblogsCount.intValue {
|
||||
self.reblogsCount = reblogsCount
|
||||
}
|
||||
}
|
||||
|
||||
public func update(favouritesCount: NSNumber) {
|
||||
if self.favouritesCount.intValue != favouritesCount.intValue {
|
||||
self.favouritesCount = favouritesCount
|
||||
}
|
||||
}
|
||||
|
||||
public func update(repliesCount: NSNumber?) {
|
||||
guard let count = repliesCount else {
|
||||
return
|
||||
}
|
||||
if self.repliesCount?.intValue != count.intValue {
|
||||
self.repliesCount = repliesCount
|
||||
}
|
||||
}
|
||||
|
||||
public func update(replyTo: Status?) {
|
||||
if self.replyTo != replyTo {
|
||||
self.replyTo = replyTo
|
||||
}
|
||||
}
|
||||
|
||||
public func update(liked: Bool, by mastodonUser: MastodonUser) {
|
||||
if liked {
|
||||
if !(self.favouritedBy ?? Set()).contains(mastodonUser) {
|
||||
self.mutableSetValue(forKey: #keyPath(Status.favouritedBy)).add(mastodonUser)
|
||||
}
|
||||
} else {
|
||||
if (self.favouritedBy ?? Set()).contains(mastodonUser) {
|
||||
self.mutableSetValue(forKey: #keyPath(Status.favouritedBy)).remove(mastodonUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func update(reblogged: Bool, by mastodonUser: MastodonUser) {
|
||||
if reblogged {
|
||||
if !(self.rebloggedBy ?? Set()).contains(mastodonUser) {
|
||||
self.mutableSetValue(forKey: #keyPath(Status.rebloggedBy)).add(mastodonUser)
|
||||
}
|
||||
} else {
|
||||
if (self.rebloggedBy ?? Set()).contains(mastodonUser) {
|
||||
self.mutableSetValue(forKey: #keyPath(Status.rebloggedBy)).remove(mastodonUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func update(muted: Bool, by mastodonUser: MastodonUser) {
|
||||
if muted {
|
||||
if !(self.mutedBy ?? Set()).contains(mastodonUser) {
|
||||
self.mutableSetValue(forKey: #keyPath(Status.mutedBy)).add(mastodonUser)
|
||||
}
|
||||
} else {
|
||||
if (self.mutedBy ?? Set()).contains(mastodonUser) {
|
||||
self.mutableSetValue(forKey: #keyPath(Status.mutedBy)).remove(mastodonUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func update(bookmarked: Bool, by mastodonUser: MastodonUser) {
|
||||
if bookmarked {
|
||||
if !(self.bookmarkedBy ?? Set()).contains(mastodonUser) {
|
||||
self.mutableSetValue(forKey: #keyPath(Status.bookmarkedBy)).add(mastodonUser)
|
||||
}
|
||||
} else {
|
||||
if (self.bookmarkedBy ?? Set()).contains(mastodonUser) {
|
||||
self.mutableSetValue(forKey: #keyPath(Status.bookmarkedBy)).remove(mastodonUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func update(isReveal: Bool) {
|
||||
revealedAt = isReveal ? Date() : nil
|
||||
}
|
||||
|
||||
public func didUpdate(at networkDate: Date) {
|
||||
self.updatedAt = networkDate
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Status {
|
||||
public struct Property {
|
||||
|
||||
public let identifier: ID
|
||||
public let domain: String
|
||||
|
||||
public let id: String
|
||||
public let uri: String
|
||||
public let createdAt: Date
|
||||
public let content: String
|
||||
|
||||
public let visibility: String?
|
||||
public let sensitive: Bool
|
||||
public let spoilerText: String?
|
||||
|
||||
public let emojisData: Data?
|
||||
|
||||
public let reblogsCount: NSNumber
|
||||
public let favouritesCount: NSNumber
|
||||
public let repliesCount: NSNumber?
|
||||
|
||||
public let url: String?
|
||||
public let inReplyToID: Status.ID?
|
||||
public let inReplyToAccountID: MastodonUser.ID?
|
||||
public let language: String? // (ISO 639 Part @1 two-letter language code)
|
||||
public let text: String?
|
||||
|
||||
public let networkDate: Date
|
||||
|
||||
public init(
|
||||
domain: String,
|
||||
id: String,
|
||||
uri: String,
|
||||
createdAt: Date,
|
||||
content: String,
|
||||
visibility: String?,
|
||||
sensitive: Bool,
|
||||
spoilerText: String?,
|
||||
emojisData: Data?,
|
||||
reblogsCount: NSNumber,
|
||||
favouritesCount: NSNumber,
|
||||
repliesCount: NSNumber?,
|
||||
url: String?,
|
||||
inReplyToID: Status.ID?,
|
||||
inReplyToAccountID: MastodonUser.ID?,
|
||||
language: String?,
|
||||
text: String?,
|
||||
networkDate: Date
|
||||
) {
|
||||
self.identifier = id + "@" + domain
|
||||
self.domain = domain
|
||||
self.id = id
|
||||
self.uri = uri
|
||||
self.createdAt = createdAt
|
||||
self.content = content
|
||||
self.visibility = visibility
|
||||
self.sensitive = sensitive
|
||||
self.spoilerText = spoilerText
|
||||
self.emojisData = emojisData
|
||||
self.reblogsCount = reblogsCount
|
||||
self.favouritesCount = favouritesCount
|
||||
self.repliesCount = repliesCount
|
||||
self.url = url
|
||||
self.inReplyToID = inReplyToID
|
||||
self.inReplyToAccountID = inReplyToAccountID
|
||||
self.language = language
|
||||
self.text = text
|
||||
self.networkDate = networkDate
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
extension Status: Managed {
|
||||
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
||||
return [NSSortDescriptor(keyPath: \Status.createdAt, ascending: false)]
|
||||
}
|
||||
}
|
||||
|
||||
extension Status {
|
||||
|
||||
static func predicate(domain: String) -> NSPredicate {
|
||||
return NSPredicate(format: "%K == %@", #keyPath(Status.domain), domain)
|
||||
}
|
||||
|
||||
static func predicate(id: String) -> NSPredicate {
|
||||
return NSPredicate(format: "%K == %@", #keyPath(Status.id), id)
|
||||
}
|
||||
|
||||
public static func predicate(domain: String, id: String) -> NSPredicate {
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [
|
||||
predicate(domain: domain),
|
||||
predicate(id: id)
|
||||
])
|
||||
}
|
||||
|
||||
static func predicate(ids: [String]) -> NSPredicate {
|
||||
return NSPredicate(format: "%K IN %@", #keyPath(Status.id), ids)
|
||||
}
|
||||
|
||||
public static func predicate(domain: String, ids: [String]) -> NSPredicate {
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [
|
||||
predicate(domain: domain),
|
||||
predicate(ids: ids)
|
||||
])
|
||||
}
|
||||
|
||||
public static func notDeleted() -> NSPredicate {
|
||||
return NSPredicate(format: "%K == nil", #keyPath(Status.deletedAt))
|
||||
}
|
||||
|
||||
public static func deleted() -> NSPredicate {
|
||||
return NSPredicate(format: "%K != nil", #keyPath(Status.deletedAt))
|
||||
}
|
||||
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
//
|
||||
// Tag.swift
|
||||
// CoreDataStack
|
||||
//
|
||||
// Created by sxiaojian on 2021/2/1.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
public final class Tag: NSManagedObject {
|
||||
public typealias ID = UUID
|
||||
@NSManaged public private(set) var identifier: ID
|
||||
@NSManaged public private(set) var createAt: Date
|
||||
@NSManaged public private(set) var updatedAt: Date
|
||||
|
||||
@NSManaged public private(set) var name: String
|
||||
@NSManaged public private(set) var url: String
|
||||
|
||||
// one-to-one relationship
|
||||
|
||||
// many-to-many relationship
|
||||
|
||||
// one-to-many relationship
|
||||
@NSManaged public private(set) var histories: Set<History>?
|
||||
@NSManaged public private(set) var searchHistories: Set<SearchHistory>
|
||||
}
|
||||
|
||||
public extension Tag {
|
||||
override func awakeFromInsert() {
|
||||
super.awakeFromInsert()
|
||||
setPrimitiveValue(UUID(), forKey: #keyPath(Tag.identifier))
|
||||
setPrimitiveValue(Date(), forKey: #keyPath(Tag.createAt))
|
||||
setPrimitiveValue(Date(), forKey: #keyPath(Tag.updatedAt))
|
||||
}
|
||||
|
||||
override func willSave() {
|
||||
super.willSave()
|
||||
setPrimitiveValue(Date(), forKey: #keyPath(Tag.updatedAt))
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
static func insert(
|
||||
into context: NSManagedObjectContext,
|
||||
property: Property
|
||||
) -> Tag {
|
||||
let tag: Tag = context.insertObject()
|
||||
tag.name = property.name
|
||||
tag.url = property.url
|
||||
if let histories = property.histories {
|
||||
tag.mutableSetValue(forKey: #keyPath(Tag.histories)).addObjects(from: histories)
|
||||
}
|
||||
return tag
|
||||
}
|
||||
}
|
||||
|
||||
extension Tag {
|
||||
public func findSearchHistory(domain: String, userID: MastodonUser.ID) -> SearchHistory? {
|
||||
return searchHistories.first { searchHistory in
|
||||
return searchHistory.domain == domain
|
||||
&& searchHistory.userID == userID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Tag {
|
||||
struct Property {
|
||||
public let name: String
|
||||
public let url: String
|
||||
public let histories: [History]?
|
||||
|
||||
public init(name: String, url: String, histories: [History]?) {
|
||||
self.name = name
|
||||
self.url = url
|
||||
self.histories = histories
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Tag {
|
||||
func updateHistory(index: Int, day: Date, uses: String, account: String) {
|
||||
guard let histories = self.histories?.sorted(by: {
|
||||
$0.createAt.compare($1.createAt) == .orderedAscending
|
||||
}) else { return }
|
||||
let history = histories[index]
|
||||
history.update(day: day)
|
||||
history.update(uses: uses)
|
||||
history.update(accounts: account)
|
||||
}
|
||||
|
||||
func appendHistory(history: History) {
|
||||
self.mutableSetValue(forKeyPath: #keyPath(Tag.histories)).add(history)
|
||||
}
|
||||
|
||||
func update(url: String) {
|
||||
if self.url != url {
|
||||
self.url = url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Tag: Managed {
|
||||
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
||||
[NSSortDescriptor(keyPath: \Tag.createAt, ascending: false)]
|
||||
}
|
||||
}
|
||||
|
||||
public extension Tag {
|
||||
static func predicate(name: String) -> NSPredicate {
|
||||
NSPredicate(format: "%K == %@", #keyPath(Tag.name), name)
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
//
|
||||
// NSManagedObjectContext.swift
|
||||
// CoreDataStack
|
||||
//
|
||||
// Created by Cirno MainasuK on 2020-8-10.
|
||||
//
|
||||
|
||||
import os
|
||||
import Foundation
|
||||
import Combine
|
||||
import CoreData
|
||||
|
||||
extension NSManagedObjectContext {
|
||||
public func insert<T: NSManagedObject>() -> T where T: Managed {
|
||||
guard let object = NSEntityDescription.insertNewObject(forEntityName: T.entityName, into: self) as? T else {
|
||||
fatalError("cannot insert object: \(T.self)")
|
||||
}
|
||||
|
||||
return object
|
||||
}
|
||||
|
||||
public func saveOrRollback() throws {
|
||||
do {
|
||||
guard hasChanges else {
|
||||
return
|
||||
}
|
||||
try save()
|
||||
} catch {
|
||||
rollback()
|
||||
|
||||
os_log("%{public}s[%{public}ld], %{public}s: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public func performChanges(block: @escaping () -> Void) -> Future<Result<Void, Error>, Never> {
|
||||
Future { promise in
|
||||
self.perform {
|
||||
block()
|
||||
do {
|
||||
try self.saveOrRollback()
|
||||
promise(.success(Result.success(())))
|
||||
} catch {
|
||||
promise(.success(Result.failure(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.3.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>90</string>
|
||||
</dict>
|
||||
</plist>
|
|
@ -1,33 +0,0 @@
|
|||
//
|
||||
// CoreDataStackTests.swift
|
||||
// CoreDataStackTests
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/27.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import CoreDataStack
|
||||
|
||||
class CoreDataStackTests: XCTestCase {
|
||||
|
||||
override func setUpWithError() throws {
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
func testExample() throws {
|
||||
// This is an example of a functional test case.
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||
}
|
||||
|
||||
func testPerformanceExample() throws {
|
||||
// This is an example of a performance test case.
|
||||
self.measure {
|
||||
// Put the code you want to measure the time of here.
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.3.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>90</string>
|
||||
</dict>
|
||||
</plist>
|
|
@ -1,11 +1,6 @@
|
|||
import os.log
|
||||
import Foundation
|
||||
|
||||
let currentFileURL = URL(fileURLWithPath: "\(#file)", isDirectory: false)
|
||||
let packageRootURL = currentFileURL.deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent()
|
||||
let inputDirectoryURL = packageRootURL.appendingPathComponent("input", isDirectory: true)
|
||||
let outputDirectoryURL = packageRootURL.appendingPathComponent("output", isDirectory: true)
|
||||
|
||||
// conver i18n JSON templates to strings files
|
||||
private func convert(from inputDirectoryURL: URL, to outputDirectory: URL) {
|
||||
do {
|
||||
|
@ -17,7 +12,6 @@ private func convert(from inputDirectoryURL: URL, to outputDirectory: URL) {
|
|||
for inputLanguageDirectoryURL in inputLanguageDirectoryURLs {
|
||||
let language = inputLanguageDirectoryURL.lastPathComponent
|
||||
guard let mappedLanguage = map(language: language) else { continue }
|
||||
let outputDirectoryURL = outputDirectory.appendingPathComponent(mappedLanguage + ".lproj", isDirectory: true)
|
||||
os_log("%{public}s[%{public}ld], %{public}s: process %s -> %s", ((#file as NSString).lastPathComponent), #line, #function, language, mappedLanguage)
|
||||
|
||||
let fileURLs = try FileManager.default.contentsOfDirectory(
|
||||
|
@ -29,9 +23,19 @@ private func convert(from inputDirectoryURL: URL, to outputDirectory: URL) {
|
|||
os_log("%{public}s[%{public}ld], %{public}s: process %s", ((#file as NSString).lastPathComponent), #line, #function, jsonURL.debugDescription)
|
||||
let filename = jsonURL.deletingPathExtension().lastPathComponent
|
||||
guard let (mappedFilename, keyStyle) = map(filename: filename) else { continue }
|
||||
let outputFileURL = outputDirectoryURL.appendingPathComponent(mappedFilename).appendingPathExtension("strings")
|
||||
guard let bundle = bundle(filename: filename) else { continue }
|
||||
|
||||
let outputDirectoryURL = outputDirectory
|
||||
.appendingPathComponent(bundle, isDirectory: true)
|
||||
.appendingPathComponent(mappedLanguage + ".lproj", isDirectory: true)
|
||||
|
||||
let outputFileURL = outputDirectoryURL
|
||||
.appendingPathComponent(mappedFilename)
|
||||
.appendingPathExtension("strings")
|
||||
|
||||
let strings = try process(url: jsonURL, keyStyle: keyStyle)
|
||||
try? FileManager.default.createDirectory(at: outputDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
try strings.write(to: outputFileURL, atomically: true, encoding: .utf8)
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +48,7 @@ private func convert(from inputDirectoryURL: URL, to outputDirectory: URL) {
|
|||
private func map(language: String) -> String? {
|
||||
switch language {
|
||||
case "ar_SA": return "ar" // Arabic (Saudi Arabia)
|
||||
case "eu_ES": return "eu-ES" // Basque
|
||||
case "ca_ES": return "ca" // Catalan
|
||||
case "zh_CN": return "zh-Hans" // Chinese Simplified
|
||||
case "nl_NL": return "nl" // Dutch
|
||||
|
@ -56,6 +61,7 @@ private func map(language: String) -> String? {
|
|||
case "gd_GB": return "gd-GB" // Scottish Gaelic
|
||||
case "es_ES": return "es" // Spanish
|
||||
case "es_AR": return "es-419" // Spanish, Argentina
|
||||
case "sv_FI": return "sv_FI" // Swedish, Finland
|
||||
case "th_TH": return "th" // Thai
|
||||
default: return nil
|
||||
}
|
||||
|
@ -69,6 +75,14 @@ private func map(filename: String) -> (filename: String, keyStyle: Parser.KeySty
|
|||
}
|
||||
}
|
||||
|
||||
private func bundle(filename: String) -> String? {
|
||||
switch filename {
|
||||
case "app": return "module"
|
||||
case "ios-infoPlist": return "main"
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func process(url: URL, keyStyle: Parser.KeyStyle) throws -> String {
|
||||
do {
|
||||
let data = try Data(contentsOf: url)
|
||||
|
@ -115,9 +129,16 @@ private func move(from inputDirectoryURL: URL, to outputDirectoryURL: URL, pathE
|
|||
}
|
||||
}
|
||||
|
||||
// i18n from "input" to "output"
|
||||
|
||||
let currentFileURL = URL(fileURLWithPath: "\(#file)", isDirectory: false)
|
||||
let packageRootURL = currentFileURL.deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent()
|
||||
|
||||
let inputDirectoryURL = packageRootURL.appendingPathComponent("input", isDirectory: true)
|
||||
let outputDirectoryURL = packageRootURL.appendingPathComponent("output", isDirectory: true)
|
||||
convert(from: inputDirectoryURL, to: outputDirectoryURL)
|
||||
move(from: inputDirectoryURL, to: outputDirectoryURL, pathExtension: "stringsdict")
|
||||
|
||||
let moduleDirectoryURL = outputDirectoryURL.appendingPathComponent("module", isDirectory: true)
|
||||
move(from: inputDirectoryURL, to: moduleDirectoryURL, pathExtension: "stringsdict")
|
||||
|
||||
// i18n from "Intents/input" to "Intents/output"
|
||||
let intentsDirectoryURL = packageRootURL.appendingPathComponent("Intents", isDirectory: true)
|
||||
|
|
|
@ -45,8 +45,8 @@
|
|||
"message": "Please enable the photo library access permission to save the photo."
|
||||
},
|
||||
"delete_post": {
|
||||
"title": "Are you sure you want to delete this post?",
|
||||
"delete": "Delete"
|
||||
"title": "Delete Post",
|
||||
"message": "Are you sure you want to delete this post?"
|
||||
},
|
||||
"clean_cache": {
|
||||
"title": "Clean Cache",
|
||||
|
@ -140,7 +140,8 @@
|
|||
"unreblog": "Undo reblog",
|
||||
"favorite": "Favorite",
|
||||
"unfavorite": "Unfavorite",
|
||||
"menu": "Menu"
|
||||
"menu": "Menu",
|
||||
"hide": "Hide"
|
||||
},
|
||||
"tag": {
|
||||
"url": "URL",
|
||||
|
@ -149,6 +150,12 @@
|
|||
"hashtag": "Hashtag",
|
||||
"email": "Email",
|
||||
"emoji": "Emoji"
|
||||
},
|
||||
"visibility": {
|
||||
"unlisted": "Everyone can see this post but not display in the public timeline.",
|
||||
"private": "Only their followers can see this post.",
|
||||
"private_from_me": "Only my followers can see this post.",
|
||||
"direct": "Only mentioned user can see this post."
|
||||
}
|
||||
},
|
||||
"friendship": {
|
||||
|
@ -412,14 +419,24 @@
|
|||
"segmented_control": {
|
||||
"posts": "Posts",
|
||||
"replies": "Replies",
|
||||
"media": "Media"
|
||||
"posts_and_replies": "Posts and Replies",
|
||||
"media": "Media",
|
||||
"about": "About"
|
||||
},
|
||||
"relationship_action_alert": {
|
||||
"confirm_mute_user": {
|
||||
"title": "Mute Account",
|
||||
"message": "Confirm to mute %s"
|
||||
},
|
||||
"confirm_unmute_user": {
|
||||
"title": "Unmute Account",
|
||||
"message": "Confirm to unmute %s"
|
||||
},
|
||||
"confirm_unblock_usre": {
|
||||
"confirm_block_user": {
|
||||
"title": "Block Account",
|
||||
"message": "Confirm to block %s"
|
||||
},
|
||||
"confirm_unblock_user": {
|
||||
"title": "Unblock Account",
|
||||
"message": "Confirm to unblock %s"
|
||||
}
|
||||
|
@ -472,12 +489,14 @@
|
|||
"Everything": "Everything",
|
||||
"Mentions": "Mentions"
|
||||
},
|
||||
"user_followed_you": "%s followed you",
|
||||
"user_favorited your post": "%s favorited your post",
|
||||
"user_reblogged_your_post": "%s reblogged your post",
|
||||
"user_mentioned_you": "%s mentioned you",
|
||||
"user_requested_to_follow_you": "%s requested to follow you",
|
||||
"user_your_poll_has_ended": "%s Your poll has ended",
|
||||
"notification_description": {
|
||||
"followed_you": "followd you",
|
||||
"favorited_your_post": "favorited your post",
|
||||
"reblogged_your_post": "reblogged your post",
|
||||
"mentioned_you": "mentioned you",
|
||||
"request_to_follow_you": "request to follow you",
|
||||
"poll_has_ended": "poll has ended"
|
||||
},
|
||||
"keyobard": {
|
||||
"show_everything": "Show Everything",
|
||||
"show_mentions": "Show Mentions"
|
||||
|
@ -496,6 +515,13 @@
|
|||
"light": "Always Light",
|
||||
"dark": "Always Dark"
|
||||
},
|
||||
"look_and_feel": {
|
||||
"title": "Look and Feel",
|
||||
"use_system": "Use System",
|
||||
"really_dark": "Really Dark",
|
||||
"sorta_dark": "Sorta Dark",
|
||||
"light": "Light"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notifications",
|
||||
"favorites": "Favorites my post",
|
||||
|
@ -537,14 +563,17 @@
|
|||
}
|
||||
},
|
||||
"report": {
|
||||
"title_report": "Report",
|
||||
"title": "Report %s",
|
||||
"step1": "Step 1 of 2",
|
||||
"step2": "Step 2 of 2",
|
||||
"content1": "Are there any other posts you’d like to add to the report?",
|
||||
"content2": "Is there anything the moderators should know about this report?",
|
||||
"report_sent_title": "Thanks for reporting, we’ll look into this.",
|
||||
"send": "Send Report",
|
||||
"skip_to_send": "Send without comment",
|
||||
"text_placeholder": "Type or paste additional comments"
|
||||
"text_placeholder": "Type or paste additional comments",
|
||||
"reported": "REPORTED"
|
||||
},
|
||||
"preview": {
|
||||
"keyboard": {
|
||||
|
@ -564,4 +593,4 @@
|
|||
"accessibility_hint": "Double tap to dismiss this wizard"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,108 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1250"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DB427DD125BAA00100D1B89D"
|
||||
BuildableName = "Mastodon.app"
|
||||
BlueprintName = "Mastodon"
|
||||
ReferencedContainer = "container:Mastodon.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "ASDK - Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DB427DE725BAA00100D1B89D"
|
||||
BuildableName = "MastodonTests.xctest"
|
||||
BlueprintName = "MastodonTests"
|
||||
ReferencedContainer = "container:Mastodon.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DB427DF225BAA00100D1B89D"
|
||||
BuildableName = "MastodonUITests.xctest"
|
||||
BlueprintName = "MastodonUITests"
|
||||
ReferencedContainer = "container:Mastodon.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DB89B9F525C10FD0008580ED"
|
||||
BuildableName = "CoreDataStackTests.xctest"
|
||||
BlueprintName = "CoreDataStackTests"
|
||||
ReferencedContainer = "container:Mastodon.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "ASDK - Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DB427DD125BAA00100D1B89D"
|
||||
BuildableName = "Mastodon.app"
|
||||
BlueprintName = "Mastodon"
|
||||
ReferencedContainer = "container:Mastodon.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "ASDK - Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DB427DD125BAA00100D1B89D"
|
||||
BuildableName = "Mastodon.app"
|
||||
BlueprintName = "Mastodon"
|
||||
ReferencedContainer = "container:Mastodon.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "ASDK - Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "ASDK - Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
|
@ -7,18 +7,13 @@
|
|||
<key>AppShared.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>26</integer>
|
||||
<integer>33</integer>
|
||||
</dict>
|
||||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>27</integer>
|
||||
</dict>
|
||||
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
|
@ -102,7 +97,7 @@
|
|||
<key>MastodonIntent.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>25</integer>
|
||||
<integer>32</integer>
|
||||
</dict>
|
||||
<key>MastodonIntents.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
@ -117,15 +112,36 @@
|
|||
<key>NotificationService.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>3</integer>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>24</integer>
|
||||
<integer>31</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
<dict/>
|
||||
<dict>
|
||||
<key>DB427DD125BAA00100D1B89D</key>
|
||||
<dict>
|
||||
<key>primary</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>DB427DE725BAA00100D1B89D</key>
|
||||
<dict>
|
||||
<key>primary</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>DB427DF225BAA00100D1B89D</key>
|
||||
<dict>
|
||||
<key>primary</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>DB89B9F525C10FD0008580ED</key>
|
||||
<dict>
|
||||
<key>primary</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -55,15 +55,6 @@
|
|||
"version": "1.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "FLAnimatedImage",
|
||||
"repositoryURL": "https://github.com/Flipboard/FLAnimatedImage",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "e7f9fd4681ae41bf6f3056db08af4f401d61da52",
|
||||
"version": "1.0.16"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "FPSIndicator",
|
||||
"repositoryURL": "https://github.com/MainasuK/FPSIndicator.git",
|
||||
|
@ -96,8 +87,8 @@
|
|||
"repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "7af4182f64329440a4656f2cba307cb5848e496a",
|
||||
"version": "2.1.2"
|
||||
"revision": "3ea336d3de7938dc112084c596a646e697b0feee",
|
||||
"version": "2.2.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -141,8 +132,8 @@
|
|||
"repositoryURL": "https://github.com/SDWebImage/SDWebImage.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "0fff0d7505b5306348263ea64fcc561253bbeb21",
|
||||
"version": "5.12.2"
|
||||
"revision": "2c53f531f1bedd253f55d85105409c28ed4a922c",
|
||||
"version": "5.12.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -195,8 +186,8 @@
|
|||
"repositoryURL": "https://github.com/uias/Tabman",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "f43489cdd743ba7ad86a422ebb5fcbf34e333df4",
|
||||
"version": "2.11.1"
|
||||
"revision": "a9f10cb862a32e6a22549836af013abd6b0692d3",
|
||||
"version": "2.12.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -213,8 +204,8 @@
|
|||
"repositoryURL": "https://github.com/TimOliver/TOCropViewController.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "dad97167bf1be16aeecd109130900995dd01c515",
|
||||
"version": "2.6.0"
|
||||
"revision": "d0470491f56e734731bbf77991944c0dfdee3e0e",
|
||||
"version": "2.6.1"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
sources:
|
||||
- .
|
||||
- ../MastodonSDK/Sources
|
||||
templates:
|
||||
- ./Template
|
||||
output:
|
||||
Generated
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
import UIKit
|
||||
import SafariServices
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
final class SafariActivity: UIActivity {
|
||||
|
||||
|
@ -55,8 +57,10 @@ final class SafariActivity: UIActivity {
|
|||
return
|
||||
}
|
||||
|
||||
sceneCoordinator?.present(scene: .safari(url: url as URL), from: nil, transition: .safariPresent(animated: true, completion: nil))
|
||||
activityDidFinish(true)
|
||||
Task {
|
||||
await sceneCoordinator?.present(scene: .safari(url: url as URL), from: nil, transition: .safariPresent(animated: true, completion: nil))
|
||||
activityDidFinish(true)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
//
|
||||
// ShareActivityProvider.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-1-25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
protocol ShareActivityProvider {
|
||||
var activities: [Any] { get }
|
||||
var applicationActivities: [UIActivity] { get }
|
||||
}
|
|
@ -10,6 +10,8 @@ import SafariServices
|
|||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import PanModal
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
final public class SceneCoordinator {
|
||||
|
||||
|
@ -43,7 +45,7 @@ final public class SceneCoordinator {
|
|||
return Just(nil).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
let accessToken = pushNotification._accessToken // use raw accessToken value without normalize
|
||||
let accessToken = pushNotification.accessToken // use raw accessToken value without normalize
|
||||
if currentActiveAuthenticationBox.userAuthorization.accessToken == accessToken {
|
||||
// do nothing if notification for current account
|
||||
return Just(pushNotification).eraseToAnyPublisher()
|
||||
|
@ -182,6 +184,8 @@ extension SceneCoordinator {
|
|||
|
||||
// report
|
||||
case report(viewModel: ReportViewModel)
|
||||
case reportSupplementary(viewModel: ReportSupplementaryViewModel)
|
||||
case reportResult(viewModel: ReportResultViewModel)
|
||||
|
||||
// suggestion account
|
||||
case suggestionAccount(viewModel: SuggestionAccountViewModel)
|
||||
|
@ -194,10 +198,6 @@ extension SceneCoordinator {
|
|||
case alertController(alertController: UIAlertController)
|
||||
case activityViewController(activityViewController: UIActivityViewController, sourceView: UIView?, barButtonItem: UIBarButtonItem?)
|
||||
|
||||
#if DEBUG
|
||||
case publicTimeline
|
||||
#endif
|
||||
|
||||
var isOnboarding: Bool {
|
||||
switch self {
|
||||
case .welcome,
|
||||
|
@ -211,7 +211,7 @@ extension SceneCoordinator {
|
|||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
} // end enum Scene { }
|
||||
}
|
||||
|
||||
extension SceneCoordinator {
|
||||
|
@ -266,6 +266,7 @@ extension SceneCoordinator {
|
|||
}
|
||||
|
||||
@discardableResult
|
||||
@MainActor
|
||||
func present(scene: Scene, from sender: UIViewController?, transition: Transition) -> UIViewController? {
|
||||
guard let viewController = get(scene: scene) else {
|
||||
return nil
|
||||
|
@ -442,6 +443,18 @@ private extension SceneCoordinator {
|
|||
let _viewController = FollowingListViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .report(let viewModel):
|
||||
let _viewController = ReportViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .reportSupplementary(let viewModel):
|
||||
let _viewController = ReportSupplementaryViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .reportResult(let viewModel):
|
||||
let _viewController = ReportResultViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .suggestionAccount(let viewModel):
|
||||
let _viewController = SuggestionAccountViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
|
@ -477,16 +490,6 @@ private extension SceneCoordinator {
|
|||
let _viewController = SettingsViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .report(let viewModel):
|
||||
let _viewController = ReportViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
#if DEBUG
|
||||
case .publicTimeline:
|
||||
let _viewController = PublicTimelineViewController()
|
||||
_viewController.viewModel = PublicTimelineViewModel(context: appContext)
|
||||
viewController = _viewController
|
||||
#endif
|
||||
}
|
||||
|
||||
setupDependency(for: viewController as? NeedsDependency)
|
||||
|
|
|
@ -1,145 +0,0 @@
|
|||
//
|
||||
// PickServerCategoriesCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by BradGao on 2021/2/23.
|
||||
//
|
||||
|
||||
//import os.log
|
||||
//import UIKit
|
||||
//import MastodonSDK
|
||||
//
|
||||
//protocol PickServerCategoriesCellDelegate: AnyObject {
|
||||
// func pickServerCategoriesCell(_ cell: PickServerCategoriesCell, collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
|
||||
//}
|
||||
//
|
||||
//final class PickServerCategoriesCell: UITableViewCell {
|
||||
//
|
||||
// weak var delegate: PickServerCategoriesCellDelegate?
|
||||
//
|
||||
// var diffableDataSource: UICollectionViewDiffableDataSource<CategoryPickerSection, CategoryPickerItem>?
|
||||
//
|
||||
// let metricView = UIView()
|
||||
//
|
||||
// let collectionView: UICollectionView = {
|
||||
// let flowLayout = UICollectionViewFlowLayout()
|
||||
// flowLayout.scrollDirection = .horizontal
|
||||
// let view = ControlContainableCollectionView(frame: .zero, collectionViewLayout: flowLayout)
|
||||
// view.register(PickServerCategoryCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: PickServerCategoryCollectionViewCell.self))
|
||||
// view.backgroundColor = .clear
|
||||
// view.showsHorizontalScrollIndicator = false
|
||||
// view.showsVerticalScrollIndicator = false
|
||||
// view.layer.masksToBounds = false
|
||||
// view.translatesAutoresizingMaskIntoConstraints = false
|
||||
// return view
|
||||
// }()
|
||||
//
|
||||
// override func prepareForReuse() {
|
||||
// super.prepareForReuse()
|
||||
//
|
||||
// delegate = nil
|
||||
// }
|
||||
//
|
||||
// override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
// super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
// _init()
|
||||
// }
|
||||
//
|
||||
// required init?(coder: NSCoder) {
|
||||
// super.init(coder: coder)
|
||||
// _init()
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//extension PickServerCategoriesCell {
|
||||
//
|
||||
// private func _init() {
|
||||
// selectionStyle = .none
|
||||
// backgroundColor = Asset.Theme.Mastodon.systemGroupedBackground.color
|
||||
// configureMargin()
|
||||
//
|
||||
// metricView.translatesAutoresizingMaskIntoConstraints = false
|
||||
// contentView.addSubview(metricView)
|
||||
// NSLayoutConstraint.activate([
|
||||
// metricView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
|
||||
// metricView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
|
||||
// metricView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
// metricView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
// metricView.heightAnchor.constraint(equalToConstant: 80).priority(.defaultHigh),
|
||||
// ])
|
||||
//
|
||||
// contentView.addSubview(collectionView)
|
||||
// NSLayoutConstraint.activate([
|
||||
// collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||
// collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||
// collectionView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
|
||||
// contentView.bottomAnchor.constraint(equalTo: collectionView.bottomAnchor, constant: 20),
|
||||
// collectionView.heightAnchor.constraint(equalToConstant: 80).priority(.defaultHigh),
|
||||
// ])
|
||||
//
|
||||
// collectionView.delegate = self
|
||||
// }
|
||||
//
|
||||
// override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
// super.traitCollectionDidChange(previousTraitCollection)
|
||||
//
|
||||
// configureMargin()
|
||||
// }
|
||||
//
|
||||
// override func layoutSubviews() {
|
||||
// super.layoutSubviews()
|
||||
//
|
||||
// collectionView.collectionViewLayout.invalidateLayout()
|
||||
// }
|
||||
//
|
||||
//}
|
||||
//
|
||||
//extension PickServerCategoriesCell {
|
||||
// private func configureMargin() {
|
||||
// switch traitCollection.horizontalSizeClass {
|
||||
// case .regular:
|
||||
// let margin = MastodonPickServerViewController.viewEdgeMargin
|
||||
// contentView.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin)
|
||||
// default:
|
||||
// contentView.layoutMargins = .zero
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//// MARK: - UICollectionViewDelegateFlowLayout
|
||||
//extension PickServerCategoriesCell: UICollectionViewDelegateFlowLayout {
|
||||
//
|
||||
// func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: indexPath: %s", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription)
|
||||
// collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally)
|
||||
// delegate?.pickServerCategoriesCell(self, collectionView: collectionView, didSelectItemAt: indexPath)
|
||||
// }
|
||||
//
|
||||
// func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
|
||||
// layoutIfNeeded()
|
||||
// return UIEdgeInsets(top: 0, left: metricView.frame.minX - collectionView.frame.minX, bottom: 0, right: collectionView.frame.maxX - metricView.frame.maxX)
|
||||
// }
|
||||
//
|
||||
// func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
|
||||
// return 16
|
||||
// }
|
||||
//
|
||||
// func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||||
// return CGSize(width: 60, height: 80)
|
||||
// }
|
||||
//
|
||||
//}
|
||||
//
|
||||
//extension PickServerCategoriesCell {
|
||||
//
|
||||
// override func accessibilityElementCount() -> Int {
|
||||
// guard let diffableDataSource = diffableDataSource else { return 0 }
|
||||
// return diffableDataSource.snapshot().itemIdentifiers.count
|
||||
// }
|
||||
//
|
||||
// override func accessibilityElement(at index: Int) -> Any? {
|
||||
// guard let item = collectionView.cellForItem(at: IndexPath(item: index, section: 0)) else { return nil }
|
||||
// return item
|
||||
// }
|
||||
//
|
||||
//}
|
|
@ -1,171 +0,0 @@
|
|||
//
|
||||
// PickServerSearchCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by BradGao on 2021/2/24.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
//protocol PickServerSearchCellDelegate: AnyObject {
|
||||
// func pickServerSearchCell(_ cell: PickServerSearchCell, searchTextDidChange searchText: String?)
|
||||
//}
|
||||
//
|
||||
//class PickServerSearchCell: UITableViewCell {
|
||||
//
|
||||
// weak var delegate: PickServerSearchCellDelegate?
|
||||
//
|
||||
// private var bgView: UIView = {
|
||||
// let view = UIView()
|
||||
// view.backgroundColor = Asset.Theme.Mastodon.secondaryGroupedSystemBackground.color
|
||||
// view.translatesAutoresizingMaskIntoConstraints = false
|
||||
// view.layer.maskedCorners = [
|
||||
// .layerMinXMinYCorner,
|
||||
// .layerMaxXMinYCorner
|
||||
// ]
|
||||
// view.layer.cornerCurve = .continuous
|
||||
// view.layer.cornerRadius = MastodonPickServerAppearance.tableViewCornerRadius
|
||||
// return view
|
||||
// }()
|
||||
//
|
||||
// private var textFieldBgView: UIView = {
|
||||
// let view = UIView()
|
||||
// view.backgroundColor = Asset.Colors.TextField.background.color
|
||||
// view.translatesAutoresizingMaskIntoConstraints = false
|
||||
// view.layer.masksToBounds = true
|
||||
// view.layer.cornerRadius = 6
|
||||
// view.layer.cornerCurve = .continuous
|
||||
// return view
|
||||
// }()
|
||||
//
|
||||
// let searchTextField: UITextField = {
|
||||
// let textField = UITextField()
|
||||
// textField.translatesAutoresizingMaskIntoConstraints = false
|
||||
// textField.leftView = {
|
||||
// let imageView = UIImageView(
|
||||
// image: UIImage(
|
||||
// systemName: "magnifyingglass",
|
||||
// withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .regular)
|
||||
// )
|
||||
// )
|
||||
// imageView.tintColor = Asset.Colors.Label.secondary.color.withAlphaComponent(0.6)
|
||||
//
|
||||
// let containerView = UIView()
|
||||
// imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
// containerView.addSubview(imageView)
|
||||
// NSLayoutConstraint.activate([
|
||||
// imageView.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||
// imageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
|
||||
// imageView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
||||
// ])
|
||||
//
|
||||
// let paddingView = UIView()
|
||||
// paddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
// containerView.addSubview(paddingView)
|
||||
// NSLayoutConstraint.activate([
|
||||
// paddingView.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||
// paddingView.leadingAnchor.constraint(equalTo: imageView.trailingAnchor),
|
||||
// paddingView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
|
||||
// paddingView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
||||
// paddingView.widthAnchor.constraint(equalToConstant: 4).priority(.defaultHigh),
|
||||
// ])
|
||||
// return containerView
|
||||
// }()
|
||||
// textField.leftViewMode = .always
|
||||
// textField.font = .systemFont(ofSize: 15, weight: .regular)
|
||||
// textField.tintColor = Asset.Colors.Label.primary.color
|
||||
// textField.textColor = Asset.Colors.Label.primary.color
|
||||
// textField.adjustsFontForContentSizeCategory = true
|
||||
// textField.attributedPlaceholder =
|
||||
// NSAttributedString(string: L10n.Scene.ServerPicker.Input.placeholder,
|
||||
// attributes: [.font: UIFont.systemFont(ofSize: 15, weight: .regular),
|
||||
// .foregroundColor: Asset.Colors.Label.secondary.color.withAlphaComponent(0.6)])
|
||||
// textField.clearButtonMode = .whileEditing
|
||||
// textField.autocapitalizationType = .none
|
||||
// textField.autocorrectionType = .no
|
||||
// textField.returnKeyType = .done
|
||||
// textField.keyboardType = .URL
|
||||
// return textField
|
||||
// }()
|
||||
//
|
||||
// override func prepareForReuse() {
|
||||
// super.prepareForReuse()
|
||||
//
|
||||
// delegate = nil
|
||||
// }
|
||||
//
|
||||
// override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
// super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
// _init()
|
||||
// }
|
||||
//
|
||||
// required init?(coder: NSCoder) {
|
||||
// super.init(coder: coder)
|
||||
// _init()
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//extension PickServerSearchCell {
|
||||
// private func _init() {
|
||||
// selectionStyle = .none
|
||||
// backgroundColor = Asset.Theme.Mastodon.systemGroupedBackground.color
|
||||
// configureMargin()
|
||||
//
|
||||
// searchTextField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
|
||||
// searchTextField.delegate = self
|
||||
//
|
||||
// contentView.addSubview(bgView)
|
||||
// contentView.addSubview(textFieldBgView)
|
||||
// contentView.addSubview(searchTextField)
|
||||
//
|
||||
// NSLayoutConstraint.activate([
|
||||
// bgView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
|
||||
// bgView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
// bgView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
|
||||
// bgView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
//
|
||||
// textFieldBgView.leadingAnchor.constraint(equalTo: bgView.leadingAnchor, constant: 14),
|
||||
// textFieldBgView.topAnchor.constraint(equalTo: bgView.topAnchor, constant: 12),
|
||||
// bgView.trailingAnchor.constraint(equalTo: textFieldBgView.trailingAnchor, constant: 14),
|
||||
// bgView.bottomAnchor.constraint(equalTo: textFieldBgView.bottomAnchor, constant: 13),
|
||||
//
|
||||
// searchTextField.leadingAnchor.constraint(equalTo: textFieldBgView.leadingAnchor, constant: 11),
|
||||
// searchTextField.topAnchor.constraint(equalTo: textFieldBgView.topAnchor, constant: 4),
|
||||
// textFieldBgView.trailingAnchor.constraint(equalTo: searchTextField.trailingAnchor, constant: 11),
|
||||
// textFieldBgView.bottomAnchor.constraint(equalTo: searchTextField.bottomAnchor, constant: 4),
|
||||
// ])
|
||||
// }
|
||||
//
|
||||
// override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
// super.traitCollectionDidChange(previousTraitCollection)
|
||||
//
|
||||
// configureMargin()
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//extension PickServerSearchCell {
|
||||
// private func configureMargin() {
|
||||
// switch traitCollection.horizontalSizeClass {
|
||||
// case .regular:
|
||||
// let margin = MastodonPickServerViewController.viewEdgeMargin
|
||||
// contentView.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin)
|
||||
// default:
|
||||
// contentView.layoutMargins = .zero
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//extension PickServerSearchCell {
|
||||
// @objc private func textFieldDidChange(_ textField: UITextField) {
|
||||
// delegate?.pickServerSearchCell(self, searchTextDidChange: textField.text)
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//// MARK: - UITextFieldDelegate
|
||||
//extension PickServerSearchCell: UITextFieldDelegate {
|
||||
//
|
||||
// func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
// textField.resignFirstResponder()
|
||||
// return false
|
||||
// }
|
||||
//}
|
|
@ -7,32 +7,9 @@
|
|||
|
||||
import CoreData
|
||||
import Foundation
|
||||
import CoreDataStack
|
||||
|
||||
enum SelectedAccountItem {
|
||||
case accountObjectID(accountObjectID: NSManagedObjectID)
|
||||
enum SelectedAccountItem: Hashable {
|
||||
case account(ManagedObjectRecord<MastodonUser>)
|
||||
case placeHolder(uuid: UUID)
|
||||
}
|
||||
|
||||
extension SelectedAccountItem: Equatable {
|
||||
static func == (lhs: SelectedAccountItem, rhs: SelectedAccountItem) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.accountObjectID(let idLeft), .accountObjectID(let idRight)):
|
||||
return idLeft == idRight
|
||||
case (.placeHolder(let uuidLeft), .placeHolder(let uuidRight)):
|
||||
return uuidLeft == uuidRight
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SelectedAccountItem: Hashable {
|
||||
func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
case .accountObjectID(let id):
|
||||
hasher.combine(id)
|
||||
case .placeHolder(let id):
|
||||
hasher.combine(id.uuidString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,15 +17,17 @@ enum SelectedAccountSection: Equatable, Hashable {
|
|||
|
||||
extension SelectedAccountSection {
|
||||
static func collectionViewDiffableDataSource(
|
||||
for collectionView: UICollectionView,
|
||||
managedObjectContext: NSManagedObjectContext
|
||||
collectionView: UICollectionView,
|
||||
context: AppContext
|
||||
) -> UICollectionViewDiffableDataSource<SelectedAccountSection, SelectedAccountItem> {
|
||||
UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, item -> UICollectionViewCell? in
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: SuggestionAccountCollectionViewCell.self), for: indexPath) as! SuggestionAccountCollectionViewCell
|
||||
switch item {
|
||||
case .accountObjectID(let objectID):
|
||||
let user = managedObjectContext.object(with: objectID) as! MastodonUser
|
||||
cell.config(with: user)
|
||||
case .account(let record):
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let user = record.object(in: context.managedObjectContext) else { return }
|
||||
cell.config(with: user)
|
||||
}
|
||||
case .placeHolder:
|
||||
cell.configAsPlaceHolder()
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
import UIKit
|
||||
import MastodonSDK
|
||||
import MastodonMeta
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
enum AutoCompleteSection: Equatable, Hashable {
|
||||
case main
|
||||
|
@ -80,7 +82,7 @@ extension AutoCompleteSection {
|
|||
}
|
||||
cell.subtitleLabel.text = "@" + account.acct
|
||||
cell.avatarImageView.isHidden = false
|
||||
cell.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: URL(string: account.avatar)))
|
||||
cell.avatarImageView.configure(configuration: .init(url: URL(string: account.avatar)))
|
||||
}
|
||||
|
||||
private static func configureEmoji(cell: AutoCompleteTableViewCell, emoji: Mastodon.Entity.Emoji, isFirst: Bool) {
|
||||
|
@ -90,7 +92,7 @@ extension AutoCompleteSection {
|
|||
// cell.subtitleLabel.text = isFirst ? L10n.Scene.Compose.AutoComplete.spaceToAdd : " "
|
||||
cell.subtitleLabel.text = " "
|
||||
cell.avatarImageView.isHidden = false
|
||||
cell.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: URL(string: emoji.url)))
|
||||
cell.avatarImageView.configure(configuration: .init(url: URL(string: emoji.url)))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,11 +9,12 @@ import Foundation
|
|||
import Combine
|
||||
import CoreData
|
||||
import MastodonMeta
|
||||
import CoreDataStack
|
||||
|
||||
/// Note: update Equatable when change case
|
||||
enum ComposeStatusItem {
|
||||
case replyTo(statusObjectID: NSManagedObjectID)
|
||||
case input(replyToStatusObjectID: NSManagedObjectID?, attribute: ComposeStatusAttribute)
|
||||
case replyTo(record: ManagedObjectRecord<Status>)
|
||||
case input(replyTo: ManagedObjectRecord<Status>?, attribute: ComposeStatusAttribute)
|
||||
case attachment(attachmentAttribute: ComposeStatusAttachmentAttribute)
|
||||
case pollOption(pollOptionAttributes: [ComposeStatusPollItem.PollOptionAttribute], pollExpiresOptionAttribute: ComposeStatusPollItem.PollExpiresOptionAttribute)
|
||||
}
|
||||
|
@ -21,26 +22,21 @@ enum ComposeStatusItem {
|
|||
extension ComposeStatusItem: Hashable { }
|
||||
|
||||
extension ComposeStatusItem {
|
||||
final class ComposeStatusAttribute: Equatable, Hashable {
|
||||
final class ComposeStatusAttribute: Hashable {
|
||||
private let id = UUID()
|
||||
|
||||
let avatarURL = CurrentValueSubject<URL?, Never>(nil)
|
||||
let displayName = CurrentValueSubject<String?, Never>(nil)
|
||||
let emojiMeta = CurrentValueSubject<MastodonContent.Emojis, Never>([:])
|
||||
let username = CurrentValueSubject<String?, Never>(nil)
|
||||
let composeContent = CurrentValueSubject<String?, Never>(nil)
|
||||
|
||||
let isContentWarningComposing = CurrentValueSubject<Bool, Never>(false)
|
||||
let contentWarningContent = CurrentValueSubject<String, Never>("")
|
||||
@Published var author: ManagedObjectRecord<MastodonUser>?
|
||||
|
||||
@Published var composeContent: String?
|
||||
|
||||
@Published var isContentWarningComposing = false
|
||||
@Published var contentWarningContent = ""
|
||||
|
||||
static func == (lhs: ComposeStatusAttribute, rhs: ComposeStatusAttribute) -> Bool {
|
||||
return lhs.avatarURL.value == rhs.avatarURL.value &&
|
||||
lhs.displayName.value == rhs.displayName.value &&
|
||||
lhs.emojiMeta.value == rhs.emojiMeta.value &&
|
||||
lhs.username.value == rhs.username.value &&
|
||||
lhs.composeContent.value == rhs.composeContent.value &&
|
||||
lhs.isContentWarningComposing.value == rhs.isContentWarningComposing.value &&
|
||||
lhs.contentWarningContent.value == rhs.contentWarningContent.value
|
||||
return lhs.author == rhs.author
|
||||
&& lhs.composeContent == rhs.composeContent
|
||||
&& lhs.isContentWarningComposing == rhs.isContentWarningComposing
|
||||
&& lhs.contentWarningContent == rhs.contentWarningContent
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
import Foundation
|
||||
import Combine
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
enum ComposeStatusPollItem {
|
||||
case pollOption(attribute: PollOptionAttribute)
|
||||
|
|
|
@ -14,7 +14,7 @@ import MastodonMeta
|
|||
import AlamofireImage
|
||||
|
||||
enum ComposeStatusSection: Equatable, Hashable {
|
||||
case repliedTo
|
||||
case replyTo
|
||||
case status
|
||||
case attachment
|
||||
case poll
|
||||
|
@ -24,43 +24,44 @@ extension ComposeStatusSection {
|
|||
enum ComposeKind {
|
||||
case post
|
||||
case hashtag(hashtag: String)
|
||||
case mention(mastodonUserObjectID: NSManagedObjectID)
|
||||
case reply(repliedToStatusObjectID: NSManagedObjectID)
|
||||
case mention(user: ManagedObjectRecord<MastodonUser>)
|
||||
case reply(status: ManagedObjectRecord<Status>)
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeStatusSection {
|
||||
|
||||
static func configureStatusContent(
|
||||
static func configure(
|
||||
cell: ComposeStatusContentTableViewCell,
|
||||
attribute: ComposeStatusItem.ComposeStatusAttribute
|
||||
) {
|
||||
// set avatar
|
||||
attribute.avatarURL
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { avatarURL in
|
||||
cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: avatarURL))
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
// set display name and username
|
||||
Publishers.CombineLatest3(
|
||||
attribute.displayName,
|
||||
attribute.emojiMeta,
|
||||
attribute.username
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { displayName, emojiMeta, username in
|
||||
do {
|
||||
let mastodonContent = MastodonContent(content: displayName ?? " ", emojis: emojiMeta)
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
cell.statusView.nameLabel.configure(content: metaContent)
|
||||
} catch {
|
||||
let metaContent = PlaintextMetaContent(string: " ")
|
||||
cell.statusView.nameLabel.configure(content: metaContent)
|
||||
}
|
||||
cell.statusView.usernameLabel.text = username.flatMap { "@" + $0 } ?? " "
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
// cell.prepa
|
||||
// // set avatar
|
||||
// attribute.avatarURL
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .sink { avatarURL in
|
||||
// cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: avatarURL))
|
||||
// }
|
||||
// .store(in: &cell.disposeBag)
|
||||
// // set display name and username
|
||||
// Publishers.CombineLatest3(
|
||||
// attribute.displayName,
|
||||
// attribute.emojiMeta,
|
||||
// attribute.username
|
||||
// )
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .sink { displayName, emojiMeta, username in
|
||||
// do {
|
||||
// let mastodonContent = MastodonContent(content: displayName ?? " ", emojis: emojiMeta)
|
||||
// let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
// cell.statusView.nameLabel.configure(content: metaContent)
|
||||
// } catch {
|
||||
// let metaContent = PlaintextMetaContent(string: " ")
|
||||
// cell.statusView.nameLabel.configure(content: metaContent)
|
||||
// }
|
||||
// cell.statusView.usernameLabel.text = username.flatMap { "@" + $0 } ?? " "
|
||||
// }
|
||||
// .store(in: &cell.disposeBag)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
//
|
||||
// FeedFetchedResultsController.swift
|
||||
// FeedFetchedResultsController
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-8-19.
|
||||
// Copyright © 2021 Twidere. All rights reserved.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
final public class FeedFetchedResultsController: NSObject {
|
||||
|
||||
public let logger = Logger(subsystem: "FeedFetchedResultsController", category: "DB")
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
public let fetchedResultsController: NSFetchedResultsController<Feed>
|
||||
|
||||
// input
|
||||
@Published public var predicate = Feed.predicate(kind: .none, acct: .none)
|
||||
|
||||
// output
|
||||
private let _objectIDs = PassthroughSubject<[NSManagedObjectID], Never>()
|
||||
@Published public var records: [ManagedObjectRecord<Feed>] = []
|
||||
|
||||
public init(managedObjectContext: NSManagedObjectContext) {
|
||||
self.fetchedResultsController = {
|
||||
let fetchRequest = Feed.sortedFetchRequest
|
||||
// make sure initial query return empty results
|
||||
fetchRequest.returnsObjectsAsFaults = false
|
||||
fetchRequest.shouldRefreshRefetchedObjects = true
|
||||
fetchRequest.fetchBatchSize = 15
|
||||
let controller = NSFetchedResultsController(
|
||||
fetchRequest: fetchRequest,
|
||||
managedObjectContext: managedObjectContext,
|
||||
sectionNameKeyPath: nil,
|
||||
cacheName: nil
|
||||
)
|
||||
|
||||
return controller
|
||||
}()
|
||||
super.init()
|
||||
|
||||
// debounce output to prevent UI update issues
|
||||
_objectIDs
|
||||
.throttle(for: 0.1, scheduler: DispatchQueue.main, latest: true)
|
||||
.map { objectIDs in objectIDs.map { ManagedObjectRecord(objectID: $0) } }
|
||||
.assign(to: &$records)
|
||||
|
||||
fetchedResultsController.delegate = self
|
||||
|
||||
$predicate
|
||||
.removeDuplicates()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] predicate in
|
||||
guard let self = self else { return }
|
||||
self.fetchedResultsController.fetchRequest.predicate = predicate
|
||||
do {
|
||||
try self.fetchedResultsController.performFetch()
|
||||
} catch {
|
||||
assertionFailure(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
deinit {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - NSFetchedResultsControllerDelegate
|
||||
extension FeedFetchedResultsController: NSFetchedResultsControllerDelegate {
|
||||
public func controller(
|
||||
_ controller: NSFetchedResultsController<NSFetchRequestResult>,
|
||||
didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference
|
||||
) {
|
||||
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
let snapshot = snapshot as NSDiffableDataSourceSnapshot<String, NSManagedObjectID>
|
||||
self._objectIDs.send(snapshot.itemIdentifiers)
|
||||
}
|
||||
}
|
||||
|
|
@ -21,8 +21,9 @@ final class SearchHistoryFetchedResultController: NSObject {
|
|||
let userID = CurrentValueSubject<Mastodon.Entity.Status.ID?, Never>(nil)
|
||||
|
||||
// output
|
||||
let objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([])
|
||||
|
||||
let _objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([])
|
||||
@Published var records: [ManagedObjectRecord<SearchHistory>] = []
|
||||
|
||||
init(managedObjectContext: NSManagedObjectContext) {
|
||||
self.fetchedResultsController = {
|
||||
let fetchRequest = SearchHistory.sortedFetchRequest
|
||||
|
@ -38,12 +39,18 @@ final class SearchHistoryFetchedResultController: NSObject {
|
|||
return controller
|
||||
}()
|
||||
super.init()
|
||||
|
||||
// debounce output to prevent UI update issues
|
||||
_objectIDs
|
||||
.throttle(for: 0.1, scheduler: DispatchQueue.main, latest: true)
|
||||
.map { objectIDs in objectIDs.map { ManagedObjectRecord(objectID: $0) } }
|
||||
.assign(to: &$records)
|
||||
|
||||
fetchedResultsController.delegate = self
|
||||
|
||||
Publishers.CombineLatest(
|
||||
self.domain.removeDuplicates(),
|
||||
self.userID.removeDuplicates()
|
||||
self.domain,
|
||||
self.userID
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] domain, userID in
|
||||
|
@ -67,6 +74,6 @@ extension SearchHistoryFetchedResultController: NSFetchedResultsControllerDelega
|
|||
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
|
||||
let objects = fetchedResultsController.fetchedObjects ?? []
|
||||
self.objectIDs.value = objects.map { $0.objectID }
|
||||
self._objectIDs.value = objects.map { $0.objectID }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import Combine
|
|||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import MastodonUI
|
||||
|
||||
final class StatusFetchedResultsController: NSObject {
|
||||
|
||||
|
@ -23,7 +24,8 @@ final class StatusFetchedResultsController: NSObject {
|
|||
let statusIDs = CurrentValueSubject<[Mastodon.Entity.Status.ID], Never>([])
|
||||
|
||||
// output
|
||||
let objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([])
|
||||
let _objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([])
|
||||
@Published var records: [ManagedObjectRecord<Status>] = []
|
||||
|
||||
init(managedObjectContext: NSManagedObjectContext, domain: String?, additionalTweetPredicate: NSPredicate?) {
|
||||
self.domain.value = domain ?? ""
|
||||
|
@ -43,11 +45,17 @@ final class StatusFetchedResultsController: NSObject {
|
|||
}()
|
||||
super.init()
|
||||
|
||||
// debounce output to prevent UI update issues
|
||||
_objectIDs
|
||||
.throttle(for: 0.1, scheduler: DispatchQueue.main, latest: true)
|
||||
.map { objectIDs in objectIDs.map { ManagedObjectRecord(objectID: $0) } }
|
||||
.assign(to: &$records)
|
||||
|
||||
fetchedResultsController.delegate = self
|
||||
|
||||
Publishers.CombineLatest(
|
||||
self.domain.removeDuplicates().eraseToAnyPublisher(),
|
||||
self.statusIDs.removeDuplicates().eraseToAnyPublisher()
|
||||
self.domain.removeDuplicates(),
|
||||
self.statusIDs.removeDuplicates()
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] domain, ids in
|
||||
|
@ -68,6 +76,18 @@ final class StatusFetchedResultsController: NSObject {
|
|||
|
||||
}
|
||||
|
||||
extension StatusFetchedResultsController {
|
||||
|
||||
public func append(statusIDs: [Mastodon.Entity.Status.ID]) {
|
||||
var result = self.statusIDs.value
|
||||
for statusID in statusIDs where !result.contains(statusID) {
|
||||
result.append(statusID)
|
||||
}
|
||||
self.statusIDs.value = result
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - NSFetchedResultsControllerDelegate
|
||||
extension StatusFetchedResultsController: NSFetchedResultsControllerDelegate {
|
||||
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
|
||||
|
@ -82,6 +102,6 @@ extension StatusFetchedResultsController: NSFetchedResultsControllerDelegate {
|
|||
}
|
||||
.sorted { $0.0 < $1.0 }
|
||||
.map { $0.1.objectID }
|
||||
self.objectIDs.value = items
|
||||
self._objectIDs.value = items
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import Combine
|
|||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import MastodonUI
|
||||
|
||||
final class UserFetchedResultsController: NSObject {
|
||||
|
||||
|
@ -19,14 +20,15 @@ final class UserFetchedResultsController: NSObject {
|
|||
let fetchedResultsController: NSFetchedResultsController<MastodonUser>
|
||||
|
||||
// input
|
||||
let domain = CurrentValueSubject<String?, Never>(nil)
|
||||
let userIDs = CurrentValueSubject<[Mastodon.Entity.Account.ID], Never>([])
|
||||
@Published var domain: String? = nil
|
||||
@Published var userIDs: [Mastodon.Entity.Account.ID] = []
|
||||
|
||||
// output
|
||||
let objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([])
|
||||
let _objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([])
|
||||
@Published var records: [ManagedObjectRecord<MastodonUser>] = []
|
||||
|
||||
init(managedObjectContext: NSManagedObjectContext, domain: String?, additionalTweetPredicate: NSPredicate?) {
|
||||
self.domain.value = domain ?? ""
|
||||
self.domain = domain ?? ""
|
||||
self.fetchedResultsController = {
|
||||
let fetchRequest = MastodonUser.sortedFetchRequest
|
||||
fetchRequest.predicate = MastodonUser.predicate(domain: domain ?? "", ids: [])
|
||||
|
@ -42,12 +44,18 @@ final class UserFetchedResultsController: NSObject {
|
|||
return controller
|
||||
}()
|
||||
super.init()
|
||||
|
||||
// debounce output to prevent UI update issues
|
||||
_objectIDs
|
||||
.throttle(for: 0.1, scheduler: DispatchQueue.main, latest: true)
|
||||
.map { objectIDs in objectIDs.map { ManagedObjectRecord(objectID: $0) } }
|
||||
.assign(to: &$records)
|
||||
|
||||
fetchedResultsController.delegate = self
|
||||
|
||||
Publishers.CombineLatest(
|
||||
self.domain.removeDuplicates().eraseToAnyPublisher(),
|
||||
self.userIDs.removeDuplicates().eraseToAnyPublisher()
|
||||
self.$domain.removeDuplicates(),
|
||||
self.$userIDs.removeDuplicates()
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] domain, ids in
|
||||
|
@ -68,12 +76,24 @@ final class UserFetchedResultsController: NSObject {
|
|||
|
||||
}
|
||||
|
||||
extension UserFetchedResultsController {
|
||||
|
||||
public func append(userIDs: [Mastodon.Entity.Account.ID]) {
|
||||
var result = self.userIDs
|
||||
for userID in userIDs where !result.contains(userID) {
|
||||
result.append(userID)
|
||||
}
|
||||
self.userIDs = result
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - NSFetchedResultsControllerDelegate
|
||||
extension UserFetchedResultsController: NSFetchedResultsControllerDelegate {
|
||||
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
|
||||
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
|
||||
let indexes = userIDs.value
|
||||
let indexes = userIDs
|
||||
let objects = fetchedResultsController.fetchedObjects ?? []
|
||||
|
||||
let items: [NSManagedObjectID] = objects
|
||||
|
@ -82,6 +102,6 @@ extension UserFetchedResultsController: NSFetchedResultsControllerDelegate {
|
|||
}
|
||||
.sorted { $0.0 < $1.0 }
|
||||
.map { $0.1.objectID }
|
||||
self.objectIDs.value = items
|
||||
self._objectIDs.value = items
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,50 +7,10 @@
|
|||
|
||||
import CoreData
|
||||
import Foundation
|
||||
import CoreDataStack
|
||||
|
||||
enum NotificationItem {
|
||||
case notification(objectID: NSManagedObjectID, attribute: Item.StatusAttribute)
|
||||
case notificationStatus(objectID: NSManagedObjectID, attribute: Item.StatusAttribute) // display notification status without card wrapper
|
||||
enum NotificationItem: Hashable {
|
||||
case feed(record: ManagedObjectRecord<Feed>)
|
||||
case feedLoader(record: ManagedObjectRecord<Feed>)
|
||||
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 (.notificationStatus(let idLeft, _), .notificationStatus(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 .notificationStatus(let id, _):
|
||||
hasher.combine(id)
|
||||
case .bottomLoader:
|
||||
hasher.combine(String(describing: NotificationItem.bottomLoader.self))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NotificationItem {
|
||||
var statusObjectItem: StatusObjectItem? {
|
||||
switch self {
|
||||
case .notification(let objectID, _):
|
||||
return .mastodonNotification(objectID: objectID)
|
||||
case .notificationStatus(let objectID, _):
|
||||
return .mastodonNotification(objectID: objectID)
|
||||
case .bottomLoader:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,234 +13,292 @@ import MastodonSDK
|
|||
import UIKit
|
||||
import MetaTextKit
|
||||
import MastodonMeta
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
enum NotificationSection: Equatable, Hashable {
|
||||
case main
|
||||
}
|
||||
|
||||
extension NotificationSection {
|
||||
static func tableViewDiffableDataSource(
|
||||
for tableView: UITableView,
|
||||
dependency: NeedsDependency,
|
||||
managedObjectContext: NSManagedObjectContext,
|
||||
delegate: NotificationTableViewCellDelegate,
|
||||
statusTableViewCellDelegate: StatusTableViewCellDelegate
|
||||
|
||||
struct Configuration {
|
||||
weak var notificationTableViewCellDelegate: NotificationTableViewCellDelegate?
|
||||
}
|
||||
|
||||
static func diffableDataSource(
|
||||
tableView: UITableView,
|
||||
context: AppContext,
|
||||
configuration: Configuration
|
||||
) -> 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 attribute):
|
||||
guard let notification = try? managedObjectContext.existingObject(with: objectID) as? MastodonNotification,
|
||||
!notification.isDeleted
|
||||
else { return UITableViewCell() }
|
||||
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationStatusTableViewCell.self), for: indexPath) as! NotificationStatusTableViewCell
|
||||
configure(
|
||||
tableView: tableView,
|
||||
cell: cell,
|
||||
notification: notification,
|
||||
dependency: dependency,
|
||||
attribute: attribute
|
||||
)
|
||||
cell.delegate = delegate
|
||||
cell.isAccessibilityElement = true
|
||||
NotificationSection.configureStatusAccessibilityLabel(cell: cell)
|
||||
tableView.register(NotificationTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationTableViewCell.self))
|
||||
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
|
||||
|
||||
return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
|
||||
switch item {
|
||||
case .feed(let record):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationTableViewCell.self), for: indexPath) as! NotificationTableViewCell
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let feed = record.object(in: context.managedObjectContext) else { return }
|
||||
configure(
|
||||
context: context,
|
||||
tableView: tableView,
|
||||
cell: cell,
|
||||
viewModel: NotificationTableViewCell.ViewModel(value: .feed(feed)),
|
||||
configuration: configuration
|
||||
)
|
||||
}
|
||||
return cell
|
||||
|
||||
case .notificationStatus(objectID: let objectID, attribute: let attribute):
|
||||
guard let notification = try? managedObjectContext.existingObject(with: objectID) as? MastodonNotification,
|
||||
!notification.isDeleted,
|
||||
let status = notification.status,
|
||||
let requestUserID = dependency.context.authenticationService.activeMastodonAuthenticationBox.value?.userID
|
||||
else { return UITableViewCell() }
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
|
||||
|
||||
// configure cell
|
||||
StatusSection.configureStatusTableViewCell(
|
||||
cell: cell,
|
||||
tableView: tableView,
|
||||
timelineContext: .notifications,
|
||||
dependency: dependency,
|
||||
readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
|
||||
status: status,
|
||||
requestUserID: requestUserID,
|
||||
statusItemAttribute: attribute
|
||||
)
|
||||
cell.statusView.headerContainerView.isHidden = true // set header hide
|
||||
cell.statusView.actionToolbarContainer.isHidden = true // set toolbar hide
|
||||
cell.statusView.actionToolbarPlaceholderPaddingView.isHidden = false
|
||||
cell.delegate = statusTableViewCellDelegate
|
||||
cell.isAccessibilityElement = true
|
||||
StatusSection.configureStatusAccessibilityLabel(cell: cell)
|
||||
case .feedLoader:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
||||
cell.activityIndicatorView.startAnimating()
|
||||
return cell
|
||||
|
||||
case .bottomLoader:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) as! TimelineBottomLoaderTableViewCell
|
||||
cell.startAnimating()
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
||||
cell.activityIndicatorView.startAnimating()
|
||||
return cell
|
||||
}
|
||||
// switch notificationItem {
|
||||
// case .notification(let objectID, let attribute):
|
||||
// guard let notification = try? managedObjectContext.existingObject(with: objectID) as? MastodonNotification,
|
||||
// !notification.isDeleted
|
||||
// else { return UITableViewCell() }
|
||||
//
|
||||
// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationStatusTableViewCell.self), for: indexPath) as! NotificationStatusTableViewCell
|
||||
// configure(
|
||||
// tableView: tableView,
|
||||
// cell: cell,
|
||||
// notification: notification,
|
||||
// dependency: dependency,
|
||||
// attribute: attribute
|
||||
// )
|
||||
// cell.delegate = delegate
|
||||
// cell.isAccessibilityElement = true
|
||||
// NotificationSection.configureStatusAccessibilityLabel(cell: cell)
|
||||
// return cell
|
||||
//
|
||||
// case .notificationStatus(objectID: let objectID, attribute: let attribute):
|
||||
// guard let notification = try? managedObjectContext.existingObject(with: objectID) as? MastodonNotification,
|
||||
// !notification.isDeleted,
|
||||
// let status = notification.status,
|
||||
// let requestUserID = dependency.context.authenticationService.activeMastodonAuthenticationBox.value?.userID
|
||||
// else { return UITableViewCell() }
|
||||
// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
|
||||
//
|
||||
// // configure cell
|
||||
// StatusSection.configureStatusTableViewCell(
|
||||
// cell: cell,
|
||||
// tableView: tableView,
|
||||
// timelineContext: .notifications,
|
||||
// dependency: dependency,
|
||||
// readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
|
||||
// status: status,
|
||||
// requestUserID: requestUserID,
|
||||
// statusItemAttribute: attribute
|
||||
// )
|
||||
// cell.statusView.headerContainerView.isHidden = true // set header hide
|
||||
// cell.statusView.actionToolbarContainer.isHidden = true // set toolbar hide
|
||||
// cell.statusView.actionToolbarPlaceholderPaddingView.isHidden = false
|
||||
// cell.delegate = statusTableViewCellDelegate
|
||||
// cell.isAccessibilityElement = true
|
||||
// StatusSection.configureStatusAccessibilityLabel(cell: cell)
|
||||
// return cell
|
||||
//
|
||||
// case .bottomLoader:
|
||||
// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) as! TimelineBottomLoaderTableViewCell
|
||||
// cell.startAnimating()
|
||||
// return cell
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NotificationSection {
|
||||
|
||||
static func configure(
|
||||
context: AppContext,
|
||||
tableView: UITableView,
|
||||
cell: NotificationStatusTableViewCell,
|
||||
notification: MastodonNotification,
|
||||
dependency: NeedsDependency,
|
||||
attribute: Item.StatusAttribute
|
||||
cell: NotificationTableViewCell,
|
||||
viewModel: NotificationTableViewCell.ViewModel,
|
||||
configuration: Configuration
|
||||
) {
|
||||
// configure author
|
||||
cell.configure(
|
||||
with: AvatarConfigurableViewConfiguration(
|
||||
avatarImageURL: notification.account.avatarImageURL()
|
||||
)
|
||||
StatusSection.setupStatusPollDataSource(
|
||||
context: context,
|
||||
statusView: cell.notificationView.statusView
|
||||
)
|
||||
|
||||
func createActionImage() -> UIImage? {
|
||||
return UIImage(
|
||||
systemName: notification.notificationType.actionImageName,
|
||||
withConfiguration: UIImage.SymbolConfiguration(
|
||||
pointSize: 12, weight: .semibold
|
||||
)
|
||||
)?
|
||||
.withTintColor(.systemBackground)
|
||||
.af.imageAspectScaled(toFit: CGSize(width: 14, height: 14))
|
||||
}
|
||||
StatusSection.setupStatusPollDataSource(
|
||||
context: context,
|
||||
statusView: cell.notificationView.quoteStatusView
|
||||
)
|
||||
|
||||
cell.avatarButton.badgeImageView.backgroundColor = notification.notificationType.color
|
||||
cell.avatarButton.badgeImageView.image = createActionImage()
|
||||
cell.traitCollectionDidChange
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak cell] in
|
||||
guard let cell = cell else { return }
|
||||
cell.avatarButton.badgeImageView.image = createActionImage()
|
||||
}
|
||||
context.authenticationService.activeMastodonAuthenticationBox
|
||||
.map { $0 as UserIdentifier? }
|
||||
.assign(to: \.userIdentifier, on: cell.notificationView.viewModel)
|
||||
.store(in: &cell.disposeBag)
|
||||
|
||||
// configure author name, notification description, timestamp
|
||||
let nameText = notification.account.displayNameWithFallback
|
||||
let titleLabelText: String = {
|
||||
switch notification.notificationType {
|
||||
case .favourite: return L10n.Scene.Notification.userFavoritedYourPost(nameText)
|
||||
case .follow: return L10n.Scene.Notification.userFollowedYou(nameText)
|
||||
case .followRequest: return L10n.Scene.Notification.userRequestedToFollowYou(nameText)
|
||||
case .mention: return L10n.Scene.Notification.userMentionedYou(nameText)
|
||||
case .poll: return L10n.Scene.Notification.userYourPollHasEnded(nameText)
|
||||
case .reblog: return L10n.Scene.Notification.userRebloggedYourPost(nameText)
|
||||
default: return ""
|
||||
}
|
||||
}()
|
||||
|
||||
do {
|
||||
let nameContent = MastodonContent(content: nameText, emojis: notification.account.emojiMeta)
|
||||
let nameMetaContent = try MastodonMetaContent.convert(document: nameContent)
|
||||
|
||||
let mastodonContent = MastodonContent(content: titleLabelText, emojis: notification.account.emojiMeta)
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
|
||||
cell.titleLabel.configure(content: metaContent)
|
||||
|
||||
if let nameRange = metaContent.string.range(of: nameMetaContent.string) {
|
||||
let nsRange = NSRange(nameRange, in: metaContent.string)
|
||||
cell.titleLabel.textStorage.addAttributes([
|
||||
.font: UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold), maximumPointSize: 20),
|
||||
.foregroundColor: Asset.Colors.brandBlue.color,
|
||||
], range: nsRange)
|
||||
}
|
||||
|
||||
} catch {
|
||||
let metaContent = PlaintextMetaContent(string: titleLabelText)
|
||||
cell.titleLabel.configure(content: metaContent)
|
||||
}
|
||||
|
||||
let createAt = notification.createAt
|
||||
cell.timestampLabel.text = createAt.localizedSlowedTimeAgoSinceNow
|
||||
AppContext.shared.timestampUpdatePublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak cell] _ in
|
||||
guard let cell = cell else { return }
|
||||
cell.timestampLabel.text = createAt.localizedSlowedTimeAgoSinceNow
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
|
||||
// configure follow request (if exist)
|
||||
if case .followRequest = notification.notificationType {
|
||||
cell.acceptButton.publisher(for: .touchUpInside)
|
||||
.sink { [weak cell] _ in
|
||||
guard let cell = cell else { return }
|
||||
cell.delegate?.notificationTableViewCell(cell, notification: notification, acceptButtonDidPressed: cell.acceptButton)
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
cell.rejectButton.publisher(for: .touchUpInside)
|
||||
.sink { [weak cell] _ in
|
||||
guard let cell = cell else { return }
|
||||
cell.delegate?.notificationTableViewCell(cell, notification: notification, rejectButtonDidPressed: cell.rejectButton)
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
cell.buttonStackView.isHidden = false
|
||||
} else {
|
||||
cell.buttonStackView.isHidden = true
|
||||
}
|
||||
|
||||
// configure status (if exist)
|
||||
if let status = notification.status {
|
||||
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,
|
||||
tableView: tableView,
|
||||
timelineContext: .notifications,
|
||||
dependency: dependency,
|
||||
readableLayoutFrame: frame,
|
||||
status: status,
|
||||
requestUserID: notification.userID,
|
||||
statusItemAttribute: attribute
|
||||
)
|
||||
cell.statusContainerView.isHidden = false
|
||||
cell.containerStackView.alignment = .top
|
||||
cell.containerStackViewBottomLayoutConstraint.constant = 0
|
||||
} else {
|
||||
if case .followRequest = notification.notificationType {
|
||||
cell.containerStackView.alignment = .top
|
||||
} else {
|
||||
cell.containerStackView.alignment = .center
|
||||
}
|
||||
cell.statusContainerView.isHidden = true
|
||||
cell.containerStackViewBottomLayoutConstraint.constant = 5 // 5pt margin when no status view
|
||||
}
|
||||
cell.configure(
|
||||
tableView: tableView,
|
||||
viewModel: viewModel,
|
||||
delegate: configuration.notificationTableViewCellDelegate
|
||||
)
|
||||
}
|
||||
|
||||
static func configureStatusAccessibilityLabel(cell: NotificationStatusTableViewCell) {
|
||||
// FIXME:
|
||||
cell.accessibilityLabel = {
|
||||
var accessibilityViews: [UIView?] = []
|
||||
accessibilityViews.append(contentsOf: [
|
||||
cell.titleLabel,
|
||||
cell.timestampLabel,
|
||||
cell.statusView
|
||||
])
|
||||
if !cell.statusContainerView.isHidden {
|
||||
if !cell.statusView.headerContainerView.isHidden {
|
||||
accessibilityViews.append(cell.statusView.headerInfoLabel)
|
||||
}
|
||||
accessibilityViews.append(contentsOf: [
|
||||
cell.statusView.nameMetaLabel,
|
||||
cell.statusView.dateLabel,
|
||||
cell.statusView.contentMetaText.textView,
|
||||
])
|
||||
}
|
||||
return accessibilityViews
|
||||
.compactMap { $0?.accessibilityLabel }
|
||||
.joined(separator: " ")
|
||||
}()
|
||||
}
|
||||
// static func configure(
|
||||
// tableView: UITableView,
|
||||
// cell: NotificationStatusTableViewCell,
|
||||
// notification: MastodonNotification,
|
||||
// dependency: NeedsDependency,
|
||||
// attribute: Item.StatusAttribute
|
||||
// ) {
|
||||
// // configure author
|
||||
// cell.configure(
|
||||
// with: AvatarConfigurableViewConfiguration(
|
||||
// avatarImageURL: notification.account.avatarImageURL()
|
||||
// )
|
||||
// )
|
||||
//
|
||||
// func createActionImage() -> UIImage? {
|
||||
// return UIImage(
|
||||
// systemName: notification.notificationType.actionImageName,
|
||||
// withConfiguration: UIImage.SymbolConfiguration(
|
||||
// pointSize: 12, weight: .semibold
|
||||
// )
|
||||
// )?
|
||||
// .withTintColor(.systemBackground)
|
||||
// .af.imageAspectScaled(toFit: CGSize(width: 14, height: 14))
|
||||
// }
|
||||
//
|
||||
// cell.avatarButton.badgeImageView.backgroundColor = notification.notificationType.color
|
||||
// cell.avatarButton.badgeImageView.image = createActionImage()
|
||||
// cell.traitCollectionDidChange
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .sink { [weak cell] in
|
||||
// guard let cell = cell else { return }
|
||||
// cell.avatarButton.badgeImageView.image = createActionImage()
|
||||
// }
|
||||
// .store(in: &cell.disposeBag)
|
||||
//
|
||||
// // configure author name, notification description, timestamp
|
||||
// let nameText = notification.account.displayNameWithFallback
|
||||
// let titleLabelText: String = {
|
||||
// switch notification.notificationType {
|
||||
// case .favourite: return L10n.Scene.Notification.userFavoritedYourPost(nameText)
|
||||
// case .follow: return L10n.Scene.Notification.userFollowedYou(nameText)
|
||||
// case .followRequest: return L10n.Scene.Notification.userRequestedToFollowYou(nameText)
|
||||
// case .mention: return L10n.Scene.Notification.userMentionedYou(nameText)
|
||||
// case .poll: return L10n.Scene.Notification.userYourPollHasEnded(nameText)
|
||||
// case .reblog: return L10n.Scene.Notification.userRebloggedYourPost(nameText)
|
||||
// default: return ""
|
||||
// }
|
||||
// }()
|
||||
//
|
||||
// do {
|
||||
// let nameContent = MastodonContent(content: nameText, emojis: notification.account.emojiMeta)
|
||||
// let nameMetaContent = try MastodonMetaContent.convert(document: nameContent)
|
||||
//
|
||||
// let mastodonContent = MastodonContent(content: titleLabelText, emojis: notification.account.emojiMeta)
|
||||
// let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
//
|
||||
// cell.titleLabel.configure(content: metaContent)
|
||||
//
|
||||
// if let nameRange = metaContent.string.range(of: nameMetaContent.string) {
|
||||
// let nsRange = NSRange(nameRange, in: metaContent.string)
|
||||
// cell.titleLabel.textStorage.addAttributes([
|
||||
// .font: UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold), maximumPointSize: 20),
|
||||
// .foregroundColor: Asset.Colors.brandBlue.color,
|
||||
// ], range: nsRange)
|
||||
// }
|
||||
//
|
||||
// } catch {
|
||||
// let metaContent = PlaintextMetaContent(string: titleLabelText)
|
||||
// cell.titleLabel.configure(content: metaContent)
|
||||
// }
|
||||
//
|
||||
// let createAt = notification.createAt
|
||||
// cell.timestampLabel.text = createAt.localizedSlowedTimeAgoSinceNow
|
||||
// AppContext.shared.timestampUpdatePublisher
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .sink { [weak cell] _ in
|
||||
// guard let cell = cell else { return }
|
||||
// cell.timestampLabel.text = createAt.localizedSlowedTimeAgoSinceNow
|
||||
// }
|
||||
// .store(in: &cell.disposeBag)
|
||||
//
|
||||
// // configure follow request (if exist)
|
||||
// if case .followRequest = notification.notificationType {
|
||||
// cell.acceptButton.publisher(for: .touchUpInside)
|
||||
// .sink { [weak cell] _ in
|
||||
// guard let cell = cell else { return }
|
||||
// cell.delegate?.notificationTableViewCell(cell, notification: notification, acceptButtonDidPressed: cell.acceptButton)
|
||||
// }
|
||||
// .store(in: &cell.disposeBag)
|
||||
// cell.rejectButton.publisher(for: .touchUpInside)
|
||||
// .sink { [weak cell] _ in
|
||||
// guard let cell = cell else { return }
|
||||
// cell.delegate?.notificationTableViewCell(cell, notification: notification, rejectButtonDidPressed: cell.rejectButton)
|
||||
// }
|
||||
// .store(in: &cell.disposeBag)
|
||||
// cell.buttonStackView.isHidden = false
|
||||
// } else {
|
||||
// cell.buttonStackView.isHidden = true
|
||||
// }
|
||||
//
|
||||
// // configure status (if exist)
|
||||
// if let status = notification.status {
|
||||
// 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,
|
||||
// tableView: tableView,
|
||||
// timelineContext: .notifications,
|
||||
// dependency: dependency,
|
||||
// readableLayoutFrame: frame,
|
||||
// status: status,
|
||||
// requestUserID: notification.userID,
|
||||
// statusItemAttribute: attribute
|
||||
// )
|
||||
// cell.statusContainerView.isHidden = false
|
||||
// cell.containerStackView.alignment = .top
|
||||
// cell.containerStackViewBottomLayoutConstraint.constant = 0
|
||||
// } else {
|
||||
// if case .followRequest = notification.notificationType {
|
||||
// cell.containerStackView.alignment = .top
|
||||
// } else {
|
||||
// cell.containerStackView.alignment = .center
|
||||
// }
|
||||
// cell.statusContainerView.isHidden = true
|
||||
// cell.containerStackViewBottomLayoutConstraint.constant = 5 // 5pt margin when no status view
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// static func configureStatusAccessibilityLabel(cell: NotificationStatusTableViewCell) {
|
||||
// // FIXME:
|
||||
// cell.accessibilityLabel = {
|
||||
// var accessibilityViews: [UIView?] = []
|
||||
// accessibilityViews.append(contentsOf: [
|
||||
// cell.titleLabel,
|
||||
// cell.timestampLabel,
|
||||
// cell.statusView
|
||||
// ])
|
||||
// if !cell.statusContainerView.isHidden {
|
||||
// if !cell.statusView.headerContainerView.isHidden {
|
||||
// accessibilityViews.append(cell.statusView.headerInfoLabel)
|
||||
// }
|
||||
// accessibilityViews.append(contentsOf: [
|
||||
// cell.statusView.nameMetaLabel,
|
||||
// cell.statusView.dateLabel,
|
||||
// cell.statusView.contentMetaText.textView,
|
||||
// ])
|
||||
// }
|
||||
// return accessibilityViews
|
||||
// .compactMap { $0?.accessibilityLabel }
|
||||
// .joined(separator: " ")
|
||||
// }()
|
||||
// }
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
/// Note: update Equatable when change case
|
||||
enum CategoryPickerItem {
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
enum CategoryPickerSection: Equatable, Hashable {
|
||||
case main
|
||||
|
|
|
@ -21,7 +21,11 @@ extension PickServerSection {
|
|||
dependency: NeedsDependency,
|
||||
pickServerCellDelegate: PickServerCellDelegate
|
||||
) -> UITableViewDiffableDataSource<PickServerSection, PickServerItem> {
|
||||
UITableViewDiffableDataSource(tableView: tableView) { [
|
||||
tableView.register(OnboardingHeadlineTableViewCell.self, forCellReuseIdentifier: String(describing: OnboardingHeadlineTableViewCell.self))
|
||||
tableView.register(PickServerCell.self, forCellReuseIdentifier: String(describing: PickServerCell.self))
|
||||
tableView.register(PickServerLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: PickServerLoaderTableViewCell.self))
|
||||
|
||||
return UITableViewDiffableDataSource(tableView: tableView) { [
|
||||
weak dependency,
|
||||
weak pickServerCellDelegate
|
||||
] tableView, indexPath, item -> UITableViewCell? in
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
enum ServerRuleSection: Hashable {
|
||||
case header
|
||||
|
|
|
@ -1,68 +0,0 @@
|
|||
//
|
||||
// PollItem.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-2.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
/// Note: update Equatable when change case
|
||||
enum PollItem {
|
||||
case option(objectID: NSManagedObjectID, attribute: Attribute)
|
||||
}
|
||||
|
||||
|
||||
extension PollItem {
|
||||
class Attribute: Hashable {
|
||||
|
||||
enum SelectState: Equatable, Hashable {
|
||||
case none
|
||||
case off
|
||||
case on
|
||||
}
|
||||
|
||||
enum VoteState: Equatable, Hashable {
|
||||
case hidden
|
||||
case reveal(voted: Bool, percentage: Double, animated: Bool)
|
||||
}
|
||||
|
||||
var selectState: SelectState
|
||||
var voteState: VoteState
|
||||
|
||||
init(selectState: SelectState, voteState: VoteState) {
|
||||
self.selectState = selectState
|
||||
self.voteState = voteState
|
||||
}
|
||||
|
||||
static func == (lhs: PollItem.Attribute, rhs: PollItem.Attribute) -> Bool {
|
||||
return lhs.selectState == rhs.selectState &&
|
||||
lhs.voteState == rhs.voteState
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(selectState)
|
||||
hasher.combine(voteState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PollItem: Equatable {
|
||||
static func == (lhs: PollItem, rhs: PollItem) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.option(let objectIDLeft, _), .option(let objectIDRight, _)):
|
||||
return objectIDLeft == objectIDRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension PollItem: Hashable {
|
||||
func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
case .option(let objectID, _):
|
||||
hasher.combine(objectID)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,120 +0,0 @@
|
|||
//
|
||||
// PollSection.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-2.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
|
||||
import MastodonSDK
|
||||
|
||||
extension Mastodon.Entity.Attachment: Hashable {
|
||||
public static func == (lhs: Mastodon.Entity.Attachment, rhs: Mastodon.Entity.Attachment) -> Bool {
|
||||
return lhs.id == rhs.id
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
|
||||
enum PollSection: Equatable, Hashable {
|
||||
case main
|
||||
}
|
||||
|
||||
extension PollSection {
|
||||
static func tableViewDiffableDataSource(
|
||||
for tableView: UITableView,
|
||||
managedObjectContext: NSManagedObjectContext
|
||||
) -> UITableViewDiffableDataSource<PollSection, PollItem> {
|
||||
return UITableViewDiffableDataSource<PollSection, PollItem>(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
|
||||
switch item {
|
||||
case .option(let objectID, let attribute):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PollOptionTableViewCell.self), for: indexPath) as! PollOptionTableViewCell
|
||||
managedObjectContext.performAndWait {
|
||||
let option = managedObjectContext.object(with: objectID) as! PollOption
|
||||
PollSection.configure(cell: cell, pollOption: option, pollItemAttribute: attribute)
|
||||
|
||||
cell.isAccessibilityElement = true
|
||||
cell.accessibilityLabel = {
|
||||
var labels: [String] = [option.title]
|
||||
if let percentage = cell.pollOptionView.optionPercentageLabel.text {
|
||||
labels.append(percentage)
|
||||
}
|
||||
return labels.joined(separator: ",")
|
||||
}()
|
||||
}
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PollSection {
|
||||
static func configure(
|
||||
cell: PollOptionTableViewCell,
|
||||
pollOption option: PollOption,
|
||||
pollItemAttribute attribute: PollItem.Attribute
|
||||
) {
|
||||
cell.pollOptionView.optionTextField.text = option.title
|
||||
configure(cell: cell, selectState: attribute.selectState)
|
||||
configure(cell: cell, voteState: attribute.voteState)
|
||||
cell.attribute = attribute
|
||||
cell.layoutIfNeeded()
|
||||
cell.updateTextAppearance()
|
||||
}
|
||||
}
|
||||
|
||||
extension PollSection {
|
||||
|
||||
static func configure(cell: PollOptionTableViewCell, selectState state: PollItem.Attribute.SelectState) {
|
||||
switch state {
|
||||
case .none:
|
||||
cell.pollOptionView.checkmarkBackgroundView.isHidden = true
|
||||
cell.pollOptionView.checkmarkImageView.isHidden = true
|
||||
case .off:
|
||||
ThemeService.shared.currentTheme
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak cell] theme in
|
||||
guard let cell = cell else { return }
|
||||
cell.pollOptionView.checkmarkBackgroundView.backgroundColor = theme.tertiarySystemBackgroundColor
|
||||
cell.pollOptionView.checkmarkBackgroundView.layer.borderColor = theme.tableViewCellSelectionBackgroundColor.withAlphaComponent(0.3).cgColor
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
cell.pollOptionView.checkmarkBackgroundView.layer.borderWidth = 1
|
||||
cell.pollOptionView.checkmarkBackgroundView.isHidden = false
|
||||
cell.pollOptionView.checkmarkImageView.isHidden = true
|
||||
case .on:
|
||||
ThemeService.shared.currentTheme
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak cell] theme in
|
||||
guard let cell = cell else { return }
|
||||
cell.pollOptionView.checkmarkBackgroundView.backgroundColor = theme.tertiarySystemBackgroundColor
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
cell.pollOptionView.checkmarkBackgroundView.layer.borderColor = UIColor.clear.cgColor
|
||||
cell.pollOptionView.checkmarkBackgroundView.layer.borderWidth = 0
|
||||
cell.pollOptionView.checkmarkBackgroundView.isHidden = false
|
||||
cell.pollOptionView.checkmarkImageView.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
static func configure(cell: PollOptionTableViewCell, voteState state: PollItem.Attribute.VoteState) {
|
||||
switch state {
|
||||
case .hidden:
|
||||
cell.pollOptionView.optionPercentageLabel.isHidden = true
|
||||
cell.pollOptionView.voteProgressStripView.isHidden = true
|
||||
cell.pollOptionView.voteProgressStripView.setProgress(0.0, animated: false)
|
||||
case .reveal(let voted, let percentage, let animated):
|
||||
cell.pollOptionView.optionPercentageLabel.isHidden = false
|
||||
cell.pollOptionView.optionPercentageLabel.text = String(Int(100 * percentage)) + "%"
|
||||
cell.pollOptionView.voteProgressStripView.isHidden = false
|
||||
cell.pollOptionView.voteProgressStripView.tintColor = voted ? Asset.Colors.brandBlue.color : Asset.Colors.Poll.disabled.color
|
||||
cell.pollOptionView.voteProgressStripView.setProgress(CGFloat(percentage), animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -10,24 +10,10 @@ import Combine
|
|||
import MastodonSDK
|
||||
import MastodonMeta
|
||||
|
||||
enum ProfileFieldItem {
|
||||
case field(field: FieldValue, attribute: FieldItemAttribute)
|
||||
case addEntry(attribute: AddEntryItemAttribute)
|
||||
}
|
||||
|
||||
protocol ProfileFieldListSeparatorLineConfigurable: AnyObject {
|
||||
var isLast: Bool { get set }
|
||||
}
|
||||
|
||||
extension ProfileFieldItem {
|
||||
var listSeparatorLineConfigurable: ProfileFieldListSeparatorLineConfigurable? {
|
||||
switch self {
|
||||
case .field(_, let attribute):
|
||||
return attribute
|
||||
case .addEntry(let attribute):
|
||||
return attribute
|
||||
}
|
||||
}
|
||||
enum ProfileFieldItem: Hashable {
|
||||
case field(field: FieldValue)
|
||||
case editField(field: FieldValue)
|
||||
case addEntry
|
||||
}
|
||||
|
||||
extension ProfileFieldItem {
|
||||
|
@ -36,17 +22,29 @@ extension ProfileFieldItem {
|
|||
|
||||
var name: CurrentValueSubject<String, Never>
|
||||
var value: CurrentValueSubject<String, Never>
|
||||
|
||||
let emojiMeta: MastodonContent.Emojis
|
||||
|
||||
init(id: UUID = UUID(), name: String, value: String) {
|
||||
init(
|
||||
id: UUID = UUID(),
|
||||
name: String,
|
||||
value: String,
|
||||
emojiMeta: MastodonContent.Emojis
|
||||
) {
|
||||
self.id = id
|
||||
self.name = CurrentValueSubject(name)
|
||||
self.value = CurrentValueSubject(value)
|
||||
self.emojiMeta = emojiMeta
|
||||
}
|
||||
|
||||
static func == (lhs: ProfileFieldItem.FieldValue, rhs: ProfileFieldItem.FieldValue) -> Bool {
|
||||
static func == (
|
||||
lhs: ProfileFieldItem.FieldValue,
|
||||
rhs: ProfileFieldItem.FieldValue
|
||||
) -> Bool {
|
||||
return lhs.id == rhs.id
|
||||
&& lhs.name.value == rhs.name.value
|
||||
&& lhs.value.value == rhs.value.value
|
||||
&& lhs.emojiMeta == rhs.emojiMeta
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
|
@ -54,50 +52,3 @@ extension ProfileFieldItem {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ProfileFieldItem {
|
||||
class FieldItemAttribute: Equatable, ProfileFieldListSeparatorLineConfigurable {
|
||||
let emojiMeta = CurrentValueSubject<MastodonContent.Emojis, Never>([:])
|
||||
|
||||
var isEditing = false
|
||||
var isLast = false
|
||||
|
||||
static func == (lhs: ProfileFieldItem.FieldItemAttribute, rhs: ProfileFieldItem.FieldItemAttribute) -> Bool {
|
||||
return lhs.isEditing == rhs.isEditing
|
||||
&& lhs.isLast == rhs.isLast
|
||||
}
|
||||
}
|
||||
|
||||
class AddEntryItemAttribute: Equatable, ProfileFieldListSeparatorLineConfigurable {
|
||||
var isLast = false
|
||||
|
||||
static func == (lhs: ProfileFieldItem.AddEntryItemAttribute, rhs: ProfileFieldItem.AddEntryItemAttribute) -> Bool {
|
||||
return lhs.isLast == rhs.isLast
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ProfileFieldItem: Equatable {
|
||||
static func == (lhs: ProfileFieldItem, rhs: ProfileFieldItem) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.field(let fieldLeft, let attributeLeft), .field(let fieldRight, let attributeRight)):
|
||||
return fieldLeft.id == fieldRight.id
|
||||
&& attributeLeft == attributeRight
|
||||
case (.addEntry(let attributeLeft), .addEntry(let attributeRight)):
|
||||
return attributeLeft == attributeRight
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ProfileFieldItem: Hashable {
|
||||
func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
case .field(let field, _):
|
||||
hasher.combine(field.id)
|
||||
case .addEntry:
|
||||
hasher.combine(String(describing: ProfileFieldItem.addEntry.self))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,125 +9,124 @@ import os
|
|||
import UIKit
|
||||
import Combine
|
||||
import MastodonMeta
|
||||
import MastodonLocalization
|
||||
|
||||
enum ProfileFieldSection: Equatable, Hashable {
|
||||
case main
|
||||
}
|
||||
|
||||
extension ProfileFieldSection {
|
||||
static func collectionViewDiffableDataSource(
|
||||
for collectionView: UICollectionView,
|
||||
profileFieldCollectionViewCellDelegate: ProfileFieldCollectionViewCellDelegate,
|
||||
profileFieldAddEntryCollectionViewCellDelegate: ProfileFieldAddEntryCollectionViewCellDelegate
|
||||
|
||||
struct Configuration {
|
||||
weak var profileFieldCollectionViewCellDelegate: ProfileFieldCollectionViewCellDelegate?
|
||||
weak var profileFieldEditCollectionViewCellDelegate: ProfileFieldEditCollectionViewCellDelegate?
|
||||
}
|
||||
|
||||
static func diffableDataSource(
|
||||
collectionView: UICollectionView,
|
||||
context: AppContext,
|
||||
configuration: Configuration
|
||||
) -> UICollectionViewDiffableDataSource<ProfileFieldSection, ProfileFieldItem> {
|
||||
let dataSource = UICollectionViewDiffableDataSource<ProfileFieldSection, ProfileFieldItem>(collectionView: collectionView) {
|
||||
[
|
||||
weak profileFieldCollectionViewCellDelegate,
|
||||
weak profileFieldAddEntryCollectionViewCellDelegate
|
||||
] collectionView, indexPath, item in
|
||||
collectionView.register(ProfileFieldCollectionViewHeaderFooterView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: ProfileFieldCollectionViewHeaderFooterView.headerReuseIdentifer)
|
||||
collectionView.register(ProfileFieldCollectionViewHeaderFooterView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: ProfileFieldCollectionViewHeaderFooterView.footerReuseIdentifer)
|
||||
|
||||
let fieldCellRegistration = UICollectionView.CellRegistration<ProfileFieldCollectionViewCell, ProfileFieldItem> { cell, indexPath, item in
|
||||
guard case let .field(field) = item else { return }
|
||||
|
||||
// set key
|
||||
do {
|
||||
let mastodonContent = MastodonContent(content: field.name.value, emojis: field.emojiMeta)
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
cell.keyMetaLabel.configure(content: metaContent)
|
||||
} catch {
|
||||
let content = PlaintextMetaContent(string: field.name.value)
|
||||
cell.keyMetaLabel.configure(content: content)
|
||||
}
|
||||
|
||||
// set value
|
||||
do {
|
||||
let mastodonContent = MastodonContent(content: field.value.value, emojis: field.emojiMeta)
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
cell.valueMetaLabel.configure(content: metaContent)
|
||||
} catch {
|
||||
let content = PlaintextMetaContent(string: field.value.value)
|
||||
cell.valueMetaLabel.configure(content: content)
|
||||
}
|
||||
|
||||
// set background
|
||||
var backgroundConfiguration = UIBackgroundConfiguration.listPlainCell()
|
||||
backgroundConfiguration.backgroundColor = UIColor.secondarySystemBackground
|
||||
cell.backgroundConfiguration = backgroundConfiguration
|
||||
|
||||
cell.delegate = configuration.profileFieldCollectionViewCellDelegate
|
||||
}
|
||||
|
||||
let editFieldCellRegistration = UICollectionView.CellRegistration<ProfileFieldEditCollectionViewCell, ProfileFieldItem> { cell, indexPath, item in
|
||||
guard case let .editField(field) = item else { return }
|
||||
|
||||
cell.keyTextField.text = field.name.value
|
||||
cell.valueTextField.text = field.value.value
|
||||
|
||||
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: cell.keyTextField)
|
||||
.compactMap { $0.object as? UITextField }
|
||||
.map { $0.text ?? "" }
|
||||
.removeDuplicates()
|
||||
.assign(to: \.value, on: field.name)
|
||||
.store(in: &cell.disposeBag)
|
||||
|
||||
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: cell.valueTextField)
|
||||
.compactMap { $0.object as? UITextField }
|
||||
.map { $0.text ?? "" }
|
||||
.removeDuplicates()
|
||||
.assign(to: \.value, on: field.value)
|
||||
.store(in: &cell.disposeBag)
|
||||
|
||||
// set background
|
||||
var backgroundConfiguration = UIBackgroundConfiguration.listPlainCell()
|
||||
backgroundConfiguration.backgroundColor = UIColor.secondarySystemBackground
|
||||
cell.backgroundConfiguration = backgroundConfiguration
|
||||
|
||||
cell.delegate = configuration.profileFieldEditCollectionViewCellDelegate
|
||||
}
|
||||
|
||||
let addEntryCellRegistration = UICollectionView.CellRegistration<ProfileFieldAddEntryCollectionViewCell, ProfileFieldItem> { cell, indexPath, item in
|
||||
guard case .addEntry = item else { return }
|
||||
|
||||
var backgroundConfiguration = UIBackgroundConfiguration.listPlainCell()
|
||||
backgroundConfiguration.backgroundColorTransformer = .init { [weak cell] _ in
|
||||
guard let cell = cell else {
|
||||
return .secondarySystemBackground
|
||||
}
|
||||
let state = cell.configurationState
|
||||
if state.isHighlighted || state.isSelected {
|
||||
return .secondarySystemBackground.withAlphaComponent(0.5)
|
||||
} else {
|
||||
return .secondarySystemBackground
|
||||
}
|
||||
}
|
||||
cell.backgroundConfiguration = backgroundConfiguration
|
||||
}
|
||||
|
||||
let dataSource = UICollectionViewDiffableDataSource<ProfileFieldSection, ProfileFieldItem>(collectionView: collectionView) { collectionView, indexPath, item in
|
||||
switch item {
|
||||
case .field(let field, let attribute):
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ProfileFieldCollectionViewCell.self), for: indexPath) as! ProfileFieldCollectionViewCell
|
||||
|
||||
// set key
|
||||
do {
|
||||
let mastodonContent = MastodonContent(content: field.name.value, emojis: attribute.emojiMeta.value)
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
cell.fieldView.titleMetaLabel.configure(content: metaContent)
|
||||
} catch {
|
||||
let content = PlaintextMetaContent(string: field.name.value)
|
||||
cell.fieldView.titleMetaLabel.configure(content: content)
|
||||
}
|
||||
cell.fieldView.titleTextField.text = field.name.value
|
||||
Publishers.CombineLatest(
|
||||
field.name.removeDuplicates(),
|
||||
attribute.emojiMeta.removeDuplicates()
|
||||
case .field:
|
||||
return collectionView.dequeueConfiguredReusableCell(
|
||||
using: fieldCellRegistration,
|
||||
for: indexPath,
|
||||
item: item
|
||||
)
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak cell] name, emojiMeta in
|
||||
guard let cell = cell else { return }
|
||||
do {
|
||||
let mastodonContent = MastodonContent(content: name, emojis: emojiMeta)
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
cell.fieldView.titleMetaLabel.configure(content: metaContent)
|
||||
} catch {
|
||||
let content = PlaintextMetaContent(string: name)
|
||||
cell.fieldView.titleMetaLabel.configure(content: content)
|
||||
}
|
||||
// only bind label. The text field should only set once
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
|
||||
|
||||
// set value
|
||||
do {
|
||||
let mastodonContent = MastodonContent(content: field.value.value, emojis: attribute.emojiMeta.value)
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
cell.fieldView.valueMetaLabel.configure(content: metaContent)
|
||||
} catch {
|
||||
let content = PlaintextMetaContent(string: field.value.value)
|
||||
cell.fieldView.valueMetaLabel.configure(content: content)
|
||||
}
|
||||
cell.fieldView.valueTextField.text = field.value.value
|
||||
Publishers.CombineLatest(
|
||||
field.value.removeDuplicates(),
|
||||
attribute.emojiMeta.removeDuplicates()
|
||||
case .editField:
|
||||
return collectionView.dequeueConfiguredReusableCell(
|
||||
using: editFieldCellRegistration,
|
||||
for: indexPath,
|
||||
item: item
|
||||
)
|
||||
case .addEntry:
|
||||
return collectionView.dequeueConfiguredReusableCell(
|
||||
using: addEntryCellRegistration,
|
||||
for: indexPath,
|
||||
item: item
|
||||
)
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak cell] value, emojiMeta in
|
||||
guard let cell = cell else { return }
|
||||
do {
|
||||
let mastodonContent = MastodonContent(content: value, emojis: emojiMeta)
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
cell.fieldView.valueMetaLabel.configure(content: metaContent)
|
||||
} catch {
|
||||
let content = PlaintextMetaContent(string: value)
|
||||
cell.fieldView.valueMetaLabel.configure(content: content)
|
||||
}
|
||||
// only bind label. The text field should only set once
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
|
||||
// bind editing
|
||||
if attribute.isEditing {
|
||||
cell.fieldView.name
|
||||
.removeDuplicates()
|
||||
.receive(on: RunLoop.main)
|
||||
.assign(to: \.value, on: field.name)
|
||||
.store(in: &cell.disposeBag)
|
||||
cell.fieldView.value
|
||||
.removeDuplicates()
|
||||
.receive(on: RunLoop.main)
|
||||
.assign(to: \.value, on: field.value)
|
||||
.store(in: &cell.disposeBag)
|
||||
}
|
||||
|
||||
// setup editing state
|
||||
cell.fieldView.titleTextField.isHidden = !attribute.isEditing
|
||||
cell.fieldView.valueTextField.isHidden = !attribute.isEditing
|
||||
cell.fieldView.titleMetaLabel.isHidden = attribute.isEditing
|
||||
cell.fieldView.valueMetaLabel.isHidden = attribute.isEditing
|
||||
|
||||
// set control hidden
|
||||
let isHidden = !attribute.isEditing
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: update editing state: %s", ((#file as NSString).lastPathComponent), #line, #function, isHidden ? "true" : "false")
|
||||
cell.editButton.isHidden = isHidden
|
||||
cell.reorderBarImageView.isHidden = isHidden
|
||||
|
||||
// update separator line
|
||||
cell.bottomSeparatorLine.isHidden = attribute.isLast
|
||||
|
||||
cell.delegate = profileFieldCollectionViewCellDelegate
|
||||
|
||||
return cell
|
||||
|
||||
case .addEntry(let attribute):
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ProfileFieldAddEntryCollectionViewCell.self), for: indexPath) as! ProfileFieldAddEntryCollectionViewCell
|
||||
|
||||
cell.bottomSeparatorLine.isHidden = attribute.isLast
|
||||
cell.delegate = profileFieldAddEntryCollectionViewCellDelegate
|
||||
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -135,6 +134,7 @@ extension ProfileFieldSection {
|
|||
switch kind {
|
||||
case UICollectionView.elementKindSectionHeader:
|
||||
let reusableView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ProfileFieldCollectionViewHeaderFooterView.headerReuseIdentifer, for: indexPath) as! ProfileFieldCollectionViewHeaderFooterView
|
||||
reusableView.frame.size.height = 20
|
||||
return reusableView
|
||||
case UICollectionView.elementKindSectionFooter:
|
||||
let reusableView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ProfileFieldCollectionViewHeaderFooterView.footerReuseIdentifer, for: indexPath) as! ProfileFieldCollectionViewHeaderFooterView
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
//
|
||||
// RecommendAccountItem.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-2-10.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreDataStack
|
||||
|
||||
enum RecommendAccountItem: Hashable {
|
||||
case account(ManagedObjectRecord<MastodonUser>)
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
//
|
||||
// RecommendAccountSection.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by sxiaojian on 2021/4/1.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
import UIKit
|
||||
import MetaTextKit
|
||||
import MastodonMeta
|
||||
import Combine
|
||||
|
||||
enum RecommendAccountSection: Equatable, Hashable {
|
||||
case main
|
||||
}
|
||||
|
||||
//extension RecommendAccountSection {
|
||||
// static func collectionViewDiffableDataSource(
|
||||
// for collectionView: UICollectionView,
|
||||
// dependency: NeedsDependency,
|
||||
// delegate: SearchRecommendAccountsCollectionViewCellDelegate,
|
||||
// managedObjectContext: NSManagedObjectContext
|
||||
// ) -> UICollectionViewDiffableDataSource<RecommendAccountSection, NSManagedObjectID> {
|
||||
// UICollectionViewDiffableDataSource(collectionView: collectionView) { [weak delegate] collectionView, indexPath, objectID -> UICollectionViewCell? in
|
||||
// let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: SearchRecommendAccountsCollectionViewCell.self), for: indexPath) as! SearchRecommendAccountsCollectionViewCell
|
||||
// managedObjectContext.performAndWait {
|
||||
// let user = managedObjectContext.object(with: objectID) as! MastodonUser
|
||||
// configure(cell: cell, user: user, dependency: dependency)
|
||||
// }
|
||||
// cell.delegate = delegate
|
||||
// return cell
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// static func configure(
|
||||
// cell: SearchRecommendAccountsCollectionViewCell,
|
||||
// user: MastodonUser,
|
||||
// dependency: NeedsDependency
|
||||
// ) {
|
||||
// configureContent(cell: cell, user: user)
|
||||
//
|
||||
// if let currentMastodonUser = dependency.context.authenticationService.activeMastodonAuthentication.value?.user {
|
||||
// configureFollowButton(with: user, currentMastodonUser: currentMastodonUser, followButton: cell.followButton)
|
||||
// }
|
||||
//
|
||||
// Publishers.CombineLatest(
|
||||
// ManagedObjectObserver.observe(object: user).eraseToAnyPublisher().mapError { $0 as Error },
|
||||
// dependency.context.authenticationService.activeMastodonAuthentication.setFailureType(to: Error.self)
|
||||
// )
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .sink { _ in
|
||||
// // do nothing
|
||||
// } receiveValue: { [weak cell] change, authentication in
|
||||
// guard let cell = cell else { return }
|
||||
// guard case .update(let object) = change.changeType,
|
||||
// let user = object as? MastodonUser else { return }
|
||||
// guard let currentMastodonUser = authentication?.user else { return }
|
||||
//
|
||||
// configureFollowButton(with: user, currentMastodonUser: currentMastodonUser, followButton: cell.followButton)
|
||||
// }
|
||||
// .store(in: &cell.disposeBag)
|
||||
//
|
||||
// }
|
||||
//
|
||||
// static func configureContent(
|
||||
// cell: SearchRecommendAccountsCollectionViewCell,
|
||||
// user: MastodonUser
|
||||
// ) {
|
||||
// do {
|
||||
// let mastodonContent = MastodonContent(content: user.displayNameWithFallback, emojis: user.emojis.asDictionary)
|
||||
// let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
// cell.displayNameLabel.configure(content: metaContent)
|
||||
// } catch {
|
||||
// let metaContent = PlaintextMetaContent(string: user.displayNameWithFallback)
|
||||
// cell.displayNameLabel.configure(content: metaContent)
|
||||
// }
|
||||
// cell.acctLabel.text = "@" + user.acct
|
||||
// cell.avatarImageView.af.setImage(
|
||||
// withURL: user.avatarImageURLWithFallback(domain: user.domain),
|
||||
// placeholderImage: UIImage.placeholder(color: .systemFill),
|
||||
// imageTransition: .crossDissolve(0.2)
|
||||
// )
|
||||
// cell.headerImageView.af.setImage(
|
||||
// withURL: URL(string: user.header)!,
|
||||
// placeholderImage: UIImage.placeholder(color: .systemFill),
|
||||
// imageTransition: .crossDissolve(0.2)
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// static func configureFollowButton(
|
||||
// with mastodonUser: MastodonUser,
|
||||
// currentMastodonUser: MastodonUser,
|
||||
// followButton: HighlightDimmableButton
|
||||
// ) {
|
||||
// let relationshipActionSet = relationShipActionSet(mastodonUser: mastodonUser, currentMastodonUser: currentMastodonUser)
|
||||
// followButton.setTitle(relationshipActionSet.title, for: .normal)
|
||||
// }
|
||||
//
|
||||
// static func relationShipActionSet(
|
||||
// mastodonUser: MastodonUser,
|
||||
// currentMastodonUser: MastodonUser
|
||||
// ) -> ProfileViewModel.RelationshipActionOptionSet {
|
||||
// var relationshipActionSet = ProfileViewModel.RelationshipActionOptionSet([.follow])
|
||||
// let isFollowing = mastodonUser.followingBy.flatMap { $0.contains(currentMastodonUser) } ?? false
|
||||
// if isFollowing {
|
||||
// relationshipActionSet.insert(.following)
|
||||
// }
|
||||
//
|
||||
// let isPending = mastodonUser.followRequestedBy.flatMap { $0.contains(currentMastodonUser) } ?? false
|
||||
// if isPending {
|
||||
// relationshipActionSet.insert(.pending)
|
||||
// }
|
||||
//
|
||||
// let isBlocking = mastodonUser.blockingBy.flatMap { $0.contains(currentMastodonUser) } ?? false
|
||||
// if isBlocking {
|
||||
// relationshipActionSet.insert(.blocking)
|
||||
// }
|
||||
//
|
||||
// let isBlockedBy = currentMastodonUser.blockingBy.flatMap { $0.contains(mastodonUser) } ?? false
|
||||
// if isBlockedBy {
|
||||
// relationshipActionSet.insert(.blocked)
|
||||
// }
|
||||
// return relationshipActionSet
|
||||
// }
|
||||
//
|
||||
//}
|
||||
//
|
||||
extension RecommendAccountSection {
|
||||
|
||||
struct Configuration {
|
||||
weak var suggestionAccountTableViewCellDelegate: SuggestionAccountTableViewCellDelegate?
|
||||
}
|
||||
|
||||
static func tableViewDiffableDataSource(
|
||||
tableView: UITableView,
|
||||
context: AppContext,
|
||||
configuration: Configuration
|
||||
) -> UITableViewDiffableDataSource<RecommendAccountSection, RecommendAccountItem> {
|
||||
UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SuggestionAccountTableViewCell.self)) as! SuggestionAccountTableViewCell
|
||||
switch item {
|
||||
case .account(let record):
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let user = record.object(in: context.managedObjectContext) else { return }
|
||||
cell.config(with: user)
|
||||
}
|
||||
}
|
||||
cell.delegate = configuration.suggestionAccountTableViewCellDelegate
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
//
|
||||
// ReportItem.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-1-27.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreDataStack
|
||||
|
||||
enum ReportItem: Hashable {
|
||||
case header(context: HeaderContext)
|
||||
case status(record: ManagedObjectRecord<Status>)
|
||||
case comment(context: CommentContext)
|
||||
case result(record: ManagedObjectRecord<MastodonUser>)
|
||||
case bottomLoader
|
||||
}
|
||||
|
||||
extension ReportItem {
|
||||
struct HeaderContext: Hashable {
|
||||
let primaryLabelText: String
|
||||
let secondaryLabelText: String
|
||||
}
|
||||
|
||||
class CommentContext: Hashable {
|
||||
let id = UUID()
|
||||
@Published var comment: String = ""
|
||||
|
||||
static func == (
|
||||
lhs: ReportItem.CommentContext,
|
||||
rhs: ReportItem.CommentContext
|
||||
) -> Bool {
|
||||
lhs.comment == rhs.comment
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
//
|
||||
// ReportSection.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by ihugo on 2021/4/20.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
import UIKit
|
||||
import os.log
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
enum ReportSection: Equatable, Hashable {
|
||||
case main
|
||||
}
|
||||
|
||||
extension ReportSection {
|
||||
|
||||
struct Configuration {
|
||||
}
|
||||
|
||||
static func diffableDataSource(
|
||||
tableView: UITableView,
|
||||
context: AppContext,
|
||||
configuration: Configuration
|
||||
) -> UITableViewDiffableDataSource<ReportSection, ReportItem> {
|
||||
|
||||
tableView.register(ReportHeadlineTableViewCell.self, forCellReuseIdentifier: String(describing: ReportHeadlineTableViewCell.self))
|
||||
tableView.register(ReportStatusTableViewCell.self, forCellReuseIdentifier: String(describing: ReportStatusTableViewCell.self))
|
||||
tableView.register(ReportCommentTableViewCell.self, forCellReuseIdentifier: String(describing: ReportCommentTableViewCell.self))
|
||||
tableView.register(ReportResultActionTableViewCell.self, forCellReuseIdentifier: String(describing: ReportResultActionTableViewCell.self))
|
||||
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
|
||||
|
||||
return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
|
||||
switch item {
|
||||
case .header(let headerContext):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ReportHeadlineTableViewCell.self), for: indexPath) as! ReportHeadlineTableViewCell
|
||||
cell.primaryLabel.text = headerContext.primaryLabelText
|
||||
cell.secondaryLabel.text = headerContext.secondaryLabelText
|
||||
return cell
|
||||
case .status(let record):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ReportStatusTableViewCell.self), for: indexPath) as! ReportStatusTableViewCell
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let status = record.object(in: context.managedObjectContext) else { return }
|
||||
configure(
|
||||
context: context,
|
||||
tableView: tableView,
|
||||
cell: cell,
|
||||
viewModel: .init(value: status),
|
||||
configuration: configuration
|
||||
)
|
||||
}
|
||||
return cell
|
||||
case .comment(let commentContext):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ReportCommentTableViewCell.self), for: indexPath) as! ReportCommentTableViewCell
|
||||
cell.commentTextView.text = commentContext.comment
|
||||
NotificationCenter.default.publisher(for: UITextView.textDidChangeNotification, object: cell.commentTextView)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak cell] notification in
|
||||
guard let cell = cell else { return }
|
||||
commentContext.comment = cell.commentTextView.text
|
||||
|
||||
// fix shadow get animation issue when cell height changes
|
||||
UIView.performWithoutAnimation {
|
||||
tableView.beginUpdates()
|
||||
tableView.endUpdates()
|
||||
}
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
return cell
|
||||
case .result(let record):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ReportResultActionTableViewCell.self), for: indexPath) as! ReportResultActionTableViewCell
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let user = record.object(in: context.managedObjectContext) else { return }
|
||||
cell.avatarImageView.configure(configuration: .init(url: user.avatarImageURL()))
|
||||
}
|
||||
return cell
|
||||
case .bottomLoader:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
||||
cell.activityIndicatorView.startAnimating()
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ReportSection {
|
||||
|
||||
static func configure(
|
||||
context: AppContext,
|
||||
tableView: UITableView,
|
||||
cell: ReportStatusTableViewCell,
|
||||
viewModel: ReportStatusTableViewCell.ViewModel,
|
||||
configuration: Configuration
|
||||
) {
|
||||
StatusSection.setupStatusPollDataSource(
|
||||
context: context,
|
||||
statusView: cell.statusView
|
||||
)
|
||||
|
||||
context.authenticationService.activeMastodonAuthenticationBox
|
||||
.map { $0 as UserIdentifier? }
|
||||
.assign(to: \.userIdentifier, on: cell.statusView.viewModel)
|
||||
.store(in: &cell.disposeBag)
|
||||
|
||||
cell.configure(
|
||||
tableView: tableView,
|
||||
viewModel: viewModel
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,150 +0,0 @@
|
|||
//
|
||||
// RecommendAccountSection.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by sxiaojian on 2021/4/1.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
import UIKit
|
||||
import MetaTextKit
|
||||
import MastodonMeta
|
||||
import Combine
|
||||
|
||||
enum RecommendAccountSection: Equatable, Hashable {
|
||||
case main
|
||||
}
|
||||
|
||||
extension RecommendAccountSection {
|
||||
static func collectionViewDiffableDataSource(
|
||||
for collectionView: UICollectionView,
|
||||
dependency: NeedsDependency,
|
||||
delegate: SearchRecommendAccountsCollectionViewCellDelegate,
|
||||
managedObjectContext: NSManagedObjectContext
|
||||
) -> UICollectionViewDiffableDataSource<RecommendAccountSection, NSManagedObjectID> {
|
||||
UICollectionViewDiffableDataSource(collectionView: collectionView) { [weak delegate] collectionView, indexPath, objectID -> UICollectionViewCell? in
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: SearchRecommendAccountsCollectionViewCell.self), for: indexPath) as! SearchRecommendAccountsCollectionViewCell
|
||||
managedObjectContext.performAndWait {
|
||||
let user = managedObjectContext.object(with: objectID) as! MastodonUser
|
||||
configure(cell: cell, user: user, dependency: dependency)
|
||||
}
|
||||
cell.delegate = delegate
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
static func configure(
|
||||
cell: SearchRecommendAccountsCollectionViewCell,
|
||||
user: MastodonUser,
|
||||
dependency: NeedsDependency
|
||||
) {
|
||||
configureContent(cell: cell, user: user)
|
||||
|
||||
if let currentMastodonUser = dependency.context.authenticationService.activeMastodonAuthentication.value?.user {
|
||||
configureFollowButton(with: user, currentMastodonUser: currentMastodonUser, followButton: cell.followButton)
|
||||
}
|
||||
|
||||
Publishers.CombineLatest(
|
||||
ManagedObjectObserver.observe(object: user).eraseToAnyPublisher().mapError { $0 as Error },
|
||||
dependency.context.authenticationService.activeMastodonAuthentication.setFailureType(to: Error.self)
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { _ in
|
||||
// do nothing
|
||||
} receiveValue: { [weak cell] change, authentication in
|
||||
guard let cell = cell else { return }
|
||||
guard case .update(let object) = change.changeType,
|
||||
let user = object as? MastodonUser else { return }
|
||||
guard let currentMastodonUser = authentication?.user else { return }
|
||||
|
||||
configureFollowButton(with: user, currentMastodonUser: currentMastodonUser, followButton: cell.followButton)
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
|
||||
}
|
||||
|
||||
static func configureContent(
|
||||
cell: SearchRecommendAccountsCollectionViewCell,
|
||||
user: MastodonUser
|
||||
) {
|
||||
do {
|
||||
let mastodonContent = MastodonContent(content: user.displayNameWithFallback, emojis: user.emojiMeta)
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
cell.displayNameLabel.configure(content: metaContent)
|
||||
} catch {
|
||||
let metaContent = PlaintextMetaContent(string: user.displayNameWithFallback)
|
||||
cell.displayNameLabel.configure(content: metaContent)
|
||||
}
|
||||
cell.acctLabel.text = "@" + user.acct
|
||||
cell.avatarImageView.af.setImage(
|
||||
withURL: user.avatarImageURLWithFallback(domain: user.domain),
|
||||
placeholderImage: UIImage.placeholder(color: .systemFill),
|
||||
imageTransition: .crossDissolve(0.2)
|
||||
)
|
||||
cell.headerImageView.af.setImage(
|
||||
withURL: URL(string: user.header)!,
|
||||
placeholderImage: UIImage.placeholder(color: .systemFill),
|
||||
imageTransition: .crossDissolve(0.2)
|
||||
)
|
||||
}
|
||||
|
||||
static func configureFollowButton(
|
||||
with mastodonUser: MastodonUser,
|
||||
currentMastodonUser: MastodonUser,
|
||||
followButton: HighlightDimmableButton
|
||||
) {
|
||||
let relationshipActionSet = relationShipActionSet(mastodonUser: mastodonUser, currentMastodonUser: currentMastodonUser)
|
||||
followButton.setTitle(relationshipActionSet.title, for: .normal)
|
||||
}
|
||||
|
||||
static func relationShipActionSet(
|
||||
mastodonUser: MastodonUser,
|
||||
currentMastodonUser: MastodonUser
|
||||
) -> ProfileViewModel.RelationshipActionOptionSet {
|
||||
var relationshipActionSet = ProfileViewModel.RelationshipActionOptionSet([.follow])
|
||||
let isFollowing = mastodonUser.followingBy.flatMap { $0.contains(currentMastodonUser) } ?? false
|
||||
if isFollowing {
|
||||
relationshipActionSet.insert(.following)
|
||||
}
|
||||
|
||||
let isPending = mastodonUser.followRequestedBy.flatMap { $0.contains(currentMastodonUser) } ?? false
|
||||
if isPending {
|
||||
relationshipActionSet.insert(.pending)
|
||||
}
|
||||
|
||||
let isBlocking = mastodonUser.blockingBy.flatMap { $0.contains(currentMastodonUser) } ?? false
|
||||
if isBlocking {
|
||||
relationshipActionSet.insert(.blocking)
|
||||
}
|
||||
|
||||
let isBlockedBy = currentMastodonUser.blockingBy.flatMap { $0.contains(mastodonUser) } ?? false
|
||||
if isBlockedBy {
|
||||
relationshipActionSet.insert(.blocked)
|
||||
}
|
||||
return relationshipActionSet
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension RecommendAccountSection {
|
||||
|
||||
static func tableViewDiffableDataSource(
|
||||
for tableView: UITableView,
|
||||
managedObjectContext: NSManagedObjectContext,
|
||||
viewModel: SuggestionAccountViewModel,
|
||||
delegate: SuggestionAccountTableViewCellDelegate
|
||||
) -> UITableViewDiffableDataSource<RecommendAccountSection, NSManagedObjectID> {
|
||||
UITableViewDiffableDataSource(tableView: tableView) { [weak viewModel, weak delegate] (tableView, indexPath, objectID) -> UITableViewCell? in
|
||||
guard let viewModel = viewModel else { return nil }
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SuggestionAccountTableViewCell.self)) as! SuggestionAccountTableViewCell
|
||||
let user = managedObjectContext.object(with: objectID) as! MastodonUser
|
||||
let isSelected = viewModel.selectedAccounts.value.contains(objectID)
|
||||
cell.delegate = delegate
|
||||
cell.config(with: user, isSelected: isSelected)
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
//
|
||||
// RecommendHashTagSection.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by sxiaojian on 2021/4/1.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
import UIKit
|
||||
|
||||
enum RecommendHashTagSection: Equatable, Hashable {
|
||||
case main
|
||||
}
|
||||
|
||||
extension RecommendHashTagSection {
|
||||
static func collectionViewDiffableDataSource(
|
||||
for collectionView: UICollectionView
|
||||
) -> UICollectionViewDiffableDataSource<RecommendHashTagSection, Mastodon.Entity.Tag> {
|
||||
UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, tag -> UICollectionViewCell? in
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: SearchRecommendTagsCollectionViewCell.self), for: indexPath) as! SearchRecommendTagsCollectionViewCell
|
||||
cell.config(with: tag)
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,35 +7,9 @@
|
|||
|
||||
import Foundation
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
|
||||
enum SearchHistoryItem {
|
||||
case account(objectID: NSManagedObjectID)
|
||||
case hashtag(objectID: NSManagedObjectID)
|
||||
case status(objectID: NSManagedObjectID, attribute: Item.StatusAttribute)
|
||||
}
|
||||
|
||||
extension SearchHistoryItem: Hashable {
|
||||
static func == (lhs: SearchHistoryItem, rhs: SearchHistoryItem) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.account(let objectIDLeft), account(let objectIDRight)):
|
||||
return objectIDLeft == objectIDRight
|
||||
case (.hashtag(let objectIDLeft), hashtag(let objectIDRight)):
|
||||
return objectIDLeft == objectIDRight
|
||||
case (.status(let objectIDLeft, _), status(let objectIDRight, _)):
|
||||
return objectIDLeft == objectIDRight
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
case .account(let objectID):
|
||||
hasher.combine(objectID)
|
||||
case .hashtag(let objectID):
|
||||
hasher.combine(objectID)
|
||||
case .status(let objectID, _):
|
||||
hasher.combine(objectID)
|
||||
}
|
||||
}
|
||||
enum SearchHistoryItem: Hashable {
|
||||
case hashtag(ManagedObjectRecord<Tag>)
|
||||
case user(ManagedObjectRecord<MastodonUser>)
|
||||
}
|
||||
|
|
|
@ -13,28 +13,80 @@ enum SearchHistorySection: Hashable {
|
|||
}
|
||||
|
||||
extension SearchHistorySection {
|
||||
static func tableViewDiffableDataSource(
|
||||
for tableView: UITableView,
|
||||
dependency: NeedsDependency
|
||||
) -> UITableViewDiffableDataSource<SearchHistorySection, SearchHistoryItem> {
|
||||
UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
|
||||
|
||||
struct Configuration {
|
||||
weak var searchHistorySectionHeaderCollectionReusableViewDelegate: SearchHistorySectionHeaderCollectionReusableViewDelegate?
|
||||
}
|
||||
|
||||
static func diffableDataSource(
|
||||
collectionView: UICollectionView,
|
||||
context: AppContext,
|
||||
configuration: Configuration
|
||||
) -> UICollectionViewDiffableDataSource<SearchHistorySection, SearchHistoryItem> {
|
||||
|
||||
let userCellRegister = UICollectionView.CellRegistration<SearchHistoryUserCollectionViewCell, ManagedObjectRecord<MastodonUser>> { cell, indexPath, item in
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let user = item.object(in: context.managedObjectContext) else { return }
|
||||
cell.configure(viewModel: .init(value: user))
|
||||
}
|
||||
}
|
||||
|
||||
let hashtagCellRegister = UICollectionView.CellRegistration<UICollectionViewListCell, ManagedObjectRecord<Tag>> { cell, indexPath, item in
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let hashtag = item.object(in: context.managedObjectContext) else { return }
|
||||
var contentConfiguration = cell.defaultContentConfiguration()
|
||||
contentConfiguration.text = "#" + hashtag.name
|
||||
cell.contentConfiguration = contentConfiguration
|
||||
}
|
||||
|
||||
var backgroundConfiguration = UIBackgroundConfiguration.listGroupedCell()
|
||||
backgroundConfiguration.backgroundColorTransformer = .init { [weak cell] _ in
|
||||
guard let state = cell?.configurationState else {
|
||||
return ThemeService.shared.currentTheme.value.secondarySystemGroupedBackgroundColor
|
||||
}
|
||||
|
||||
if state.isHighlighted || state.isSelected {
|
||||
return ThemeService.shared.currentTheme.value.tableViewCellSelectionBackgroundColor
|
||||
}
|
||||
return ThemeService.shared.currentTheme.value.secondarySystemGroupedBackgroundColor
|
||||
}
|
||||
cell.backgroundConfiguration = backgroundConfiguration
|
||||
}
|
||||
|
||||
let dataSource = UICollectionViewDiffableDataSource<SearchHistorySection, SearchHistoryItem>(collectionView: collectionView) { collectionView, indexPath, item in
|
||||
switch item {
|
||||
case .account(let objectID):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchResultTableViewCell.self), for: indexPath) as! SearchResultTableViewCell
|
||||
if let user = try? dependency.context.managedObjectContext.existingObject(with: objectID) as? MastodonUser {
|
||||
cell.config(with: user)
|
||||
}
|
||||
return cell
|
||||
case .hashtag(let objectID):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchResultTableViewCell.self), for: indexPath) as! SearchResultTableViewCell
|
||||
if let hashtag = try? dependency.context.managedObjectContext.existingObject(with: objectID) as? Tag {
|
||||
cell.config(with: hashtag)
|
||||
}
|
||||
return cell
|
||||
case .status:
|
||||
// Should not show status in the history list
|
||||
return UITableViewCell()
|
||||
} // end switch
|
||||
} // end UITableViewDiffableDataSource
|
||||
case .user(let record):
|
||||
return collectionView.dequeueConfiguredReusableCell(
|
||||
using: userCellRegister,
|
||||
for: indexPath, item: record)
|
||||
case .hashtag(let record):
|
||||
return collectionView.dequeueConfiguredReusableCell(
|
||||
using: hashtagCellRegister,
|
||||
for: indexPath, item: record)
|
||||
}
|
||||
}
|
||||
|
||||
let trendHeaderRegister = UICollectionView.SupplementaryRegistration<SearchHistorySectionHeaderCollectionReusableView>(elementKind: UICollectionView.elementKindSectionHeader) { [weak dataSource] supplementaryView, elementKind, indexPath in
|
||||
supplementaryView.delegate = configuration.searchHistorySectionHeaderCollectionReusableViewDelegate
|
||||
|
||||
guard let dataSource = dataSource else { return }
|
||||
let sections = dataSource.snapshot().sectionIdentifiers
|
||||
guard indexPath.section < sections.count else { return }
|
||||
let section = sections[indexPath.section]
|
||||
}
|
||||
|
||||
dataSource.supplementaryViewProvider = { (collectionView: UICollectionView, elementKind: String, indexPath: IndexPath) in
|
||||
let fallback = UICollectionReusableView()
|
||||
|
||||
switch elementKind {
|
||||
case UICollectionView.elementKindSectionHeader:
|
||||
return collectionView.dequeueConfiguredReusableSupplementary(using: trendHeaderRegister, for: indexPath)
|
||||
default:
|
||||
assertionFailure()
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
return dataSource
|
||||
} // end func
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
//
|
||||
// SearchItem.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-1-18.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
|
||||
enum SearchItem: Hashable {
|
||||
case trend(Mastodon.Entity.Tag)
|
||||
}
|
|
@ -5,14 +5,15 @@
|
|||
// Created by sxiaojian on 2021/4/6.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
enum SearchResultItem {
|
||||
enum SearchResultItem: Hashable {
|
||||
case user(ManagedObjectRecord<MastodonUser>)
|
||||
case status(ManagedObjectRecord<Status>)
|
||||
case hashtag(tag: Mastodon.Entity.Tag)
|
||||
case account(account: Mastodon.Entity.Account)
|
||||
case status(statusObjectID: NSManagedObjectID, attribute: Item.StatusAttribute)
|
||||
case bottomLoader(attribute: BottomLoaderAttribute)
|
||||
}
|
||||
|
||||
|
@ -26,7 +27,10 @@ extension SearchResultItem {
|
|||
self.isNoResult = isEmptyResult
|
||||
}
|
||||
|
||||
static func == (lhs: SearchResultItem.BottomLoaderAttribute, rhs: SearchResultItem.BottomLoaderAttribute) -> Bool {
|
||||
static func == (
|
||||
lhs: SearchResultItem.BottomLoaderAttribute,
|
||||
rhs: SearchResultItem.BottomLoaderAttribute
|
||||
) -> Bool {
|
||||
return lhs.id == rhs.id
|
||||
}
|
||||
|
||||
|
@ -35,60 +39,3 @@ extension SearchResultItem {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchResultItem: Equatable {
|
||||
static func == (lhs: SearchResultItem, rhs: SearchResultItem) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.hashtag(let tagLeft), .hashtag(let tagRight)):
|
||||
return tagLeft == tagRight
|
||||
case (.account(let accountLeft), .account(let accountRight)):
|
||||
return accountLeft == accountRight
|
||||
case (.status(let idLeft, _), .status(let idRight, _)):
|
||||
return idLeft == idRight
|
||||
case (.bottomLoader(let attributeLeft), .bottomLoader(let attributeRight)):
|
||||
return attributeLeft == attributeRight
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchResultItem: Hashable {
|
||||
func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
case .account(let account):
|
||||
hasher.combine(String(describing: SearchResultItem.account.self))
|
||||
hasher.combine(account.id)
|
||||
case .hashtag(let tag):
|
||||
hasher.combine(String(describing: SearchResultItem.hashtag.self))
|
||||
hasher.combine(tag.name)
|
||||
case .status(let id, _):
|
||||
hasher.combine(id)
|
||||
case .bottomLoader(let attribute):
|
||||
hasher.combine(attribute)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchResultItem {
|
||||
var sortKey: String? {
|
||||
switch self {
|
||||
case .account(let account): return account.displayName.lowercased()
|
||||
case .hashtag(let hashtag): return hashtag.name.lowercased()
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchResultItem {
|
||||
var statusObjectItem: StatusObjectItem? {
|
||||
switch self {
|
||||
case .status(let objectID, _):
|
||||
return .status(objectID: objectID)
|
||||
case .hashtag,
|
||||
.account,
|
||||
.bottomLoader:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,51 +5,70 @@
|
|||
// Created by sxiaojian on 2021/4/6.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
import UIKit
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
import MastodonUI
|
||||
|
||||
enum SearchResultSection: Equatable, Hashable {
|
||||
enum SearchResultSection: Hashable {
|
||||
case main
|
||||
}
|
||||
|
||||
extension SearchResultSection {
|
||||
|
||||
static let logger = Logger(subsystem: "SearchResultSection", category: "logic")
|
||||
|
||||
struct Configuration {
|
||||
weak var statusViewTableViewCellDelegate: StatusTableViewCellDelegate?
|
||||
weak var userTableViewCellDelegate: UserTableViewCellDelegate?
|
||||
}
|
||||
|
||||
static func tableViewDiffableDataSource(
|
||||
for tableView: UITableView,
|
||||
dependency: NeedsDependency,
|
||||
statusTableViewCellDelegate: StatusTableViewCellDelegate
|
||||
tableView: UITableView,
|
||||
context: AppContext,
|
||||
configuration: Configuration
|
||||
) -> UITableViewDiffableDataSource<SearchResultSection, SearchResultItem> {
|
||||
UITableViewDiffableDataSource(tableView: tableView) { [
|
||||
weak statusTableViewCellDelegate
|
||||
] tableView, indexPath, item -> UITableViewCell? in
|
||||
tableView.register(UserTableViewCell.self, forCellReuseIdentifier: String(describing: UserTableViewCell.self))
|
||||
tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.self))
|
||||
tableView.register(HashtagTableViewCell.self, forCellReuseIdentifier: String(describing: HashtagTableViewCell.self))
|
||||
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
|
||||
|
||||
return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
|
||||
switch item {
|
||||
case .account(let account):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchResultTableViewCell.self), for: indexPath) as! SearchResultTableViewCell
|
||||
cell.config(with: account)
|
||||
return cell
|
||||
case .hashtag(let tag):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchResultTableViewCell.self), for: indexPath) as! SearchResultTableViewCell
|
||||
cell.config(with: tag)
|
||||
return cell
|
||||
case .status(let statusObjectID, let attribute):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
|
||||
if let status = try? dependency.context.managedObjectContext.existingObject(with: statusObjectID) as? Status {
|
||||
let activeMastodonAuthenticationBox = dependency.context.authenticationService.activeMastodonAuthenticationBox.value
|
||||
let requestUserID = activeMastodonAuthenticationBox?.userID ?? ""
|
||||
StatusSection.configure(
|
||||
cell: cell,
|
||||
case .user(let record):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let user = record.object(in: context.managedObjectContext) else { return }
|
||||
configure(
|
||||
context: context,
|
||||
tableView: tableView,
|
||||
timelineContext: .search,
|
||||
dependency: dependency,
|
||||
readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
|
||||
status: status,
|
||||
requestUserID: requestUserID,
|
||||
statusItemAttribute: attribute
|
||||
cell: cell,
|
||||
viewModel: .init(value: .user(user)),
|
||||
configuration: configuration
|
||||
)
|
||||
}
|
||||
cell.delegate = statusTableViewCellDelegate
|
||||
return cell
|
||||
case .status(let record):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let status = record.object(in: context.managedObjectContext) else { return }
|
||||
configure(
|
||||
context: context,
|
||||
tableView: tableView,
|
||||
cell: cell,
|
||||
viewModel: StatusTableViewCell.ViewModel(value: .status(status)),
|
||||
configuration: configuration
|
||||
)
|
||||
}
|
||||
return cell
|
||||
case .hashtag(let tag):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: HashtagTableViewCell.self)) as! HashtagTableViewCell
|
||||
cell.primaryLabel.configure(content: PlaintextMetaContent(string: "#" + tag.name))
|
||||
return cell
|
||||
case .bottomLoader(let attribute):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) as! TimelineBottomLoaderTableViewCell
|
||||
|
@ -63,7 +82,49 @@ extension SearchResultSection {
|
|||
cell.loadMoreLabel.isHidden = true
|
||||
}
|
||||
return cell
|
||||
} // end switch
|
||||
}
|
||||
} // end UITableViewDiffableDataSource
|
||||
} // end func
|
||||
}
|
||||
|
||||
extension SearchResultSection {
|
||||
|
||||
static func configure(
|
||||
context: AppContext,
|
||||
tableView: UITableView,
|
||||
cell: StatusTableViewCell,
|
||||
viewModel: StatusTableViewCell.ViewModel,
|
||||
configuration: Configuration
|
||||
) {
|
||||
StatusSection.setupStatusPollDataSource(
|
||||
context: context,
|
||||
statusView: cell.statusView
|
||||
)
|
||||
|
||||
context.authenticationService.activeMastodonAuthenticationBox
|
||||
.map { $0 as UserIdentifier? }
|
||||
.assign(to: \.userIdentifier, on: cell.statusView.viewModel)
|
||||
.store(in: &cell.disposeBag)
|
||||
|
||||
cell.configure(
|
||||
tableView: tableView,
|
||||
viewModel: viewModel,
|
||||
delegate: configuration.statusViewTableViewCellDelegate
|
||||
)
|
||||
}
|
||||
|
||||
static func configure(
|
||||
context: AppContext,
|
||||
tableView: UITableView,
|
||||
cell: UserTableViewCell,
|
||||
viewModel: UserTableViewCell.ViewModel,
|
||||
configuration: Configuration
|
||||
) {
|
||||
cell.configure(
|
||||
tableView: tableView,
|
||||
viewModel: viewModel,
|
||||
delegate: configuration.userTableViewCellDelegate
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
//
|
||||
// SearchSection.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-1-18.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MastodonSDK
|
||||
import MastodonLocalization
|
||||
|
||||
enum SearchSection: Hashable {
|
||||
case trend
|
||||
}
|
||||
|
||||
extension SearchSection {
|
||||
|
||||
static func diffableDataSource(
|
||||
collectionView: UICollectionView,
|
||||
context: AppContext
|
||||
) -> UICollectionViewDiffableDataSource<SearchSection, SearchItem> {
|
||||
|
||||
let trendCellRegister = UICollectionView.CellRegistration<TrendCollectionViewCell, Mastodon.Entity.Tag> { cell, indexPath, item in
|
||||
cell.primaryLabel.text = "#" + item.name
|
||||
cell.secondaryLabel.text = L10n.Scene.Search.Recommend.HashTag.peopleTalking(item.talkingPeopleCount ?? 0)
|
||||
|
||||
cell.lineChartView.data = (item.history ?? [])
|
||||
.sorted(by: { $0.day < $1.day }) // latest last
|
||||
.map { entry in
|
||||
guard let point = Int(entry.accounts) else {
|
||||
return .zero
|
||||
}
|
||||
return CGFloat(point)
|
||||
}
|
||||
}
|
||||
|
||||
let dataSource = UICollectionViewDiffableDataSource<SearchSection, SearchItem>(
|
||||
collectionView: collectionView
|
||||
) { collectionView, indexPath, item in
|
||||
switch item {
|
||||
case .trend(let hashtag):
|
||||
let cell = collectionView.dequeueConfiguredReusableCell(
|
||||
using: trendCellRegister,
|
||||
for: indexPath,
|
||||
item: hashtag
|
||||
)
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
let trendHeaderRegister = UICollectionView.SupplementaryRegistration<TrendSectionHeaderCollectionReusableView>(elementKind: UICollectionView.elementKindSectionHeader) { supplementaryView, elementKind, indexPath in
|
||||
// do nothing
|
||||
}
|
||||
|
||||
dataSource.supplementaryViewProvider = { [weak dataSource] (collectionView: UICollectionView, elementKind: String, indexPath: IndexPath) in
|
||||
let fallback = UICollectionReusableView()
|
||||
guard let dataSource = dataSource else { return fallback }
|
||||
let sections = dataSource.snapshot().sectionIdentifiers
|
||||
guard indexPath.section < sections.count else { return fallback }
|
||||
let section = sections[indexPath.section]
|
||||
|
||||
switch elementKind {
|
||||
case UICollectionView.elementKindSectionHeader:
|
||||
switch section {
|
||||
case .trend:
|
||||
return collectionView.dequeueConfiguredReusableSupplementary(using: trendHeaderRegister, for: indexPath)
|
||||
}
|
||||
default:
|
||||
assertionFailure()
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
return dataSource
|
||||
} // end func
|
||||
}
|
|
@ -7,11 +7,14 @@
|
|||
|
||||
import UIKit
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
enum SettingsItem {
|
||||
case appearance(settingObjectID: NSManagedObjectID)
|
||||
case notification(settingObjectID: NSManagedObjectID, switchMode: NotificationSwitchMode)
|
||||
case preference(settingObjectID: NSManagedObjectID, preferenceType: PreferenceType)
|
||||
case appearance(record: ManagedObjectRecord<Setting>)
|
||||
case preference(settingRecord: ManagedObjectRecord<Setting>, preferenceType: PreferenceType)
|
||||
case notification(settingRecord: ManagedObjectRecord<Setting>, switchMode: NotificationSwitchMode)
|
||||
case boringZone(item: Link)
|
||||
case spicyZone(item: Link)
|
||||
}
|
||||
|
@ -19,9 +22,10 @@ enum SettingsItem {
|
|||
extension SettingsItem {
|
||||
|
||||
enum AppearanceMode: String {
|
||||
case automatic
|
||||
case system
|
||||
case reallyDark
|
||||
case sortaDark
|
||||
case light
|
||||
case dark
|
||||
}
|
||||
|
||||
enum NotificationSwitchMode: CaseIterable, Hashable {
|
||||
|
@ -41,14 +45,12 @@ extension SettingsItem {
|
|||
}
|
||||
|
||||
enum PreferenceType: CaseIterable {
|
||||
case darkMode
|
||||
case disableAvatarAnimation
|
||||
case disableEmojiAnimation
|
||||
case useDefaultBrowser
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .darkMode: return L10n.Scene.Settings.Section.Preference.trueBlackDarkMode
|
||||
case .disableAvatarAnimation: return L10n.Scene.Settings.Section.Preference.disableAvatarAnimation
|
||||
case .disableEmojiAnimation: return L10n.Scene.Settings.Section.Preference.disableEmojiAnimation
|
||||
case .useDefaultBrowser: return L10n.Scene.Settings.Section.Preference.usingDefaultBrowser
|
||||
|
@ -75,12 +77,12 @@ extension SettingsItem {
|
|||
}
|
||||
}
|
||||
|
||||
var textColor: UIColor {
|
||||
var textColor: UIColor? {
|
||||
switch self {
|
||||
case .accountSettings: return Asset.Colors.brandBlue.color
|
||||
case .github: return Asset.Colors.brandBlue.color
|
||||
case .termsOfService: return Asset.Colors.brandBlue.color
|
||||
case .privacyPolicy: return Asset.Colors.brandBlue.color
|
||||
case .accountSettings: return nil // tintColor
|
||||
case .github: return nil
|
||||
case .termsOfService: return nil
|
||||
case .privacyPolicy: return nil
|
||||
case .clearMediaCache: return .systemRed
|
||||
case .signOut: return .systemRed
|
||||
}
|
||||
|
|
|
@ -8,19 +8,21 @@
|
|||
import UIKit
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
enum SettingsSection: Hashable {
|
||||
case appearance
|
||||
case notifications
|
||||
case preference
|
||||
case notifications
|
||||
case boringZone
|
||||
case spicyZone
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .appearance: return L10n.Scene.Settings.Section.Appearance.title
|
||||
case .appearance: return "Look and Feel" // TODO: i18n
|
||||
case .preference: return ""
|
||||
case .notifications: return L10n.Scene.Settings.Section.Notifications.title
|
||||
case .preference: return L10n.Scene.Settings.Section.Preference.title
|
||||
case .boringZone: return L10n.Scene.Settings.Section.BoringZone.title
|
||||
case .spicyZone: return L10n.Scene.Settings.Section.SpicyZone.title
|
||||
}
|
||||
|
@ -39,25 +41,38 @@ extension SettingsSection {
|
|||
weak settingsToggleCellDelegate
|
||||
] tableView, indexPath, item -> UITableViewCell? in
|
||||
switch item {
|
||||
case .appearance(let objectID):
|
||||
case .appearance(let record):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsAppearanceTableViewCell.self), for: indexPath) as! SettingsAppearanceTableViewCell
|
||||
UserDefaults.shared.observe(\.customUserInterfaceStyle, options: [.initial, .new]) { [weak cell] defaults, _ in
|
||||
guard let cell = cell else { return }
|
||||
switch defaults.customUserInterfaceStyle {
|
||||
case .unspecified: cell.update(with: .automatic)
|
||||
case .dark: cell.update(with: .dark)
|
||||
case .light: cell.update(with: .light)
|
||||
@unknown default:
|
||||
assertionFailure()
|
||||
}
|
||||
managedObjectContext.performAndWait {
|
||||
guard let setting = record.object(in: managedObjectContext) else { return }
|
||||
cell.configure(setting: setting)
|
||||
}
|
||||
.store(in: &cell.observations)
|
||||
cell.delegate = settingsAppearanceTableViewCellDelegate
|
||||
return cell
|
||||
case .notification(let objectID, let switchMode):
|
||||
case .preference(let record, _):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsToggleTableViewCell.self), for: indexPath) as! SettingsToggleTableViewCell
|
||||
cell.delegate = settingsToggleCellDelegate
|
||||
managedObjectContext.performAndWait {
|
||||
guard let setting = record.object(in: managedObjectContext) else { return }
|
||||
SettingsSection.configureSettingToggle(cell: cell, item: item, setting: setting)
|
||||
|
||||
ManagedObjectObserver.observe(object: setting)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveCompletion: { _ in
|
||||
// do nothing
|
||||
}, receiveValue: { [weak cell] change in
|
||||
guard let cell = cell else { return }
|
||||
guard case .update(let object) = change.changeType,
|
||||
let setting = object as? Setting else { return }
|
||||
SettingsSection.configureSettingToggle(cell: cell, item: item, setting: setting)
|
||||
})
|
||||
.store(in: &cell.disposeBag)
|
||||
}
|
||||
return cell
|
||||
case .notification(let record, let switchMode):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsToggleTableViewCell.self), for: indexPath) as! SettingsToggleTableViewCell
|
||||
managedObjectContext.performAndWait {
|
||||
let setting = managedObjectContext.object(with: objectID) as! Setting
|
||||
guard let setting = record.object(in: managedObjectContext) else { return }
|
||||
if let subscription = setting.activeSubscription {
|
||||
SettingsSection.configureSettingToggle(cell: cell, switchMode: switchMode, subscription: subscription)
|
||||
}
|
||||
|
@ -75,32 +90,12 @@ extension SettingsSection {
|
|||
}
|
||||
cell.delegate = settingsToggleCellDelegate
|
||||
return cell
|
||||
case .preference(let objectID, _):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsToggleTableViewCell.self), for: indexPath) as! SettingsToggleTableViewCell
|
||||
cell.delegate = settingsToggleCellDelegate
|
||||
managedObjectContext.performAndWait {
|
||||
let setting = managedObjectContext.object(with: objectID) as! Setting
|
||||
SettingsSection.configureSettingToggle(cell: cell, item: item, setting: setting)
|
||||
|
||||
ManagedObjectObserver.observe(object: setting)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveCompletion: { _ in
|
||||
// do nothing
|
||||
}, receiveValue: { [weak cell] change in
|
||||
guard let cell = cell else { return }
|
||||
guard case .update(let object) = change.changeType,
|
||||
let setting = object as? Setting else { return }
|
||||
SettingsSection.configureSettingToggle(cell: cell, item: item, setting: setting)
|
||||
})
|
||||
.store(in: &cell.disposeBag)
|
||||
}
|
||||
return cell
|
||||
case .boringZone(let item),
|
||||
.spicyZone(let item):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsLinkTableViewCell.self), for: indexPath) as! SettingsLinkTableViewCell
|
||||
cell.update(with: item)
|
||||
return cell
|
||||
}
|
||||
} // end switch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -117,8 +112,6 @@ extension SettingsSection {
|
|||
cell.textLabel?.text = preferenceType.title
|
||||
|
||||
switch preferenceType {
|
||||
case .darkMode:
|
||||
cell.switchButton.isOn = setting.preferredTrueBlackDarkMode
|
||||
case .disableAvatarAnimation:
|
||||
cell.switchButton.isOn = setting.preferredStaticAvatar
|
||||
case .disableEmojiAnimation:
|
||||
|
|
|
@ -1,198 +0,0 @@
|
|||
//
|
||||
// Item.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by sxiaojian on 2021/1/27.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
import DifferenceKit
|
||||
|
||||
/// Note: update Equatable when change case
|
||||
enum Item {
|
||||
// timeline
|
||||
case homeTimelineIndex(objectID: NSManagedObjectID, attribute: StatusAttribute)
|
||||
|
||||
// thread
|
||||
case root(statusObjectID: NSManagedObjectID, attribute: StatusAttribute)
|
||||
case reply(statusObjectID: NSManagedObjectID, attribute: StatusAttribute)
|
||||
case leaf(statusObjectID: NSManagedObjectID, attribute: StatusAttribute)
|
||||
case leafBottomLoader(statusObjectID: NSManagedObjectID)
|
||||
|
||||
// normal list
|
||||
case status(objectID: NSManagedObjectID, attribute: StatusAttribute)
|
||||
|
||||
// loader
|
||||
case homeMiddleLoader(upperTimelineIndexAnchorObjectID: NSManagedObjectID)
|
||||
case publicMiddleLoader(statusID: String)
|
||||
case topLoader
|
||||
case bottomLoader
|
||||
case emptyBottomLoader
|
||||
|
||||
case emptyStateHeader(attribute: EmptyStateHeaderAttribute)
|
||||
|
||||
// reports
|
||||
case reportStatus(objectID: NSManagedObjectID, attribute: ReportStatusAttribute)
|
||||
}
|
||||
|
||||
extension Item {
|
||||
class StatusAttribute {
|
||||
var isSeparatorLineHidden: Bool
|
||||
|
||||
/// is media loaded or not
|
||||
let isImageLoaded = CurrentValueSubject<Bool, Never>(false)
|
||||
|
||||
/// flag for current sensitive content reveal state
|
||||
///
|
||||
/// - true: displaying sensitive content
|
||||
/// - false: displaying content warning overlay
|
||||
let isRevealing = CurrentValueSubject<Bool, Never>(false)
|
||||
|
||||
init(isSeparatorLineHidden: Bool = false) {
|
||||
self.isSeparatorLineHidden = isSeparatorLineHidden
|
||||
}
|
||||
}
|
||||
|
||||
class EmptyStateHeaderAttribute: Hashable {
|
||||
let id = UUID()
|
||||
let reason: Reason
|
||||
|
||||
enum Reason: Equatable {
|
||||
case noStatusFound
|
||||
case blocking(name: String?)
|
||||
case blocked(name: String?)
|
||||
case suspended(name: String?)
|
||||
|
||||
static func == (lhs: Item.EmptyStateHeaderAttribute.Reason, rhs: Item.EmptyStateHeaderAttribute.Reason) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.noStatusFound, noStatusFound): return true
|
||||
case (.blocking(let nameLeft), blocking(let nameRight)): return nameLeft == nameRight
|
||||
case (.blocked(let nameLeft), blocked(let nameRight)): return nameLeft == nameRight
|
||||
case (.suspended(let nameLeft), .suspended(let nameRight)): return nameLeft == nameRight
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(reason: Reason) {
|
||||
self.reason = reason
|
||||
}
|
||||
|
||||
static func == (lhs: Item.EmptyStateHeaderAttribute, rhs: Item.EmptyStateHeaderAttribute) -> Bool {
|
||||
return lhs.reason == rhs.reason
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
|
||||
class ReportStatusAttribute: StatusAttribute {
|
||||
var isSelected: Bool
|
||||
|
||||
init(isSeparatorLineHidden: Bool = false, isSelected: Bool = false) {
|
||||
self.isSelected = isSelected
|
||||
super.init(isSeparatorLineHidden: isSeparatorLineHidden)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Item: Equatable {
|
||||
static func == (lhs: Item, rhs: Item) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.homeTimelineIndex(let objectIDLeft, _), .homeTimelineIndex(let objectIDRight, _)):
|
||||
return objectIDLeft == objectIDRight
|
||||
case (.root(let objectIDLeft, _), .root(let objectIDRight, _)):
|
||||
return objectIDLeft == objectIDRight
|
||||
case (.reply(let objectIDLeft, _), .reply(let objectIDRight, _)):
|
||||
return objectIDLeft == objectIDRight
|
||||
case (.leaf(let objectIDLeft, _), .leaf(let objectIDRight, _)):
|
||||
return objectIDLeft == objectIDRight
|
||||
case (.leafBottomLoader(let objectIDLeft), .leafBottomLoader(let objectIDRight)):
|
||||
return objectIDLeft == objectIDRight
|
||||
case (.status(let objectIDLeft, _), .status(let objectIDRight, _)):
|
||||
return objectIDLeft == objectIDRight
|
||||
case (.homeMiddleLoader(let upperLeft), .homeMiddleLoader(let upperRight)):
|
||||
return upperLeft == upperRight
|
||||
case (.publicMiddleLoader(let upperLeft), .publicMiddleLoader(let upperRight)):
|
||||
return upperLeft == upperRight
|
||||
case (.topLoader, .topLoader):
|
||||
return true
|
||||
case (.bottomLoader, .bottomLoader):
|
||||
return true
|
||||
case (.emptyBottomLoader, .emptyBottomLoader):
|
||||
return true
|
||||
case (.emptyStateHeader(let attributeLeft), .emptyStateHeader(let attributeRight)):
|
||||
return attributeLeft == attributeRight
|
||||
case (.reportStatus(let objectIDLeft, _), .reportStatus(let objectIDRight, _)):
|
||||
return objectIDLeft == objectIDRight
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Item: Hashable {
|
||||
func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
case .homeTimelineIndex(let objectID, _):
|
||||
hasher.combine(objectID)
|
||||
case .root(let objectID, _):
|
||||
hasher.combine(objectID)
|
||||
case .reply(let objectID, _):
|
||||
hasher.combine(objectID)
|
||||
case .leaf(let objectID, _):
|
||||
hasher.combine(objectID)
|
||||
case .leafBottomLoader(let objectID):
|
||||
hasher.combine(objectID)
|
||||
case .status(let objectID, _):
|
||||
hasher.combine(objectID)
|
||||
case .homeMiddleLoader(upperTimelineIndexAnchorObjectID: let upper):
|
||||
hasher.combine(String(describing: Item.homeMiddleLoader.self))
|
||||
hasher.combine(upper)
|
||||
case .publicMiddleLoader(let upper):
|
||||
hasher.combine(String(describing: Item.publicMiddleLoader.self))
|
||||
hasher.combine(upper)
|
||||
case .topLoader:
|
||||
hasher.combine(String(describing: Item.topLoader.self))
|
||||
case .bottomLoader:
|
||||
hasher.combine(String(describing: Item.bottomLoader.self))
|
||||
case .emptyBottomLoader:
|
||||
hasher.combine(String(describing: Item.emptyBottomLoader.self))
|
||||
case .emptyStateHeader(let attribute):
|
||||
hasher.combine(attribute)
|
||||
case .reportStatus(let objectID, _):
|
||||
hasher.combine(objectID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Item: Differentiable { }
|
||||
|
||||
extension Item {
|
||||
var statusObjectItem: StatusObjectItem? {
|
||||
switch self {
|
||||
case .homeTimelineIndex(let objectID, _):
|
||||
return .homeTimelineIndex(objectID: objectID)
|
||||
case .root(let objectID, _),
|
||||
.reply(let objectID, _),
|
||||
.leaf(let objectID, _),
|
||||
.status(let objectID, _),
|
||||
.reportStatus(let objectID, _):
|
||||
return .status(objectID: objectID)
|
||||
case .leafBottomLoader,
|
||||
.homeMiddleLoader,
|
||||
.publicMiddleLoader,
|
||||
.topLoader,
|
||||
.bottomLoader,
|
||||
.emptyBottomLoader,
|
||||
.emptyStateHeader:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
//
|
||||
// ReportSection.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by ihugo on 2021/4/20.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
import UIKit
|
||||
import AVKit
|
||||
import os.log
|
||||
|
||||
enum ReportSection: Equatable, Hashable {
|
||||
case main
|
||||
}
|
||||
|
||||
extension ReportSection {
|
||||
static func tableViewDiffableDataSource(
|
||||
for tableView: UITableView,
|
||||
dependency: ReportViewController,
|
||||
managedObjectContext: NSManagedObjectContext,
|
||||
timestampUpdatePublisher: AnyPublisher<Date, Never>
|
||||
) -> UITableViewDiffableDataSource<ReportSection, Item> {
|
||||
UITableViewDiffableDataSource(tableView: tableView) {[
|
||||
weak dependency
|
||||
] tableView, indexPath, item -> UITableViewCell? in
|
||||
guard let dependency = dependency else { return UITableViewCell() }
|
||||
|
||||
switch item {
|
||||
case .reportStatus(let objectID, let attribute):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ReportedStatusTableViewCell.self), for: indexPath) as! ReportedStatusTableViewCell
|
||||
cell.dependency = dependency
|
||||
let activeMastodonAuthenticationBox = dependency.context.authenticationService.activeMastodonAuthenticationBox.value
|
||||
let requestUserID = activeMastodonAuthenticationBox?.userID ?? ""
|
||||
managedObjectContext.performAndWait { [weak dependency] in
|
||||
guard let dependency = dependency else { return }
|
||||
let status = managedObjectContext.object(with: objectID) as! Status
|
||||
StatusSection.configure(
|
||||
cell: cell,
|
||||
tableView: tableView,
|
||||
timelineContext: .report,
|
||||
dependency: dependency,
|
||||
readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
|
||||
status: status,
|
||||
requestUserID: requestUserID,
|
||||
statusItemAttribute: attribute
|
||||
)
|
||||
}
|
||||
|
||||
// defalut to select the report status
|
||||
if attribute.isSelected {
|
||||
tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
|
||||
} else {
|
||||
tableView.deselectRow(at: indexPath, animated: false)
|
||||
}
|
||||
|
||||
return cell
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
//
|
||||
// StatusItem.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-1-11.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreDataStack
|
||||
import MastodonUI
|
||||
|
||||
enum StatusItem: Hashable {
|
||||
case feed(record: ManagedObjectRecord<Feed>)
|
||||
case feedLoader(record: ManagedObjectRecord<Feed>)
|
||||
case status(record: ManagedObjectRecord<Status>)
|
||||
case thread(Thread)
|
||||
case topLoader
|
||||
case bottomLoader
|
||||
}
|
||||
|
||||
extension StatusItem {
|
||||
enum Thread: Hashable {
|
||||
case root(context: Context)
|
||||
case reply(context: Context)
|
||||
case leaf(context: Context)
|
||||
|
||||
public var record: ManagedObjectRecord<Status> {
|
||||
switch self {
|
||||
case .root(let threadContext),
|
||||
.reply(let threadContext),
|
||||
.leaf(let threadContext):
|
||||
return threadContext.status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension StatusItem.Thread {
|
||||
class Context: Hashable {
|
||||
let status: ManagedObjectRecord<Status>
|
||||
var displayUpperConversationLink: Bool
|
||||
var displayBottomConversationLink: Bool
|
||||
|
||||
init(
|
||||
status: ManagedObjectRecord<Status>,
|
||||
displayUpperConversationLink: Bool = false,
|
||||
displayBottomConversationLink: Bool = false
|
||||
) {
|
||||
self.status = status
|
||||
self.displayUpperConversationLink = displayUpperConversationLink
|
||||
self.displayBottomConversationLink = displayBottomConversationLink
|
||||
}
|
||||
|
||||
static func == (lhs: StatusItem.Thread.Context, rhs: StatusItem.Thread.Context) -> Bool {
|
||||
return lhs.status == rhs.status
|
||||
&& lhs.displayUpperConversationLink == rhs.displayUpperConversationLink
|
||||
&& lhs.displayBottomConversationLink == rhs.displayBottomConversationLink
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(status)
|
||||
hasher.combine(displayUpperConversationLink)
|
||||
hasher.combine(displayBottomConversationLink)
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -7,10 +7,10 @@
|
|||
|
||||
import Foundation
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
|
||||
enum UserItem: Hashable {
|
||||
case follower(objectID: NSManagedObjectID)
|
||||
case following(objectID: NSManagedObjectID)
|
||||
case user(record: ManagedObjectRecord<MastodonUser>)
|
||||
case bottomLoader
|
||||
case bottomHeader(text: String)
|
||||
}
|
||||
|
|
|
@ -19,23 +19,30 @@ enum UserSection: Hashable {
|
|||
extension UserSection {
|
||||
|
||||
static let logger = Logger(subsystem: "StatusSection", category: "logic")
|
||||
|
||||
struct Configuration {
|
||||
weak var userTableViewCellDelegate: UserTableViewCellDelegate?
|
||||
}
|
||||
|
||||
static func tableViewDiffableDataSource(
|
||||
for tableView: UITableView,
|
||||
dependency: NeedsDependency,
|
||||
managedObjectContext: NSManagedObjectContext
|
||||
static func diffableDataSource(
|
||||
tableView: UITableView,
|
||||
context: AppContext,
|
||||
configuration: Configuration
|
||||
) -> UITableViewDiffableDataSource<UserSection, UserItem> {
|
||||
UITableViewDiffableDataSource(tableView: tableView) { [
|
||||
weak dependency
|
||||
] tableView, indexPath, item -> UITableViewCell? in
|
||||
guard let dependency = dependency else { return UITableViewCell() }
|
||||
tableView.register(UserTableViewCell.self, forCellReuseIdentifier: String(describing: UserTableViewCell.self))
|
||||
|
||||
return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
|
||||
switch item {
|
||||
case .follower(let objectID),
|
||||
.following(let objectID):
|
||||
case .user(let record):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell
|
||||
managedObjectContext.performAndWait {
|
||||
let user = managedObjectContext.object(with: objectID) as! MastodonUser
|
||||
configure(cell: cell, user: user)
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let user = record.object(in: context.managedObjectContext) else { return }
|
||||
configure(
|
||||
tableView: tableView,
|
||||
cell: cell,
|
||||
viewModel: .init(value: .user(user)),
|
||||
configuration: configuration
|
||||
)
|
||||
}
|
||||
return cell
|
||||
case .bottomLoader:
|
||||
|
@ -55,10 +62,17 @@ extension UserSection {
|
|||
extension UserSection {
|
||||
|
||||
static func configure(
|
||||
tableView: UITableView,
|
||||
cell: UserTableViewCell,
|
||||
user: MastodonUser
|
||||
viewModel: UserTableViewCell.ViewModel,
|
||||
configuration: Configuration
|
||||
) {
|
||||
cell.configure(user: user)
|
||||
|
||||
cell.configure(
|
||||
tableView: tableView,
|
||||
viewModel: viewModel,
|
||||
delegate: configuration.userTableViewCellDelegate
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
//
|
||||
// Attachment.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-2-23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
extension Attachment {
|
||||
|
||||
var type: Mastodon.Entity.Attachment.AttachmentType {
|
||||
return Mastodon.Entity.Attachment.AttachmentType(rawValue: typeRaw) ?? ._other(typeRaw)
|
||||
}
|
||||
|
||||
var meta: Mastodon.Entity.Attachment.Meta? {
|
||||
let decoder = JSONDecoder()
|
||||
return metaData.flatMap { try? decoder.decode(Mastodon.Entity.Attachment.Meta.self, from: $0) }
|
||||
}
|
||||
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
//
|
||||
// Emojis.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-5-7.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
import MastodonMeta
|
||||
|
||||
protocol EmojiContainer {
|
||||
var emojisData: Data? { get }
|
||||
}
|
||||
|
||||
// FIXME: `Mastodon.Entity.Account` extension
|
||||
|
||||
extension EmojiContainer {
|
||||
|
||||
static func encode(emojis: [Mastodon.Entity.Emoji]) -> Data? {
|
||||
return try? JSONEncoder().encode(emojis)
|
||||
}
|
||||
|
||||
var emojis: [Mastodon.Entity.Emoji]? {
|
||||
let decoder = JSONDecoder()
|
||||
return emojisData.flatMap { try? decoder.decode([Mastodon.Entity.Emoji].self, from: $0) }
|
||||
}
|
||||
|
||||
var emojiMeta: MastodonContent.Emojis {
|
||||
let isAnimated = !UserDefaults.shared.preferredStaticEmoji
|
||||
|
||||
var dict = MastodonContent.Emojis()
|
||||
for emoji in emojis ?? [] {
|
||||
dict[emoji.shortcode] = isAnimated ? emoji.url : emoji.staticURL
|
||||
}
|
||||
return dict
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
//
|
||||
// Fields.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-5-25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
|
||||
protocol FieldContainer {
|
||||
var fieldsData: Data? { get }
|
||||
}
|
||||
|
||||
extension FieldContainer {
|
||||
|
||||
static func encode(fields: [Mastodon.Entity.Field]) -> Data? {
|
||||
return try? JSONEncoder().encode(fields)
|
||||
}
|
||||
|
||||
var fields: [Mastodon.Entity.Field]? {
|
||||
let decoder = JSONDecoder()
|
||||
return fieldsData.flatMap { try? decoder.decode([Mastodon.Entity.Field].self, from: $0) }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
//
|
||||
// MastodonUser+Property.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-7-20.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreDataStack
|
||||
|
||||
extension MastodonUser {
|
||||
|
||||
var displayNameWithFallback: String {
|
||||
return !displayName.isEmpty ? displayName : username
|
||||
}
|
||||
|
||||
var acctWithDomain: String {
|
||||
if !acct.contains("@") {
|
||||
// Safe concat due to username cannot contains "@"
|
||||
return username + "@" + domain
|
||||
} else {
|
||||
return acct
|
||||
}
|
||||
}
|
||||
|
||||
var domainFromAcct: String {
|
||||
if !acct.contains("@") {
|
||||
return domain
|
||||
} else {
|
||||
let domain = acct.split(separator: "@").last
|
||||
return String(domain!)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MastodonUser {
|
||||
|
||||
public func headerImageURL() -> URL? {
|
||||
return URL(string: header)
|
||||
}
|
||||
|
||||
public func headerImageURLWithFallback(domain: String) -> URL {
|
||||
return URL(string: header) ?? URL(string: "https://\(domain)/headers/original/missing.png")!
|
||||
}
|
||||
|
||||
public func avatarImageURL() -> URL? {
|
||||
let string = UserDefaults.shared.preferredStaticAvatar ? avatarStatic ?? avatar : avatar
|
||||
return URL(string: string)
|
||||
}
|
||||
|
||||
public func avatarImageURLWithFallback(domain: String) -> URL {
|
||||
return avatarImageURL() ?? URL(string: "https://\(domain)/avatars/original/missing.png")!
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MastodonUser {
|
||||
|
||||
var profileURL: URL {
|
||||
if let urlString = self.url,
|
||||
let url = URL(string: urlString) {
|
||||
return url
|
||||
} else {
|
||||
return URL(string: "https://\(self.domain)/@\(username)")!
|
||||
}
|
||||
}
|
||||
|
||||
var activityItems: [Any] {
|
||||
var items: [Any] = []
|
||||
items.append(profileURL)
|
||||
return items
|
||||
}
|
||||
}
|
|
@ -9,33 +9,67 @@ import Foundation
|
|||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
extension MastodonUser.Property {
|
||||
init(entity: Mastodon.Entity.Account, domain: String, networkDate: Date) {
|
||||
self.init(
|
||||
id: entity.id,
|
||||
domain: domain,
|
||||
acct: entity.acct,
|
||||
username: entity.username,
|
||||
displayName: entity.displayName,
|
||||
avatar: entity.avatar,
|
||||
avatarStatic: entity.avatarStatic,
|
||||
header: entity.header,
|
||||
headerStatic: entity.headerStatic,
|
||||
note: entity.note,
|
||||
url: entity.url,
|
||||
emojisData: entity.emojis.flatMap { MastodonUser.encode(emojis: $0) },
|
||||
fieldsData: entity.fields.flatMap { MastodonUser.encode(fields: $0) },
|
||||
statusesCount: entity.statusesCount,
|
||||
followingCount: entity.followingCount,
|
||||
followersCount: entity.followersCount,
|
||||
locked: entity.locked,
|
||||
bot: entity.bot,
|
||||
suspended: entity.suspended,
|
||||
createdAt: entity.createdAt,
|
||||
networkDate: networkDate
|
||||
)
|
||||
extension MastodonUser {
|
||||
|
||||
public var displayNameWithFallback: String {
|
||||
return !displayName.isEmpty ? displayName : username
|
||||
}
|
||||
|
||||
public var acctWithDomain: String {
|
||||
if !acct.contains("@") {
|
||||
// Safe concat due to username cannot contains "@"
|
||||
return username + "@" + domain
|
||||
} else {
|
||||
return acct
|
||||
}
|
||||
}
|
||||
|
||||
public var domainFromAcct: String {
|
||||
if !acct.contains("@") {
|
||||
return domain
|
||||
} else {
|
||||
let domain = acct.split(separator: "@").last
|
||||
return String(domain!)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MastodonUser: EmojiContainer { }
|
||||
extension MastodonUser: FieldContainer { }
|
||||
extension MastodonUser {
|
||||
|
||||
public func headerImageURL() -> URL? {
|
||||
return URL(string: header)
|
||||
}
|
||||
|
||||
public func headerImageURLWithFallback(domain: String) -> URL {
|
||||
return URL(string: header) ?? URL(string: "https://\(domain)/headers/original/missing.png")!
|
||||
}
|
||||
|
||||
public func avatarImageURL() -> URL? {
|
||||
let string = UserDefaults.shared.preferredStaticAvatar ? avatarStatic ?? avatar : avatar
|
||||
return URL(string: string)
|
||||
}
|
||||
|
||||
public func avatarImageURLWithFallback(domain: String) -> URL {
|
||||
return avatarImageURL() ?? URL(string: "https://\(domain)/avatars/original/missing.png")!
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MastodonUser {
|
||||
|
||||
public var profileURL: URL {
|
||||
if let urlString = self.url,
|
||||
let url = URL(string: urlString) {
|
||||
return url
|
||||
} else {
|
||||
return URL(string: "https://\(self.domain)/@\(username)")!
|
||||
}
|
||||
}
|
||||
|
||||
public var activityItems: [Any] {
|
||||
var items: [Any] = []
|
||||
items.append(profileURL)
|
||||
return items
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
//
|
||||
// NotificationType.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-7-3.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
extension MastodonNotification {
|
||||
var notificationType: Mastodon.Entity.Notification.NotificationType {
|
||||
return Mastodon.Entity.Notification.NotificationType(rawValue: typeRaw) ?? ._other(typeRaw)
|
||||
}
|
||||
}
|
|
@ -9,67 +9,42 @@ import CoreDataStack
|
|||
import Foundation
|
||||
import MastodonSDK
|
||||
|
||||
extension Status.Property {
|
||||
init(entity: Mastodon.Entity.Status, domain: String, networkDate: Date) {
|
||||
self.init(
|
||||
domain: domain,
|
||||
id: entity.id,
|
||||
uri: entity.uri,
|
||||
createdAt: entity.createdAt,
|
||||
content: entity.content!,
|
||||
visibility: entity.visibility?.rawValue,
|
||||
sensitive: entity.sensitive ?? false,
|
||||
spoilerText: entity.spoilerText,
|
||||
emojisData: entity.emojis.flatMap { Status.encode(emojis: $0) },
|
||||
reblogsCount: NSNumber(value: entity.reblogsCount),
|
||||
favouritesCount: NSNumber(value: entity.favouritesCount),
|
||||
repliesCount: entity.repliesCount.flatMap { NSNumber(value: $0) },
|
||||
url: entity.url ?? entity.uri,
|
||||
inReplyToID: entity.inReplyToID,
|
||||
inReplyToAccountID: entity.inReplyToAccountID,
|
||||
language: entity.language,
|
||||
text: entity.text,
|
||||
networkDate: networkDate
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Status {
|
||||
enum SensitiveType {
|
||||
case none
|
||||
case all
|
||||
case media(isSensitive: Bool)
|
||||
}
|
||||
|
||||
|
||||
var sensitiveType: SensitiveType {
|
||||
let spoilerText = self.spoilerText ?? ""
|
||||
|
||||
|
||||
// cast .all sensitive when has spoiter text
|
||||
if !spoilerText.isEmpty {
|
||||
return .all
|
||||
}
|
||||
|
||||
if let firstAttachment = mediaAttachments?.first {
|
||||
|
||||
if let firstAttachment = attachments.first {
|
||||
// cast .media when has non audio media
|
||||
if firstAttachment.type != .audio {
|
||||
if firstAttachment.kind != .audio {
|
||||
return .media(isSensitive: sensitive)
|
||||
} else {
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// not sensitive
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
extension Status {
|
||||
var authorForUserProvider: MastodonUser {
|
||||
let author = (reblog ?? self).author
|
||||
return author
|
||||
}
|
||||
}
|
||||
|
||||
//extension Status {
|
||||
// var authorForUserProvider: MastodonUser {
|
||||
// let author = (reblog ?? self).author
|
||||
// return author
|
||||
// }
|
||||
//}
|
||||
//
|
||||
extension Status {
|
||||
var statusURL: URL {
|
||||
if let urlString = self.url,
|
||||
|
@ -80,7 +55,7 @@ extension Status {
|
|||
return URL(string: "https://\(self.domain)/web/statuses/\(self.id)")!
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var activityItems: [Any] {
|
||||
var items: [Any] = []
|
||||
items.append(self.statusURL)
|
||||
|
@ -88,11 +63,15 @@ extension Status {
|
|||
}
|
||||
}
|
||||
|
||||
extension Status: EmojiContainer { }
|
||||
|
||||
//extension Status {
|
||||
// var visibilityEnum: Mastodon.Entity.Status.Visibility? {
|
||||
// return visibility.flatMap { Mastodon.Entity.Status.Visibility(rawValue: $0) }
|
||||
// }
|
||||
//}
|
||||
|
||||
extension Status {
|
||||
var visibilityEnum: Mastodon.Entity.Status.Visibility? {
|
||||
return visibility.flatMap { Mastodon.Entity.Status.Visibility(rawValue: $0) }
|
||||
var asRecord: ManagedObjectRecord<Status> {
|
||||
return .init(objectID: self.objectID)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import Combine
|
|||
import Alamofire
|
||||
import AlamofireImage
|
||||
import FLAnimatedImage
|
||||
import UIKit
|
||||
|
||||
private enum FLAnimatedImageViewAssociatedKeys {
|
||||
static var activeAvatarRequestURL = "FLAnimatedImageViewAssociatedKeys.activeAvatarRequestURL"
|
||||
|
@ -36,7 +37,12 @@ extension FLAnimatedImageView {
|
|||
}
|
||||
}
|
||||
|
||||
func setImage(url: URL?, placeholder: UIImage?, scaleToSize: CGSize?) {
|
||||
func setImage(
|
||||
url: URL?,
|
||||
placeholder: UIImage?,
|
||||
scaleToSize: CGSize?,
|
||||
completion: ((UIImage?) -> Void)? = nil
|
||||
) {
|
||||
// cancel task
|
||||
activeAvatarRequestURL = nil
|
||||
avatarRequestCancellable?.cancel()
|
||||
|
@ -64,17 +70,17 @@ extension FLAnimatedImageView {
|
|||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
if self.activeAvatarRequestURL == url {
|
||||
if let animatedImage = animatedImage {
|
||||
self.animatedImage = animatedImage
|
||||
} else {
|
||||
self.image = image
|
||||
}
|
||||
guard self.activeAvatarRequestURL == url else { return }
|
||||
if let animatedImage = animatedImage {
|
||||
self.animatedImage = animatedImage
|
||||
} else {
|
||||
self.image = image
|
||||
}
|
||||
completion?(image)
|
||||
}
|
||||
}
|
||||
case .failure:
|
||||
break
|
||||
completion?(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
extension Mastodon.API.Subscriptions.Policy {
|
||||
var title: String {
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
extension Mastodon.Entity.Error.Detail: LocalizedError {
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
import Foundation
|
||||
import MastodonSDK
|
||||
import UIKit
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
extension Mastodon.Entity.Notification.NotificationType {
|
||||
public var color: UIColor {
|
||||
|
|
|
@ -16,3 +16,15 @@ extension Mastodon.Entity.Tag: Hashable {
|
|||
return lhs.name == rhs.name
|
||||
}
|
||||
}
|
||||
|
||||
extension Mastodon.Entity.Tag {
|
||||
|
||||
/// the sum of recent 2 days
|
||||
public var talkingPeopleCount: Int? {
|
||||
return history?
|
||||
.prefix(2)
|
||||
.compactMap { Int($0.accounts) }
|
||||
.reduce(0, +)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
extension UITableView {
|
||||
|
||||
|
|
|
@ -68,8 +68,3 @@ extension UIView {
|
|||
}
|
||||
}
|
||||
|
||||
extension UIView {
|
||||
static var isZoomedMode: Bool {
|
||||
return UIScreen.main.scale != UIScreen.main.nativeScale
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,272 +0,0 @@
|
|||
// swiftlint:disable all
|
||||
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
|
||||
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
#elseif os(iOS)
|
||||
import UIKit
|
||||
#elseif os(tvOS) || os(watchOS)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
// Deprecated typealiases
|
||||
@available(*, deprecated, renamed: "ColorAsset.Color", message: "This typealias will be removed in SwiftGen 7.0")
|
||||
internal typealias AssetColorTypeAlias = ColorAsset.Color
|
||||
@available(*, deprecated, renamed: "ImageAsset.Image", message: "This typealias will be removed in SwiftGen 7.0")
|
||||
internal typealias AssetImageTypeAlias = ImageAsset.Image
|
||||
|
||||
// swiftlint:disable superfluous_disable_command file_length implicit_return
|
||||
|
||||
// MARK: - Asset Catalogs
|
||||
|
||||
// swiftlint:disable identifier_name line_length nesting type_body_length type_name
|
||||
internal enum Asset {
|
||||
internal static let accentColor = ColorAsset(name: "AccentColor")
|
||||
internal enum Asset {
|
||||
internal static let email = ImageAsset(name: "Asset/email")
|
||||
internal static let friends = ImageAsset(name: "Asset/friends")
|
||||
internal static let mastodonTextLogo = ImageAsset(name: "Asset/mastodon.text.logo")
|
||||
}
|
||||
internal enum Circles {
|
||||
internal static let plusCircleFill = ImageAsset(name: "Circles/plus.circle.fill")
|
||||
internal static let plusCircle = ImageAsset(name: "Circles/plus.circle")
|
||||
}
|
||||
internal enum Colors {
|
||||
internal enum Border {
|
||||
internal static let composePoll = ColorAsset(name: "Colors/Border/compose.poll")
|
||||
internal static let searchCard = ColorAsset(name: "Colors/Border/searchCard")
|
||||
internal static let status = ColorAsset(name: "Colors/Border/status")
|
||||
}
|
||||
internal enum Button {
|
||||
internal static let actionToolbar = ColorAsset(name: "Colors/Button/action.toolbar")
|
||||
internal static let disabled = ColorAsset(name: "Colors/Button/disabled")
|
||||
internal static let inactive = ColorAsset(name: "Colors/Button/inactive")
|
||||
}
|
||||
internal enum Icon {
|
||||
internal static let plus = ColorAsset(name: "Colors/Icon/plus")
|
||||
}
|
||||
internal enum Label {
|
||||
internal static let primary = ColorAsset(name: "Colors/Label/primary")
|
||||
internal static let primaryReverse = ColorAsset(name: "Colors/Label/primary.reverse")
|
||||
internal static let secondary = ColorAsset(name: "Colors/Label/secondary")
|
||||
internal static let tertiary = ColorAsset(name: "Colors/Label/tertiary")
|
||||
}
|
||||
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 Poll {
|
||||
internal static let disabled = ColorAsset(name: "Colors/Poll/disabled")
|
||||
}
|
||||
internal enum Shadow {
|
||||
internal static let searchCard = ColorAsset(name: "Colors/Shadow/SearchCard")
|
||||
}
|
||||
internal enum Slider {
|
||||
internal static let track = ColorAsset(name: "Colors/Slider/track")
|
||||
}
|
||||
internal enum TextField {
|
||||
internal static let background = ColorAsset(name: "Colors/TextField/background")
|
||||
internal static let invalid = ColorAsset(name: "Colors/TextField/invalid")
|
||||
internal static let valid = ColorAsset(name: "Colors/TextField/valid")
|
||||
}
|
||||
internal static let alertYellow = ColorAsset(name: "Colors/alert.yellow")
|
||||
internal static let badgeBackground = ColorAsset(name: "Colors/badge.background")
|
||||
internal static let battleshipGrey = ColorAsset(name: "Colors/battleshipGrey")
|
||||
internal static let brandBlue = ColorAsset(name: "Colors/brand.blue")
|
||||
internal static let brandBlueDarken20 = ColorAsset(name: "Colors/brand.blue.darken.20")
|
||||
internal static let dangerBorder = ColorAsset(name: "Colors/danger.border")
|
||||
internal static let danger = ColorAsset(name: "Colors/danger")
|
||||
internal static let disabled = ColorAsset(name: "Colors/disabled")
|
||||
internal static let inactive = ColorAsset(name: "Colors/inactive")
|
||||
internal static let mediaTypeIndicotor = ColorAsset(name: "Colors/media.type.indicotor")
|
||||
internal static let successGreen = ColorAsset(name: "Colors/success.green")
|
||||
internal static let systemOrange = ColorAsset(name: "Colors/system.orange")
|
||||
}
|
||||
internal enum Connectivity {
|
||||
internal static let photoFillSplit = ImageAsset(name: "Connectivity/photo.fill.split")
|
||||
}
|
||||
internal enum Human {
|
||||
internal static let faceSmilingAdaptive = ImageAsset(name: "Human/face.smiling.adaptive")
|
||||
}
|
||||
internal enum Scene {
|
||||
internal enum Onboarding {
|
||||
internal static let avatarPlaceholder = ImageAsset(name: "Scene/Onboarding/avatar.placeholder")
|
||||
internal static let navigationBackButtonBackground = ColorAsset(name: "Scene/Onboarding/navigation.back.button.background")
|
||||
internal static let navigationBackButtonBackgroundHighlighted = ColorAsset(name: "Scene/Onboarding/navigation.back.button.background.highlighted")
|
||||
internal static let navigationNextButtonBackground = ColorAsset(name: "Scene/Onboarding/navigation.next.button.background")
|
||||
internal static let navigationNextButtonBackgroundHighlighted = ColorAsset(name: "Scene/Onboarding/navigation.next.button.background.highlighted")
|
||||
internal static let onboardingBackground = ColorAsset(name: "Scene/Onboarding/onboarding.background")
|
||||
internal static let searchBarBackground = ColorAsset(name: "Scene/Onboarding/search.bar.background")
|
||||
internal static let textFieldBackground = ColorAsset(name: "Scene/Onboarding/textField.background")
|
||||
}
|
||||
internal enum Profile {
|
||||
internal enum Banner {
|
||||
internal static let bioEditBackgroundGray = ColorAsset(name: "Scene/Profile/Banner/bio.edit.background.gray")
|
||||
internal static let nameEditBackgroundGray = ColorAsset(name: "Scene/Profile/Banner/name.edit.background.gray")
|
||||
internal static let usernameGray = ColorAsset(name: "Scene/Profile/Banner/username.gray")
|
||||
}
|
||||
}
|
||||
internal enum Sidebar {
|
||||
internal static let logo = ImageAsset(name: "Scene/Sidebar/logo")
|
||||
}
|
||||
internal enum Welcome {
|
||||
internal enum Illustration {
|
||||
internal static let backgroundCyan = ColorAsset(name: "Scene/Welcome/illustration/background.cyan")
|
||||
internal static let cloudBaseExtend = ImageAsset(name: "Scene/Welcome/illustration/cloud.base.extend")
|
||||
internal static let cloudBase = ImageAsset(name: "Scene/Welcome/illustration/cloud.base")
|
||||
internal static let elephantOnAirplaneWithContrail = ImageAsset(name: "Scene/Welcome/illustration/elephant.on.airplane.with.contrail")
|
||||
internal static let elephantThreeOnGrassExtend = ImageAsset(name: "Scene/Welcome/illustration/elephant.three.on.grass.extend")
|
||||
internal static let elephantThreeOnGrass = ImageAsset(name: "Scene/Welcome/illustration/elephant.three.on.grass")
|
||||
internal static let elephantThreeOnGrassWithTreeThree = ImageAsset(name: "Scene/Welcome/illustration/elephant.three.on.grass.with.tree.three")
|
||||
internal static let elephantThreeOnGrassWithTreeTwo = ImageAsset(name: "Scene/Welcome/illustration/elephant.three.on.grass.with.tree.two")
|
||||
}
|
||||
internal static let mastodonLogoBlack = ImageAsset(name: "Scene/Welcome/mastodon.logo.black")
|
||||
internal static let mastodonLogoBlackLarge = ImageAsset(name: "Scene/Welcome/mastodon.logo.black.large")
|
||||
internal static let mastodonLogo = ImageAsset(name: "Scene/Welcome/mastodon.logo")
|
||||
internal static let mastodonLogoLarge = ImageAsset(name: "Scene/Welcome/mastodon.logo.large")
|
||||
internal static let signInButtonBackground = ColorAsset(name: "Scene/Welcome/sign.in.button.background")
|
||||
}
|
||||
}
|
||||
internal enum Settings {
|
||||
internal static let blackAuto = ImageAsset(name: "Settings/black.auto")
|
||||
internal static let black = ImageAsset(name: "Settings/black")
|
||||
internal static let darkAuto = ImageAsset(name: "Settings/dark.auto")
|
||||
internal static let dark = ImageAsset(name: "Settings/dark")
|
||||
internal static let light = ImageAsset(name: "Settings/light")
|
||||
}
|
||||
internal enum Theme {
|
||||
internal enum Mastodon {
|
||||
internal static let composeToolbarBackground = ColorAsset(name: "Theme/Mastodon/compose.toolbar.background")
|
||||
internal static let contentWarningOverlayBackground = ColorAsset(name: "Theme/Mastodon/content.warning.overlay.background")
|
||||
internal static let navigationBarBackground = ColorAsset(name: "Theme/Mastodon/navigation.bar.background")
|
||||
internal static let profileFieldCollectionViewBackground = ColorAsset(name: "Theme/Mastodon/profile.field.collection.view.background")
|
||||
internal static let secondaryGroupedSystemBackground = ColorAsset(name: "Theme/Mastodon/secondary.grouped.system.background")
|
||||
internal static let secondarySystemBackground = ColorAsset(name: "Theme/Mastodon/secondary.system.background")
|
||||
internal static let sidebarBackground = ColorAsset(name: "Theme/Mastodon/sidebar.background")
|
||||
internal static let systemBackground = ColorAsset(name: "Theme/Mastodon/system.background")
|
||||
internal static let systemElevatedBackground = ColorAsset(name: "Theme/Mastodon/system.elevated.background")
|
||||
internal static let systemGroupedBackground = ColorAsset(name: "Theme/Mastodon/system.grouped.background")
|
||||
internal static let tabBarBackground = ColorAsset(name: "Theme/Mastodon/tab.bar.background")
|
||||
internal static let tableViewCellBackground = ColorAsset(name: "Theme/Mastodon/table.view.cell.background")
|
||||
internal static let tableViewCellSelectionBackground = ColorAsset(name: "Theme/Mastodon/table.view.cell.selection.background")
|
||||
internal static let tertiarySystemBackground = ColorAsset(name: "Theme/Mastodon/tertiary.system.background")
|
||||
internal static let tertiarySystemGroupedBackground = ColorAsset(name: "Theme/Mastodon/tertiary.system.grouped.background")
|
||||
internal static let notificationStatusBorderColor = ColorAsset(name: "Theme/Mastodon/notification.status.border.color")
|
||||
internal static let separator = ColorAsset(name: "Theme/Mastodon/separator")
|
||||
internal static let tabBarItemInactiveIconColor = ColorAsset(name: "Theme/Mastodon/tab.bar.item.inactive.icon.color")
|
||||
}
|
||||
internal enum System {
|
||||
internal static let composeToolbarBackground = ColorAsset(name: "Theme/system/compose.toolbar.background")
|
||||
internal static let contentWarningOverlayBackground = ColorAsset(name: "Theme/system/content.warning.overlay.background")
|
||||
internal static let navigationBarBackground = ColorAsset(name: "Theme/system/navigation.bar.background")
|
||||
internal static let profileFieldCollectionViewBackground = ColorAsset(name: "Theme/system/profile.field.collection.view.background")
|
||||
internal static let secondaryGroupedSystemBackground = ColorAsset(name: "Theme/system/secondary.grouped.system.background")
|
||||
internal static let secondarySystemBackground = ColorAsset(name: "Theme/system/secondary.system.background")
|
||||
internal static let sidebarBackground = ColorAsset(name: "Theme/system/sidebar.background")
|
||||
internal static let systemBackground = ColorAsset(name: "Theme/system/system.background")
|
||||
internal static let systemElevatedBackground = ColorAsset(name: "Theme/system/system.elevated.background")
|
||||
internal static let systemGroupedBackground = ColorAsset(name: "Theme/system/system.grouped.background")
|
||||
internal static let tabBarBackground = ColorAsset(name: "Theme/system/tab.bar.background")
|
||||
internal static let tableViewCellBackground = ColorAsset(name: "Theme/system/table.view.cell.background")
|
||||
internal static let tableViewCellSelectionBackground = ColorAsset(name: "Theme/system/table.view.cell.selection.background")
|
||||
internal static let tertiarySystemBackground = ColorAsset(name: "Theme/system/tertiary.system.background")
|
||||
internal static let tertiarySystemGroupedBackground = ColorAsset(name: "Theme/system/tertiary.system.grouped.background")
|
||||
internal static let notificationStatusBorderColor = ColorAsset(name: "Theme/system/notification.status.border.color")
|
||||
internal static let separator = ColorAsset(name: "Theme/system/separator")
|
||||
internal static let tabBarItemInactiveIconColor = ColorAsset(name: "Theme/system/tab.bar.item.inactive.icon.color")
|
||||
}
|
||||
}
|
||||
}
|
||||
// swiftlint:enable identifier_name line_length nesting type_body_length type_name
|
||||
|
||||
// MARK: - Implementation Details
|
||||
|
||||
internal final class ColorAsset {
|
||||
internal fileprivate(set) var name: String
|
||||
|
||||
#if os(macOS)
|
||||
internal typealias Color = NSColor
|
||||
#elseif os(iOS) || os(tvOS) || os(watchOS)
|
||||
internal typealias Color = UIColor
|
||||
#endif
|
||||
|
||||
@available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *)
|
||||
internal private(set) lazy var color: Color = {
|
||||
guard let color = Color(asset: self) else {
|
||||
fatalError("Unable to load color asset named \(name).")
|
||||
}
|
||||
return color
|
||||
}()
|
||||
|
||||
fileprivate init(name: String) {
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
internal extension ColorAsset.Color {
|
||||
@available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *)
|
||||
convenience init?(asset: ColorAsset) {
|
||||
let bundle = BundleToken.bundle
|
||||
#if os(iOS) || os(tvOS)
|
||||
self.init(named: asset.name, in: bundle, compatibleWith: nil)
|
||||
#elseif os(macOS)
|
||||
self.init(named: NSColor.Name(asset.name), bundle: bundle)
|
||||
#elseif os(watchOS)
|
||||
self.init(named: asset.name)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
internal struct ImageAsset {
|
||||
internal fileprivate(set) var name: String
|
||||
|
||||
#if os(macOS)
|
||||
internal typealias Image = NSImage
|
||||
#elseif os(iOS) || os(tvOS) || os(watchOS)
|
||||
internal typealias Image = UIImage
|
||||
#endif
|
||||
|
||||
internal var image: Image {
|
||||
let bundle = BundleToken.bundle
|
||||
#if os(iOS) || os(tvOS)
|
||||
let image = Image(named: name, in: bundle, compatibleWith: nil)
|
||||
#elseif os(macOS)
|
||||
let name = NSImage.Name(self.name)
|
||||
let image = (bundle == .main) ? NSImage(named: name) : bundle.image(forResource: name)
|
||||
#elseif os(watchOS)
|
||||
let image = Image(named: name)
|
||||
#endif
|
||||
guard let result = image else {
|
||||
fatalError("Unable to load image asset named \(name).")
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
internal extension ImageAsset.Image {
|
||||
@available(macOS, deprecated,
|
||||
message: "This initializer is unsafe on macOS, please use the ImageAsset.image property")
|
||||
convenience init?(asset: ImageAsset) {
|
||||
#if os(iOS) || os(tvOS)
|
||||
let bundle = BundleToken.bundle
|
||||
self.init(named: asset.name, in: bundle, compatibleWith: nil)
|
||||
#elseif os(macOS)
|
||||
self.init(named: NSImage.Name(asset.name))
|
||||
#elseif os(watchOS)
|
||||
self.init(named: asset.name)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable convenience_type
|
||||
private final class BundleToken {
|
||||
static let bundle: Bundle = {
|
||||
#if SWIFT_PACKAGE
|
||||
return Bundle.module
|
||||
#else
|
||||
return Bundle(for: BundleToken.self)
|
||||
#endif
|
||||
}()
|
||||
}
|
||||
// swiftlint:enable convenience_type
|
|
@ -0,0 +1,12 @@
|
|||
// Generated using Sourcery 1.6.1 — https://github.com/krzysztofzablocki/Sourcery
|
||||
// DO NOT EDIT
|
||||
// sourcery:inline:NotificationTableViewCellDelegate.AutoGenerateProtocolDelegate
|
||||
notificationView(_ notificationView: NotificationView, menuButton button: UIButton, didSelectAction action: NotificationView.AuthorMenuAction, menuContext: NotificationView.AuthorMenuContext)
|
||||
notificationView(_ notificationView: NotificationView, statusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton)
|
||||
notificationView(_ notificationView: NotificationView, statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta)
|
||||
notificationView(_ notificationView: NotificationView, statusView: StatusView, actionToolbarContainer: ActionToolbarContainer, buttonDidPressed button: UIButton, action: ActionToolbarContainer.Action)
|
||||
notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton)
|
||||
notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta)
|
||||
// sourcery:end
|
||||
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
// Generated using Sourcery 1.6.1 — https://github.com/krzysztofzablocki/Sourcery
|
||||
// DO NOT EDIT
|
||||
// sourcery:inline:NotificationViewContainerTableViewCell.AutoGenerateProtocolRelayDelegate
|
||||
func notificationView(_ notificationView: NotificationView, menuButton button: UIButton, didSelectAction action: NotificationView.AuthorMenuAction, menuContext: NotificationView.AuthorMenuContext) {
|
||||
notificationView(notificationView, menuButton: button, didSelectAction: action, menuContext: menuContext)
|
||||
}
|
||||
|
||||
func notificationView(_ notificationView: NotificationView, statusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) {
|
||||
notificationView(notificationView, statusView: statusView, authorAvatarButtonDidPressed: button)
|
||||
}
|
||||
|
||||
func notificationView(_ notificationView: NotificationView, statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) {
|
||||
notificationView(notificationView, statusView: statusView, metaText: metaText, didSelectMeta: meta)
|
||||
}
|
||||
|
||||
func notificationView(_ notificationView: NotificationView, statusView: StatusView, actionToolbarContainer: ActionToolbarContainer, buttonDidPressed button: UIButton, action: ActionToolbarContainer.Action) {
|
||||
notificationView(notificationView, statusView: statusView, actionToolbarContainer: actionToolbarContainer, buttonDidPressed: button, action: action)
|
||||
}
|
||||
|
||||
func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) {
|
||||
notificationView(notificationView, quoteStatusView: quoteStatusView, authorAvatarButtonDidPressed: button)
|
||||
}
|
||||
|
||||
func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) {
|
||||
notificationView(notificationView, quoteStatusView: quoteStatusView, metaText: metaText, didSelectMeta: meta)
|
||||
}
|
||||
|
||||
// sourcery:end
|
||||
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// Generated using Sourcery 1.6.1 — https://github.com/krzysztofzablocki/Sourcery
|
||||
// DO NOT EDIT
|
||||
|
||||
|
||||
// sourcery:inline:FollowingListViewController.AutoGenerateTableViewDelegate
|
||||
|
||||
// Generated using Sourcery
|
||||
// DO NOT EDIT
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
aspectTableView(tableView, didSelectRowAt: indexPath)
|
||||
}
|
||||
|
||||
// sourcery:end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
|
@ -6,10 +6,12 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import MastodonUI
|
||||
|
||||
struct MastodonAuthenticationBox {
|
||||
struct MastodonAuthenticationBox: UserIdentifier {
|
||||
let authenticationRecord: ManagedObjectRecord<MastodonAuthentication>
|
||||
let domain: String
|
||||
let userID: MastodonUser.ID
|
||||
let appAuthorization: Mastodon.API.OAuth.Authorization
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>90</string>
|
||||
<string>96</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
//
|
||||
// MastodonEmojis.swift
|
||||
// MastodonEmojis
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-9-2.
|
||||
// Copyright © 2021 Twidere. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import MastodonMeta
|
||||
|
||||
extension MastodonEmoji {
|
||||
public convenience init(emoji: Mastodon.Entity.Emoji) {
|
||||
self.init(
|
||||
code: emoji.shortcode,
|
||||
url: emoji.url,
|
||||
staticURL: emoji.staticURL,
|
||||
visibleInPicker: emoji.visibleInPicker,
|
||||
category: emoji.category
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Collection where Element == MastodonEmoji {
|
||||
public var asDictionary: MastodonContent.Emojis {
|
||||
var dictionary: MastodonContent.Emojis = [:]
|
||||
for emoji in self {
|
||||
dictionary[emoji.code] = emoji.url
|
||||
}
|
||||
return dictionary
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
//
|
||||
// MastodonField.swift
|
||||
// TwidereX
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-9-18.
|
||||
// Copyright © 2021 Twidere. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
extension MastodonField {
|
||||
public convenience init(field: Mastodon.Entity.Field) {
|
||||
self.init(
|
||||
name: field.name,
|
||||
value: field.value,
|
||||
verifiedAt: field.verifiedAt
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
//
|
||||
// MastodonMention.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-1-17.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
extension MastodonMention {
|
||||
public convenience init(mention: Mastodon.Entity.Mention) {
|
||||
self.init(
|
||||
id: mention.id,
|
||||
username: mention.username,
|
||||
acct: mention.acct,
|
||||
url: mention.url
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
//
|
||||
// MastodonUser+Property.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-1-11.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
extension MastodonUser.Property {
|
||||
init(entity: Mastodon.Entity.Account, domain: String, networkDate: Date) {
|
||||
self.init(
|
||||
identifier: entity.id + "@" + domain,
|
||||
domain: domain,
|
||||
id: entity.id,
|
||||
acct: entity.acct,
|
||||
username: entity.username,
|
||||
displayName: entity.displayName,
|
||||
avatar: entity.avatar,
|
||||
avatarStatic: entity.avatarStatic,
|
||||
header: entity.header,
|
||||
headerStatic: entity.headerStatic,
|
||||
note: entity.note,
|
||||
url: entity.url,
|
||||
statusesCount: Int64(entity.statusesCount),
|
||||
followingCount: Int64(entity.followingCount),
|
||||
followersCount: Int64(entity.followersCount),
|
||||
locked: entity.locked,
|
||||
bot: entity.bot ?? false,
|
||||
suspended: entity.suspended ?? false,
|
||||
createdAt: entity.createdAt,
|
||||
updatedAt: networkDate,
|
||||
emojis: entity.mastodonEmojis,
|
||||
fields: entity.mastodonFields
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// Notification+Property.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-1-21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import class CoreDataStack.Notification
|
||||
|
||||
extension Notification.Property {
|
||||
public init(
|
||||
entity: Mastodon.Entity.Notification,
|
||||
domain: String,
|
||||
userID: MastodonUser.ID,
|
||||
networkDate: Date
|
||||
) {
|
||||
self.init(
|
||||
id: entity.id,
|
||||
typeRaw: entity.type.rawValue,
|
||||
domain: domain,
|
||||
userID: userID,
|
||||
createAt: entity.createdAt,
|
||||
updatedAt: networkDate
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// MastodonPoll.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK on 2021-12-9.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
extension Poll.Property {
|
||||
public init(
|
||||
entity: Mastodon.Entity.Poll,
|
||||
domain: String,
|
||||
networkDate: Date
|
||||
) {
|
||||
self.init(
|
||||
domain: domain,
|
||||
id: entity.id,
|
||||
expiresAt: entity.expiresAt,
|
||||
expired: entity.expired,
|
||||
multiple: entity.multiple,
|
||||
votesCount: Int64(entity.votesCount),
|
||||
votersCount: Int64(entity.votersCount ?? 0),
|
||||
createdAt: networkDate,
|
||||
updatedAt: networkDate
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// MastodonPollOption+Property.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK on 2021-12-9.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
import CoreDataStack
|
||||
|
||||
extension PollOption.Property {
|
||||
public init(
|
||||
index: Int,
|
||||
entity: Mastodon.Entity.Poll.Option,
|
||||
networkDate: Date
|
||||
) {
|
||||
self.init(
|
||||
index: Int64(index),
|
||||
title: entity.title,
|
||||
votesCount: Int64(entity.votesCount ?? 0),
|
||||
createdAt: networkDate,
|
||||
updatedAt: networkDate
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
//
|
||||
// Status+Property.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-1-11.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreGraphics
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
extension Status.Property {
|
||||
init(entity: Mastodon.Entity.Status, domain: String, networkDate: Date) {
|
||||
self.init(
|
||||
identifier: entity.id + "@" + domain,
|
||||
domain: domain,
|
||||
id: entity.id,
|
||||
uri: entity.uri,
|
||||
createdAt: entity.createdAt,
|
||||
content: entity.content ?? "",
|
||||
visibility: entity.mastodonVisibility,
|
||||
sensitive: entity.sensitive ?? false,
|
||||
spoilerText: entity.spoilerText,
|
||||
reblogsCount: Int64(entity.reblogsCount),
|
||||
favouritesCount: Int64(entity.favouritesCount),
|
||||
repliesCount: Int64(entity.repliesCount ?? 0),
|
||||
url: entity.url,
|
||||
inReplyToID: entity.inReplyToID,
|
||||
inReplyToAccountID: entity.inReplyToAccountID,
|
||||
language: entity.language,
|
||||
text: entity.text,
|
||||
updatedAt: networkDate,
|
||||
deletedAt: nil,
|
||||
attachments: entity.mastodonAttachments,
|
||||
emojis: entity.mastodonEmojis,
|
||||
mentions: entity.mastodonMentions
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Mastodon.Entity.Status {
|
||||
public var mastodonVisibility: MastodonVisibility {
|
||||
let rawValue = visibility?.rawValue ?? ""
|
||||
return MastodonVisibility(rawValue: rawValue) ?? ._other(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
extension Mastodon.Entity.Status {
|
||||
public var mastodonAttachments: [MastodonAttachment] {
|
||||
guard let mediaAttachments = mediaAttachments else { return [] }
|
||||
|
||||
let attachments = mediaAttachments.compactMap { media -> MastodonAttachment? in
|
||||
guard let kind = media.attachmentKind,
|
||||
let meta = media.meta,
|
||||
let original = meta.original,
|
||||
let width = original.width, // audio has width/height
|
||||
let height = original.height
|
||||
else { return nil }
|
||||
|
||||
let durationMS: Int? = original.duration.flatMap { Int($0 * 1000) }
|
||||
return MastodonAttachment(
|
||||
id: media.id,
|
||||
kind: kind,
|
||||
size: CGSize(width: width, height: height),
|
||||
focus: nil, // TODO:
|
||||
blurhash: media.blurhash,
|
||||
assetURL: media.url,
|
||||
previewURL: media.previewURL,
|
||||
textURL: media.textURL,
|
||||
durationMS: durationMS,
|
||||
altDescription: media.description
|
||||
)
|
||||
}
|
||||
|
||||
return attachments
|
||||
}
|
||||
}
|
||||
|
||||
extension Mastodon.Entity.Attachment {
|
||||
public var attachmentKind: MastodonAttachment.Kind? {
|
||||
switch type {
|
||||
case .unknown: return nil
|
||||
case .image: return .image
|
||||
case .gifv: return .gifv
|
||||
case .video: return .video
|
||||
case .audio: return .audio
|
||||
case ._other: return nil
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue