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:
|
steps:
|
||||||
- name: checkout
|
- name: checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: force Xcode 13.1
|
- name: force Xcode 13.2.1
|
||||||
run: sudo xcode-select -switch /Applications/Xcode_13.1.app
|
run: sudo xcode-select -switch /Applications/Xcode_13.2.1.app
|
||||||
- name: setup
|
- name: setup
|
||||||
run: exec ./.github/scripts/setup.sh
|
run: exec ./.github/scripts/setup.sh
|
||||||
- name: build
|
- name: build
|
||||||
|
|
|
@ -11,6 +11,10 @@ import CryptoKit
|
||||||
import KeychainAccess
|
import KeychainAccess
|
||||||
import Keys
|
import Keys
|
||||||
|
|
||||||
|
enum AppName {
|
||||||
|
public static let groupID = "group.org.joinmastodon.app"
|
||||||
|
}
|
||||||
|
|
||||||
public final class AppSecret {
|
public final class AppSecret {
|
||||||
|
|
||||||
public static let keychain = Keychain(service: "org.joinmastodon.app.keychain", accessGroup: AppName.groupID)
|
public static let keychain = Keychain(service: "org.joinmastodon.app.keychain", accessGroup: AppName.groupID)
|
||||||
|
|
|
@ -17,6 +17,6 @@
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.3.0</string>
|
<string>1.3.0</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>90</string>
|
<string>96</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import MastodonCommon
|
||||||
|
|
||||||
extension UserDefaults {
|
extension UserDefaults {
|
||||||
public static let shared = UserDefaults(suiteName: AppName.groupID)!
|
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 os.log
|
||||||
import Foundation
|
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
|
// conver i18n JSON templates to strings files
|
||||||
private func convert(from inputDirectoryURL: URL, to outputDirectory: URL) {
|
private func convert(from inputDirectoryURL: URL, to outputDirectory: URL) {
|
||||||
do {
|
do {
|
||||||
|
@ -17,7 +12,6 @@ private func convert(from inputDirectoryURL: URL, to outputDirectory: URL) {
|
||||||
for inputLanguageDirectoryURL in inputLanguageDirectoryURLs {
|
for inputLanguageDirectoryURL in inputLanguageDirectoryURLs {
|
||||||
let language = inputLanguageDirectoryURL.lastPathComponent
|
let language = inputLanguageDirectoryURL.lastPathComponent
|
||||||
guard let mappedLanguage = map(language: language) else { continue }
|
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)
|
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(
|
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)
|
os_log("%{public}s[%{public}ld], %{public}s: process %s", ((#file as NSString).lastPathComponent), #line, #function, jsonURL.debugDescription)
|
||||||
let filename = jsonURL.deletingPathExtension().lastPathComponent
|
let filename = jsonURL.deletingPathExtension().lastPathComponent
|
||||||
guard let (mappedFilename, keyStyle) = map(filename: filename) else { continue }
|
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)
|
let strings = try process(url: jsonURL, keyStyle: keyStyle)
|
||||||
try? FileManager.default.createDirectory(at: outputDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
try? FileManager.default.createDirectory(at: outputDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||||
|
|
||||||
try strings.write(to: outputFileURL, atomically: true, encoding: .utf8)
|
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? {
|
private func map(language: String) -> String? {
|
||||||
switch language {
|
switch language {
|
||||||
case "ar_SA": return "ar" // Arabic (Saudi Arabia)
|
case "ar_SA": return "ar" // Arabic (Saudi Arabia)
|
||||||
|
case "eu_ES": return "eu-ES" // Basque
|
||||||
case "ca_ES": return "ca" // Catalan
|
case "ca_ES": return "ca" // Catalan
|
||||||
case "zh_CN": return "zh-Hans" // Chinese Simplified
|
case "zh_CN": return "zh-Hans" // Chinese Simplified
|
||||||
case "nl_NL": return "nl" // Dutch
|
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 "gd_GB": return "gd-GB" // Scottish Gaelic
|
||||||
case "es_ES": return "es" // Spanish
|
case "es_ES": return "es" // Spanish
|
||||||
case "es_AR": return "es-419" // Spanish, Argentina
|
case "es_AR": return "es-419" // Spanish, Argentina
|
||||||
|
case "sv_FI": return "sv_FI" // Swedish, Finland
|
||||||
case "th_TH": return "th" // Thai
|
case "th_TH": return "th" // Thai
|
||||||
default: return nil
|
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 {
|
private func process(url: URL, keyStyle: Parser.KeyStyle) throws -> String {
|
||||||
do {
|
do {
|
||||||
let data = try Data(contentsOf: url)
|
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)
|
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"
|
// i18n from "Intents/input" to "Intents/output"
|
||||||
let intentsDirectoryURL = packageRootURL.appendingPathComponent("Intents", isDirectory: true)
|
let intentsDirectoryURL = packageRootURL.appendingPathComponent("Intents", isDirectory: true)
|
||||||
|
|
|
@ -45,8 +45,8 @@
|
||||||
"message": "Please enable the photo library access permission to save the photo."
|
"message": "Please enable the photo library access permission to save the photo."
|
||||||
},
|
},
|
||||||
"delete_post": {
|
"delete_post": {
|
||||||
"title": "Are you sure you want to delete this post?",
|
"title": "Delete Post",
|
||||||
"delete": "Delete"
|
"message": "Are you sure you want to delete this post?"
|
||||||
},
|
},
|
||||||
"clean_cache": {
|
"clean_cache": {
|
||||||
"title": "Clean Cache",
|
"title": "Clean Cache",
|
||||||
|
@ -140,7 +140,8 @@
|
||||||
"unreblog": "Undo reblog",
|
"unreblog": "Undo reblog",
|
||||||
"favorite": "Favorite",
|
"favorite": "Favorite",
|
||||||
"unfavorite": "Unfavorite",
|
"unfavorite": "Unfavorite",
|
||||||
"menu": "Menu"
|
"menu": "Menu",
|
||||||
|
"hide": "Hide"
|
||||||
},
|
},
|
||||||
"tag": {
|
"tag": {
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
|
@ -149,6 +150,12 @@
|
||||||
"hashtag": "Hashtag",
|
"hashtag": "Hashtag",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"emoji": "Emoji"
|
"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": {
|
"friendship": {
|
||||||
|
@ -412,14 +419,24 @@
|
||||||
"segmented_control": {
|
"segmented_control": {
|
||||||
"posts": "Posts",
|
"posts": "Posts",
|
||||||
"replies": "Replies",
|
"replies": "Replies",
|
||||||
"media": "Media"
|
"posts_and_replies": "Posts and Replies",
|
||||||
|
"media": "Media",
|
||||||
|
"about": "About"
|
||||||
},
|
},
|
||||||
"relationship_action_alert": {
|
"relationship_action_alert": {
|
||||||
|
"confirm_mute_user": {
|
||||||
|
"title": "Mute Account",
|
||||||
|
"message": "Confirm to mute %s"
|
||||||
|
},
|
||||||
"confirm_unmute_user": {
|
"confirm_unmute_user": {
|
||||||
"title": "Unmute Account",
|
"title": "Unmute Account",
|
||||||
"message": "Confirm to unmute %s"
|
"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",
|
"title": "Unblock Account",
|
||||||
"message": "Confirm to unblock %s"
|
"message": "Confirm to unblock %s"
|
||||||
}
|
}
|
||||||
|
@ -472,12 +489,14 @@
|
||||||
"Everything": "Everything",
|
"Everything": "Everything",
|
||||||
"Mentions": "Mentions"
|
"Mentions": "Mentions"
|
||||||
},
|
},
|
||||||
"user_followed_you": "%s followed you",
|
"notification_description": {
|
||||||
"user_favorited your post": "%s favorited your post",
|
"followed_you": "followd you",
|
||||||
"user_reblogged_your_post": "%s reblogged your post",
|
"favorited_your_post": "favorited your post",
|
||||||
"user_mentioned_you": "%s mentioned you",
|
"reblogged_your_post": "reblogged your post",
|
||||||
"user_requested_to_follow_you": "%s requested to follow you",
|
"mentioned_you": "mentioned you",
|
||||||
"user_your_poll_has_ended": "%s Your poll has ended",
|
"request_to_follow_you": "request to follow you",
|
||||||
|
"poll_has_ended": "poll has ended"
|
||||||
|
},
|
||||||
"keyobard": {
|
"keyobard": {
|
||||||
"show_everything": "Show Everything",
|
"show_everything": "Show Everything",
|
||||||
"show_mentions": "Show Mentions"
|
"show_mentions": "Show Mentions"
|
||||||
|
@ -496,6 +515,13 @@
|
||||||
"light": "Always Light",
|
"light": "Always Light",
|
||||||
"dark": "Always Dark"
|
"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": {
|
"notifications": {
|
||||||
"title": "Notifications",
|
"title": "Notifications",
|
||||||
"favorites": "Favorites my post",
|
"favorites": "Favorites my post",
|
||||||
|
@ -537,14 +563,17 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"report": {
|
"report": {
|
||||||
|
"title_report": "Report",
|
||||||
"title": "Report %s",
|
"title": "Report %s",
|
||||||
"step1": "Step 1 of 2",
|
"step1": "Step 1 of 2",
|
||||||
"step2": "Step 2 of 2",
|
"step2": "Step 2 of 2",
|
||||||
"content1": "Are there any other posts you’d like to add to the report?",
|
"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?",
|
"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",
|
"send": "Send Report",
|
||||||
"skip_to_send": "Send without comment",
|
"skip_to_send": "Send without comment",
|
||||||
"text_placeholder": "Type or paste additional comments"
|
"text_placeholder": "Type or paste additional comments",
|
||||||
|
"reported": "REPORTED"
|
||||||
},
|
},
|
||||||
"preview": {
|
"preview": {
|
||||||
"keyboard": {
|
"keyboard": {
|
||||||
|
@ -564,4 +593,4 @@
|
||||||
"accessibility_hint": "Double tap to dismiss this wizard"
|
"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>
|
<key>AppShared.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>26</integer>
|
<integer>33</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>27</integer>
|
<integer>27</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
|
|
||||||
<dict>
|
|
||||||
<key>orderHint</key>
|
|
||||||
<integer>2</integer>
|
|
||||||
</dict>
|
|
||||||
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
|
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
|
@ -102,7 +97,7 @@
|
||||||
<key>MastodonIntent.xcscheme_^#shared#^_</key>
|
<key>MastodonIntent.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>25</integer>
|
<integer>32</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>MastodonIntents.xcscheme_^#shared#^_</key>
|
<key>MastodonIntents.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
@ -117,15 +112,36 @@
|
||||||
<key>NotificationService.xcscheme_^#shared#^_</key>
|
<key>NotificationService.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>3</integer>
|
<integer>2</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
|
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>24</integer>
|
<integer>31</integer>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>SuppressBuildableAutocreation</key>
|
<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>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
@ -55,15 +55,6 @@
|
||||||
"version": "1.2.0"
|
"version": "1.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"package": "FLAnimatedImage",
|
|
||||||
"repositoryURL": "https://github.com/Flipboard/FLAnimatedImage",
|
|
||||||
"state": {
|
|
||||||
"branch": null,
|
|
||||||
"revision": "e7f9fd4681ae41bf6f3056db08af4f401d61da52",
|
|
||||||
"version": "1.0.16"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"package": "FPSIndicator",
|
"package": "FPSIndicator",
|
||||||
"repositoryURL": "https://github.com/MainasuK/FPSIndicator.git",
|
"repositoryURL": "https://github.com/MainasuK/FPSIndicator.git",
|
||||||
|
@ -96,8 +87,8 @@
|
||||||
"repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git",
|
"repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git",
|
||||||
"state": {
|
"state": {
|
||||||
"branch": null,
|
"branch": null,
|
||||||
"revision": "7af4182f64329440a4656f2cba307cb5848e496a",
|
"revision": "3ea336d3de7938dc112084c596a646e697b0feee",
|
||||||
"version": "2.1.2"
|
"version": "2.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -141,8 +132,8 @@
|
||||||
"repositoryURL": "https://github.com/SDWebImage/SDWebImage.git",
|
"repositoryURL": "https://github.com/SDWebImage/SDWebImage.git",
|
||||||
"state": {
|
"state": {
|
||||||
"branch": null,
|
"branch": null,
|
||||||
"revision": "0fff0d7505b5306348263ea64fcc561253bbeb21",
|
"revision": "2c53f531f1bedd253f55d85105409c28ed4a922c",
|
||||||
"version": "5.12.2"
|
"version": "5.12.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -195,8 +186,8 @@
|
||||||
"repositoryURL": "https://github.com/uias/Tabman",
|
"repositoryURL": "https://github.com/uias/Tabman",
|
||||||
"state": {
|
"state": {
|
||||||
"branch": null,
|
"branch": null,
|
||||||
"revision": "f43489cdd743ba7ad86a422ebb5fcbf34e333df4",
|
"revision": "a9f10cb862a32e6a22549836af013abd6b0692d3",
|
||||||
"version": "2.11.1"
|
"version": "2.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -213,8 +204,8 @@
|
||||||
"repositoryURL": "https://github.com/TimOliver/TOCropViewController.git",
|
"repositoryURL": "https://github.com/TimOliver/TOCropViewController.git",
|
||||||
"state": {
|
"state": {
|
||||||
"branch": null,
|
"branch": null,
|
||||||
"revision": "dad97167bf1be16aeecd109130900995dd01c515",
|
"revision": "d0470491f56e734731bbf77991944c0dfdee3e0e",
|
||||||
"version": "2.6.0"
|
"version": "2.6.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
sources:
|
||||||
|
- .
|
||||||
|
- ../MastodonSDK/Sources
|
||||||
|
templates:
|
||||||
|
- ./Template
|
||||||
|
output:
|
||||||
|
Generated
|
|
@ -7,6 +7,8 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import SafariServices
|
import SafariServices
|
||||||
|
import MastodonAsset
|
||||||
|
import MastodonLocalization
|
||||||
|
|
||||||
final class SafariActivity: UIActivity {
|
final class SafariActivity: UIActivity {
|
||||||
|
|
||||||
|
@ -55,8 +57,10 @@ final class SafariActivity: UIActivity {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sceneCoordinator?.present(scene: .safari(url: url as URL), from: nil, transition: .safariPresent(animated: true, completion: nil))
|
Task {
|
||||||
activityDidFinish(true)
|
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 CoreDataStack
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
import PanModal
|
import PanModal
|
||||||
|
import MastodonAsset
|
||||||
|
import MastodonLocalization
|
||||||
|
|
||||||
final public class SceneCoordinator {
|
final public class SceneCoordinator {
|
||||||
|
|
||||||
|
@ -43,7 +45,7 @@ final public class SceneCoordinator {
|
||||||
return Just(nil).eraseToAnyPublisher()
|
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 {
|
if currentActiveAuthenticationBox.userAuthorization.accessToken == accessToken {
|
||||||
// do nothing if notification for current account
|
// do nothing if notification for current account
|
||||||
return Just(pushNotification).eraseToAnyPublisher()
|
return Just(pushNotification).eraseToAnyPublisher()
|
||||||
|
@ -182,6 +184,8 @@ extension SceneCoordinator {
|
||||||
|
|
||||||
// report
|
// report
|
||||||
case report(viewModel: ReportViewModel)
|
case report(viewModel: ReportViewModel)
|
||||||
|
case reportSupplementary(viewModel: ReportSupplementaryViewModel)
|
||||||
|
case reportResult(viewModel: ReportResultViewModel)
|
||||||
|
|
||||||
// suggestion account
|
// suggestion account
|
||||||
case suggestionAccount(viewModel: SuggestionAccountViewModel)
|
case suggestionAccount(viewModel: SuggestionAccountViewModel)
|
||||||
|
@ -194,10 +198,6 @@ extension SceneCoordinator {
|
||||||
case alertController(alertController: UIAlertController)
|
case alertController(alertController: UIAlertController)
|
||||||
case activityViewController(activityViewController: UIActivityViewController, sourceView: UIView?, barButtonItem: UIBarButtonItem?)
|
case activityViewController(activityViewController: UIActivityViewController, sourceView: UIView?, barButtonItem: UIBarButtonItem?)
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
case publicTimeline
|
|
||||||
#endif
|
|
||||||
|
|
||||||
var isOnboarding: Bool {
|
var isOnboarding: Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case .welcome,
|
case .welcome,
|
||||||
|
@ -211,7 +211,7 @@ extension SceneCoordinator {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} // end enum Scene { }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SceneCoordinator {
|
extension SceneCoordinator {
|
||||||
|
@ -266,6 +266,7 @@ extension SceneCoordinator {
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
|
@MainActor
|
||||||
func present(scene: Scene, from sender: UIViewController?, transition: Transition) -> UIViewController? {
|
func present(scene: Scene, from sender: UIViewController?, transition: Transition) -> UIViewController? {
|
||||||
guard let viewController = get(scene: scene) else {
|
guard let viewController = get(scene: scene) else {
|
||||||
return nil
|
return nil
|
||||||
|
@ -442,6 +443,18 @@ private extension SceneCoordinator {
|
||||||
let _viewController = FollowingListViewController()
|
let _viewController = FollowingListViewController()
|
||||||
_viewController.viewModel = viewModel
|
_viewController.viewModel = viewModel
|
||||||
viewController = _viewController
|
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):
|
case .suggestionAccount(let viewModel):
|
||||||
let _viewController = SuggestionAccountViewController()
|
let _viewController = SuggestionAccountViewController()
|
||||||
_viewController.viewModel = viewModel
|
_viewController.viewModel = viewModel
|
||||||
|
@ -477,16 +490,6 @@ private extension SceneCoordinator {
|
||||||
let _viewController = SettingsViewController()
|
let _viewController = SettingsViewController()
|
||||||
_viewController.viewModel = viewModel
|
_viewController.viewModel = viewModel
|
||||||
viewController = _viewController
|
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)
|
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 CoreData
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import CoreDataStack
|
||||||
|
|
||||||
enum SelectedAccountItem {
|
enum SelectedAccountItem: Hashable {
|
||||||
case accountObjectID(accountObjectID: NSManagedObjectID)
|
case account(ManagedObjectRecord<MastodonUser>)
|
||||||
case placeHolder(uuid: UUID)
|
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 {
|
extension SelectedAccountSection {
|
||||||
static func collectionViewDiffableDataSource(
|
static func collectionViewDiffableDataSource(
|
||||||
for collectionView: UICollectionView,
|
collectionView: UICollectionView,
|
||||||
managedObjectContext: NSManagedObjectContext
|
context: AppContext
|
||||||
) -> UICollectionViewDiffableDataSource<SelectedAccountSection, SelectedAccountItem> {
|
) -> UICollectionViewDiffableDataSource<SelectedAccountSection, SelectedAccountItem> {
|
||||||
UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, item -> UICollectionViewCell? in
|
UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, item -> UICollectionViewCell? in
|
||||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: SuggestionAccountCollectionViewCell.self), for: indexPath) as! SuggestionAccountCollectionViewCell
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: SuggestionAccountCollectionViewCell.self), for: indexPath) as! SuggestionAccountCollectionViewCell
|
||||||
switch item {
|
switch item {
|
||||||
case .accountObjectID(let objectID):
|
case .account(let record):
|
||||||
let user = managedObjectContext.object(with: objectID) as! MastodonUser
|
context.managedObjectContext.performAndWait {
|
||||||
cell.config(with: user)
|
guard let user = record.object(in: context.managedObjectContext) else { return }
|
||||||
|
cell.config(with: user)
|
||||||
|
}
|
||||||
case .placeHolder:
|
case .placeHolder:
|
||||||
cell.configAsPlaceHolder()
|
cell.configAsPlaceHolder()
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
import MastodonMeta
|
import MastodonMeta
|
||||||
|
import MastodonAsset
|
||||||
|
import MastodonLocalization
|
||||||
|
|
||||||
enum AutoCompleteSection: Equatable, Hashable {
|
enum AutoCompleteSection: Equatable, Hashable {
|
||||||
case main
|
case main
|
||||||
|
@ -80,7 +82,7 @@ extension AutoCompleteSection {
|
||||||
}
|
}
|
||||||
cell.subtitleLabel.text = "@" + account.acct
|
cell.subtitleLabel.text = "@" + account.acct
|
||||||
cell.avatarImageView.isHidden = false
|
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) {
|
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 = isFirst ? L10n.Scene.Compose.AutoComplete.spaceToAdd : " "
|
||||||
cell.subtitleLabel.text = " "
|
cell.subtitleLabel.text = " "
|
||||||
cell.avatarImageView.isHidden = false
|
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 Combine
|
||||||
import CoreData
|
import CoreData
|
||||||
import MastodonMeta
|
import MastodonMeta
|
||||||
|
import CoreDataStack
|
||||||
|
|
||||||
/// Note: update Equatable when change case
|
/// Note: update Equatable when change case
|
||||||
enum ComposeStatusItem {
|
enum ComposeStatusItem {
|
||||||
case replyTo(statusObjectID: NSManagedObjectID)
|
case replyTo(record: ManagedObjectRecord<Status>)
|
||||||
case input(replyToStatusObjectID: NSManagedObjectID?, attribute: ComposeStatusAttribute)
|
case input(replyTo: ManagedObjectRecord<Status>?, attribute: ComposeStatusAttribute)
|
||||||
case attachment(attachmentAttribute: ComposeStatusAttachmentAttribute)
|
case attachment(attachmentAttribute: ComposeStatusAttachmentAttribute)
|
||||||
case pollOption(pollOptionAttributes: [ComposeStatusPollItem.PollOptionAttribute], pollExpiresOptionAttribute: ComposeStatusPollItem.PollExpiresOptionAttribute)
|
case pollOption(pollOptionAttributes: [ComposeStatusPollItem.PollOptionAttribute], pollExpiresOptionAttribute: ComposeStatusPollItem.PollExpiresOptionAttribute)
|
||||||
}
|
}
|
||||||
|
@ -21,26 +22,21 @@ enum ComposeStatusItem {
|
||||||
extension ComposeStatusItem: Hashable { }
|
extension ComposeStatusItem: Hashable { }
|
||||||
|
|
||||||
extension ComposeStatusItem {
|
extension ComposeStatusItem {
|
||||||
final class ComposeStatusAttribute: Equatable, Hashable {
|
final class ComposeStatusAttribute: Hashable {
|
||||||
private let id = UUID()
|
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)
|
@Published var author: ManagedObjectRecord<MastodonUser>?
|
||||||
let contentWarningContent = CurrentValueSubject<String, Never>("")
|
|
||||||
|
@Published var composeContent: String?
|
||||||
|
|
||||||
|
@Published var isContentWarningComposing = false
|
||||||
|
@Published var contentWarningContent = ""
|
||||||
|
|
||||||
static func == (lhs: ComposeStatusAttribute, rhs: ComposeStatusAttribute) -> Bool {
|
static func == (lhs: ComposeStatusAttribute, rhs: ComposeStatusAttribute) -> Bool {
|
||||||
return lhs.avatarURL.value == rhs.avatarURL.value &&
|
return lhs.author == rhs.author
|
||||||
lhs.displayName.value == rhs.displayName.value &&
|
&& lhs.composeContent == rhs.composeContent
|
||||||
lhs.emojiMeta.value == rhs.emojiMeta.value &&
|
&& lhs.isContentWarningComposing == rhs.isContentWarningComposing
|
||||||
lhs.username.value == rhs.username.value &&
|
&& lhs.contentWarningContent == rhs.contentWarningContent
|
||||||
lhs.composeContent.value == rhs.composeContent.value &&
|
|
||||||
lhs.isContentWarningComposing.value == rhs.isContentWarningComposing.value &&
|
|
||||||
lhs.contentWarningContent.value == rhs.contentWarningContent.value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
func hash(into hasher: inout Hasher) {
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
|
import MastodonAsset
|
||||||
|
import MastodonLocalization
|
||||||
|
|
||||||
enum ComposeStatusPollItem {
|
enum ComposeStatusPollItem {
|
||||||
case pollOption(attribute: PollOptionAttribute)
|
case pollOption(attribute: PollOptionAttribute)
|
||||||
|
|
|
@ -14,7 +14,7 @@ import MastodonMeta
|
||||||
import AlamofireImage
|
import AlamofireImage
|
||||||
|
|
||||||
enum ComposeStatusSection: Equatable, Hashable {
|
enum ComposeStatusSection: Equatable, Hashable {
|
||||||
case repliedTo
|
case replyTo
|
||||||
case status
|
case status
|
||||||
case attachment
|
case attachment
|
||||||
case poll
|
case poll
|
||||||
|
@ -24,43 +24,44 @@ extension ComposeStatusSection {
|
||||||
enum ComposeKind {
|
enum ComposeKind {
|
||||||
case post
|
case post
|
||||||
case hashtag(hashtag: String)
|
case hashtag(hashtag: String)
|
||||||
case mention(mastodonUserObjectID: NSManagedObjectID)
|
case mention(user: ManagedObjectRecord<MastodonUser>)
|
||||||
case reply(repliedToStatusObjectID: NSManagedObjectID)
|
case reply(status: ManagedObjectRecord<Status>)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ComposeStatusSection {
|
extension ComposeStatusSection {
|
||||||
|
|
||||||
static func configureStatusContent(
|
static func configure(
|
||||||
cell: ComposeStatusContentTableViewCell,
|
cell: ComposeStatusContentTableViewCell,
|
||||||
attribute: ComposeStatusItem.ComposeStatusAttribute
|
attribute: ComposeStatusItem.ComposeStatusAttribute
|
||||||
) {
|
) {
|
||||||
// set avatar
|
// cell.prepa
|
||||||
attribute.avatarURL
|
// // set avatar
|
||||||
.receive(on: DispatchQueue.main)
|
// attribute.avatarURL
|
||||||
.sink { avatarURL in
|
// .receive(on: DispatchQueue.main)
|
||||||
cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: avatarURL))
|
// .sink { avatarURL in
|
||||||
}
|
// cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: avatarURL))
|
||||||
.store(in: &cell.disposeBag)
|
// }
|
||||||
// set display name and username
|
// .store(in: &cell.disposeBag)
|
||||||
Publishers.CombineLatest3(
|
// // set display name and username
|
||||||
attribute.displayName,
|
// Publishers.CombineLatest3(
|
||||||
attribute.emojiMeta,
|
// attribute.displayName,
|
||||||
attribute.username
|
// attribute.emojiMeta,
|
||||||
)
|
// attribute.username
|
||||||
.receive(on: DispatchQueue.main)
|
// )
|
||||||
.sink { displayName, emojiMeta, username in
|
// .receive(on: DispatchQueue.main)
|
||||||
do {
|
// .sink { displayName, emojiMeta, username in
|
||||||
let mastodonContent = MastodonContent(content: displayName ?? " ", emojis: emojiMeta)
|
// do {
|
||||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
// let mastodonContent = MastodonContent(content: displayName ?? " ", emojis: emojiMeta)
|
||||||
cell.statusView.nameLabel.configure(content: metaContent)
|
// let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||||
} catch {
|
// cell.statusView.nameLabel.configure(content: metaContent)
|
||||||
let metaContent = PlaintextMetaContent(string: " ")
|
// } catch {
|
||||||
cell.statusView.nameLabel.configure(content: metaContent)
|
// let metaContent = PlaintextMetaContent(string: " ")
|
||||||
}
|
// cell.statusView.nameLabel.configure(content: metaContent)
|
||||||
cell.statusView.usernameLabel.text = username.flatMap { "@" + $0 } ?? " "
|
// }
|
||||||
}
|
// cell.statusView.usernameLabel.text = username.flatMap { "@" + $0 } ?? " "
|
||||||
.store(in: &cell.disposeBag)
|
// }
|
||||||
|
// .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)
|
let userID = CurrentValueSubject<Mastodon.Entity.Status.ID?, Never>(nil)
|
||||||
|
|
||||||
// output
|
// output
|
||||||
let objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([])
|
let _objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([])
|
||||||
|
@Published var records: [ManagedObjectRecord<SearchHistory>] = []
|
||||||
|
|
||||||
init(managedObjectContext: NSManagedObjectContext) {
|
init(managedObjectContext: NSManagedObjectContext) {
|
||||||
self.fetchedResultsController = {
|
self.fetchedResultsController = {
|
||||||
let fetchRequest = SearchHistory.sortedFetchRequest
|
let fetchRequest = SearchHistory.sortedFetchRequest
|
||||||
|
@ -38,12 +39,18 @@ final class SearchHistoryFetchedResultController: NSObject {
|
||||||
return controller
|
return controller
|
||||||
}()
|
}()
|
||||||
super.init()
|
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
|
fetchedResultsController.delegate = self
|
||||||
|
|
||||||
Publishers.CombineLatest(
|
Publishers.CombineLatest(
|
||||||
self.domain.removeDuplicates(),
|
self.domain,
|
||||||
self.userID.removeDuplicates()
|
self.userID
|
||||||
)
|
)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] domain, userID in
|
.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)
|
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
|
||||||
let objects = fetchedResultsController.fetchedObjects ?? []
|
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 CoreData
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
import MastodonUI
|
||||||
|
|
||||||
final class StatusFetchedResultsController: NSObject {
|
final class StatusFetchedResultsController: NSObject {
|
||||||
|
|
||||||
|
@ -23,7 +24,8 @@ final class StatusFetchedResultsController: NSObject {
|
||||||
let statusIDs = CurrentValueSubject<[Mastodon.Entity.Status.ID], Never>([])
|
let statusIDs = CurrentValueSubject<[Mastodon.Entity.Status.ID], Never>([])
|
||||||
|
|
||||||
// output
|
// output
|
||||||
let objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([])
|
let _objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([])
|
||||||
|
@Published var records: [ManagedObjectRecord<Status>] = []
|
||||||
|
|
||||||
init(managedObjectContext: NSManagedObjectContext, domain: String?, additionalTweetPredicate: NSPredicate?) {
|
init(managedObjectContext: NSManagedObjectContext, domain: String?, additionalTweetPredicate: NSPredicate?) {
|
||||||
self.domain.value = domain ?? ""
|
self.domain.value = domain ?? ""
|
||||||
|
@ -43,11 +45,17 @@ final class StatusFetchedResultsController: NSObject {
|
||||||
}()
|
}()
|
||||||
super.init()
|
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
|
fetchedResultsController.delegate = self
|
||||||
|
|
||||||
Publishers.CombineLatest(
|
Publishers.CombineLatest(
|
||||||
self.domain.removeDuplicates().eraseToAnyPublisher(),
|
self.domain.removeDuplicates(),
|
||||||
self.statusIDs.removeDuplicates().eraseToAnyPublisher()
|
self.statusIDs.removeDuplicates()
|
||||||
)
|
)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] domain, ids in
|
.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
|
// MARK: - NSFetchedResultsControllerDelegate
|
||||||
extension StatusFetchedResultsController: NSFetchedResultsControllerDelegate {
|
extension StatusFetchedResultsController: NSFetchedResultsControllerDelegate {
|
||||||
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
|
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
|
||||||
|
@ -82,6 +102,6 @@ extension StatusFetchedResultsController: NSFetchedResultsControllerDelegate {
|
||||||
}
|
}
|
||||||
.sorted { $0.0 < $1.0 }
|
.sorted { $0.0 < $1.0 }
|
||||||
.map { $0.1.objectID }
|
.map { $0.1.objectID }
|
||||||
self.objectIDs.value = items
|
self._objectIDs.value = items
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import Combine
|
||||||
import CoreData
|
import CoreData
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
import MastodonUI
|
||||||
|
|
||||||
final class UserFetchedResultsController: NSObject {
|
final class UserFetchedResultsController: NSObject {
|
||||||
|
|
||||||
|
@ -19,14 +20,15 @@ final class UserFetchedResultsController: NSObject {
|
||||||
let fetchedResultsController: NSFetchedResultsController<MastodonUser>
|
let fetchedResultsController: NSFetchedResultsController<MastodonUser>
|
||||||
|
|
||||||
// input
|
// input
|
||||||
let domain = CurrentValueSubject<String?, Never>(nil)
|
@Published var domain: String? = nil
|
||||||
let userIDs = CurrentValueSubject<[Mastodon.Entity.Account.ID], Never>([])
|
@Published var userIDs: [Mastodon.Entity.Account.ID] = []
|
||||||
|
|
||||||
// output
|
// output
|
||||||
let objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([])
|
let _objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([])
|
||||||
|
@Published var records: [ManagedObjectRecord<MastodonUser>] = []
|
||||||
|
|
||||||
init(managedObjectContext: NSManagedObjectContext, domain: String?, additionalTweetPredicate: NSPredicate?) {
|
init(managedObjectContext: NSManagedObjectContext, domain: String?, additionalTweetPredicate: NSPredicate?) {
|
||||||
self.domain.value = domain ?? ""
|
self.domain = domain ?? ""
|
||||||
self.fetchedResultsController = {
|
self.fetchedResultsController = {
|
||||||
let fetchRequest = MastodonUser.sortedFetchRequest
|
let fetchRequest = MastodonUser.sortedFetchRequest
|
||||||
fetchRequest.predicate = MastodonUser.predicate(domain: domain ?? "", ids: [])
|
fetchRequest.predicate = MastodonUser.predicate(domain: domain ?? "", ids: [])
|
||||||
|
@ -42,12 +44,18 @@ final class UserFetchedResultsController: NSObject {
|
||||||
return controller
|
return controller
|
||||||
}()
|
}()
|
||||||
super.init()
|
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
|
fetchedResultsController.delegate = self
|
||||||
|
|
||||||
Publishers.CombineLatest(
|
Publishers.CombineLatest(
|
||||||
self.domain.removeDuplicates().eraseToAnyPublisher(),
|
self.$domain.removeDuplicates(),
|
||||||
self.userIDs.removeDuplicates().eraseToAnyPublisher()
|
self.$userIDs.removeDuplicates()
|
||||||
)
|
)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] domain, ids in
|
.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
|
// MARK: - NSFetchedResultsControllerDelegate
|
||||||
extension UserFetchedResultsController: NSFetchedResultsControllerDelegate {
|
extension UserFetchedResultsController: NSFetchedResultsControllerDelegate {
|
||||||
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
|
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
|
||||||
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
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 objects = fetchedResultsController.fetchedObjects ?? []
|
||||||
|
|
||||||
let items: [NSManagedObjectID] = objects
|
let items: [NSManagedObjectID] = objects
|
||||||
|
@ -82,6 +102,6 @@ extension UserFetchedResultsController: NSFetchedResultsControllerDelegate {
|
||||||
}
|
}
|
||||||
.sorted { $0.0 < $1.0 }
|
.sorted { $0.0 < $1.0 }
|
||||||
.map { $0.1.objectID }
|
.map { $0.1.objectID }
|
||||||
self.objectIDs.value = items
|
self._objectIDs.value = items
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,50 +7,10 @@
|
||||||
|
|
||||||
import CoreData
|
import CoreData
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import CoreDataStack
|
||||||
|
|
||||||
enum NotificationItem {
|
enum NotificationItem: Hashable {
|
||||||
case notification(objectID: NSManagedObjectID, attribute: Item.StatusAttribute)
|
case feed(record: ManagedObjectRecord<Feed>)
|
||||||
case notificationStatus(objectID: NSManagedObjectID, attribute: Item.StatusAttribute) // display notification status without card wrapper
|
case feedLoader(record: ManagedObjectRecord<Feed>)
|
||||||
case bottomLoader
|
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 UIKit
|
||||||
import MetaTextKit
|
import MetaTextKit
|
||||||
import MastodonMeta
|
import MastodonMeta
|
||||||
|
import MastodonAsset
|
||||||
|
import MastodonLocalization
|
||||||
|
|
||||||
enum NotificationSection: Equatable, Hashable {
|
enum NotificationSection: Equatable, Hashable {
|
||||||
case main
|
case main
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NotificationSection {
|
extension NotificationSection {
|
||||||
static func tableViewDiffableDataSource(
|
|
||||||
for tableView: UITableView,
|
struct Configuration {
|
||||||
dependency: NeedsDependency,
|
weak var notificationTableViewCellDelegate: NotificationTableViewCellDelegate?
|
||||||
managedObjectContext: NSManagedObjectContext,
|
}
|
||||||
delegate: NotificationTableViewCellDelegate,
|
|
||||||
statusTableViewCellDelegate: StatusTableViewCellDelegate
|
static func diffableDataSource(
|
||||||
|
tableView: UITableView,
|
||||||
|
context: AppContext,
|
||||||
|
configuration: Configuration
|
||||||
) -> UITableViewDiffableDataSource<NotificationSection, NotificationItem> {
|
) -> UITableViewDiffableDataSource<NotificationSection, NotificationItem> {
|
||||||
UITableViewDiffableDataSource(tableView: tableView) {
|
tableView.register(NotificationTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationTableViewCell.self))
|
||||||
[weak delegate, weak dependency]
|
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
|
||||||
(tableView, indexPath, notificationItem) -> UITableViewCell? in
|
|
||||||
guard let dependency = dependency else { return nil }
|
return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
|
||||||
switch notificationItem {
|
switch item {
|
||||||
case .notification(let objectID, let attribute):
|
case .feed(let record):
|
||||||
guard let notification = try? managedObjectContext.existingObject(with: objectID) as? MastodonNotification,
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationTableViewCell.self), for: indexPath) as! NotificationTableViewCell
|
||||||
!notification.isDeleted
|
context.managedObjectContext.performAndWait {
|
||||||
else { return UITableViewCell() }
|
guard let feed = record.object(in: context.managedObjectContext) else { return }
|
||||||
|
configure(
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationStatusTableViewCell.self), for: indexPath) as! NotificationStatusTableViewCell
|
context: context,
|
||||||
configure(
|
tableView: tableView,
|
||||||
tableView: tableView,
|
cell: cell,
|
||||||
cell: cell,
|
viewModel: NotificationTableViewCell.ViewModel(value: .feed(feed)),
|
||||||
notification: notification,
|
configuration: configuration
|
||||||
dependency: dependency,
|
)
|
||||||
attribute: attribute
|
}
|
||||||
)
|
|
||||||
cell.delegate = delegate
|
|
||||||
cell.isAccessibilityElement = true
|
|
||||||
NotificationSection.configureStatusAccessibilityLabel(cell: cell)
|
|
||||||
return cell
|
return cell
|
||||||
|
case .feedLoader:
|
||||||
case .notificationStatus(objectID: let objectID, attribute: let attribute):
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
||||||
guard let notification = try? managedObjectContext.existingObject(with: objectID) as? MastodonNotification,
|
cell.activityIndicatorView.startAnimating()
|
||||||
!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
|
return cell
|
||||||
|
|
||||||
case .bottomLoader:
|
case .bottomLoader:
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) as! TimelineBottomLoaderTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
||||||
cell.startAnimating()
|
cell.activityIndicatorView.startAnimating()
|
||||||
return cell
|
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 {
|
extension NotificationSection {
|
||||||
|
|
||||||
static func configure(
|
static func configure(
|
||||||
|
context: AppContext,
|
||||||
tableView: UITableView,
|
tableView: UITableView,
|
||||||
cell: NotificationStatusTableViewCell,
|
cell: NotificationTableViewCell,
|
||||||
notification: MastodonNotification,
|
viewModel: NotificationTableViewCell.ViewModel,
|
||||||
dependency: NeedsDependency,
|
configuration: Configuration
|
||||||
attribute: Item.StatusAttribute
|
|
||||||
) {
|
) {
|
||||||
// configure author
|
StatusSection.setupStatusPollDataSource(
|
||||||
cell.configure(
|
context: context,
|
||||||
with: AvatarConfigurableViewConfiguration(
|
statusView: cell.notificationView.statusView
|
||||||
avatarImageURL: notification.account.avatarImageURL()
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func createActionImage() -> UIImage? {
|
StatusSection.setupStatusPollDataSource(
|
||||||
return UIImage(
|
context: context,
|
||||||
systemName: notification.notificationType.actionImageName,
|
statusView: cell.notificationView.quoteStatusView
|
||||||
withConfiguration: UIImage.SymbolConfiguration(
|
)
|
||||||
pointSize: 12, weight: .semibold
|
|
||||||
)
|
|
||||||
)?
|
|
||||||
.withTintColor(.systemBackground)
|
|
||||||
.af.imageAspectScaled(toFit: CGSize(width: 14, height: 14))
|
|
||||||
}
|
|
||||||
|
|
||||||
cell.avatarButton.badgeImageView.backgroundColor = notification.notificationType.color
|
context.authenticationService.activeMastodonAuthenticationBox
|
||||||
cell.avatarButton.badgeImageView.image = createActionImage()
|
.map { $0 as UserIdentifier? }
|
||||||
cell.traitCollectionDidChange
|
.assign(to: \.userIdentifier, on: cell.notificationView.viewModel)
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink { [weak cell] in
|
|
||||||
guard let cell = cell else { return }
|
|
||||||
cell.avatarButton.badgeImageView.image = createActionImage()
|
|
||||||
}
|
|
||||||
.store(in: &cell.disposeBag)
|
.store(in: &cell.disposeBag)
|
||||||
|
|
||||||
// configure author name, notification description, timestamp
|
cell.configure(
|
||||||
let nameText = notification.account.displayNameWithFallback
|
tableView: tableView,
|
||||||
let titleLabelText: String = {
|
viewModel: viewModel,
|
||||||
switch notification.notificationType {
|
delegate: configuration.notificationTableViewCellDelegate
|
||||||
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) {
|
// static func configure(
|
||||||
// FIXME:
|
// tableView: UITableView,
|
||||||
cell.accessibilityLabel = {
|
// cell: NotificationStatusTableViewCell,
|
||||||
var accessibilityViews: [UIView?] = []
|
// notification: MastodonNotification,
|
||||||
accessibilityViews.append(contentsOf: [
|
// dependency: NeedsDependency,
|
||||||
cell.titleLabel,
|
// attribute: Item.StatusAttribute
|
||||||
cell.timestampLabel,
|
// ) {
|
||||||
cell.statusView
|
// // configure author
|
||||||
])
|
// cell.configure(
|
||||||
if !cell.statusContainerView.isHidden {
|
// with: AvatarConfigurableViewConfiguration(
|
||||||
if !cell.statusView.headerContainerView.isHidden {
|
// avatarImageURL: notification.account.avatarImageURL()
|
||||||
accessibilityViews.append(cell.statusView.headerInfoLabel)
|
// )
|
||||||
}
|
// )
|
||||||
accessibilityViews.append(contentsOf: [
|
//
|
||||||
cell.statusView.nameMetaLabel,
|
// func createActionImage() -> UIImage? {
|
||||||
cell.statusView.dateLabel,
|
// return UIImage(
|
||||||
cell.statusView.contentMetaText.textView,
|
// systemName: notification.notificationType.actionImageName,
|
||||||
])
|
// withConfiguration: UIImage.SymbolConfiguration(
|
||||||
}
|
// pointSize: 12, weight: .semibold
|
||||||
return accessibilityViews
|
// )
|
||||||
.compactMap { $0?.accessibilityLabel }
|
// )?
|
||||||
.joined(separator: " ")
|
// .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 Foundation
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
import MastodonAsset
|
||||||
|
import MastodonLocalization
|
||||||
|
|
||||||
/// Note: update Equatable when change case
|
/// Note: update Equatable when change case
|
||||||
enum CategoryPickerItem {
|
enum CategoryPickerItem {
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import MastodonAsset
|
||||||
|
import MastodonLocalization
|
||||||
|
|
||||||
enum CategoryPickerSection: Equatable, Hashable {
|
enum CategoryPickerSection: Equatable, Hashable {
|
||||||
case main
|
case main
|
||||||
|
|
|
@ -21,7 +21,11 @@ extension PickServerSection {
|
||||||
dependency: NeedsDependency,
|
dependency: NeedsDependency,
|
||||||
pickServerCellDelegate: PickServerCellDelegate
|
pickServerCellDelegate: PickServerCellDelegate
|
||||||
) -> UITableViewDiffableDataSource<PickServerSection, PickServerItem> {
|
) -> 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 dependency,
|
||||||
weak pickServerCellDelegate
|
weak pickServerCellDelegate
|
||||||
] tableView, indexPath, item -> UITableViewCell? in
|
] tableView, indexPath, item -> UITableViewCell? in
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import MastodonAsset
|
||||||
|
import MastodonLocalization
|
||||||
|
|
||||||
enum ServerRuleSection: Hashable {
|
enum ServerRuleSection: Hashable {
|
||||||
case header
|
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 MastodonSDK
|
||||||
import MastodonMeta
|
import MastodonMeta
|
||||||
|
|
||||||
enum ProfileFieldItem {
|
enum ProfileFieldItem: Hashable {
|
||||||
case field(field: FieldValue, attribute: FieldItemAttribute)
|
case field(field: FieldValue)
|
||||||
case addEntry(attribute: AddEntryItemAttribute)
|
case editField(field: FieldValue)
|
||||||
}
|
case addEntry
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileFieldItem {
|
extension ProfileFieldItem {
|
||||||
|
@ -36,17 +22,29 @@ extension ProfileFieldItem {
|
||||||
|
|
||||||
var name: CurrentValueSubject<String, Never>
|
var name: CurrentValueSubject<String, Never>
|
||||||
var value: 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.id = id
|
||||||
self.name = CurrentValueSubject(name)
|
self.name = CurrentValueSubject(name)
|
||||||
self.value = CurrentValueSubject(value)
|
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
|
return lhs.id == rhs.id
|
||||||
&& lhs.name.value == rhs.name.value
|
&& lhs.name.value == rhs.name.value
|
||||||
&& lhs.value.value == rhs.value.value
|
&& lhs.value.value == rhs.value.value
|
||||||
|
&& lhs.emojiMeta == rhs.emojiMeta
|
||||||
}
|
}
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
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 UIKit
|
||||||
import Combine
|
import Combine
|
||||||
import MastodonMeta
|
import MastodonMeta
|
||||||
|
import MastodonLocalization
|
||||||
|
|
||||||
enum ProfileFieldSection: Equatable, Hashable {
|
enum ProfileFieldSection: Equatable, Hashable {
|
||||||
case main
|
case main
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileFieldSection {
|
extension ProfileFieldSection {
|
||||||
static func collectionViewDiffableDataSource(
|
|
||||||
for collectionView: UICollectionView,
|
struct Configuration {
|
||||||
profileFieldCollectionViewCellDelegate: ProfileFieldCollectionViewCellDelegate,
|
weak var profileFieldCollectionViewCellDelegate: ProfileFieldCollectionViewCellDelegate?
|
||||||
profileFieldAddEntryCollectionViewCellDelegate: ProfileFieldAddEntryCollectionViewCellDelegate
|
weak var profileFieldEditCollectionViewCellDelegate: ProfileFieldEditCollectionViewCellDelegate?
|
||||||
|
}
|
||||||
|
|
||||||
|
static func diffableDataSource(
|
||||||
|
collectionView: UICollectionView,
|
||||||
|
context: AppContext,
|
||||||
|
configuration: Configuration
|
||||||
) -> UICollectionViewDiffableDataSource<ProfileFieldSection, ProfileFieldItem> {
|
) -> UICollectionViewDiffableDataSource<ProfileFieldSection, ProfileFieldItem> {
|
||||||
let dataSource = UICollectionViewDiffableDataSource<ProfileFieldSection, ProfileFieldItem>(collectionView: collectionView) {
|
collectionView.register(ProfileFieldCollectionViewHeaderFooterView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: ProfileFieldCollectionViewHeaderFooterView.headerReuseIdentifer)
|
||||||
[
|
collectionView.register(ProfileFieldCollectionViewHeaderFooterView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: ProfileFieldCollectionViewHeaderFooterView.footerReuseIdentifer)
|
||||||
weak profileFieldCollectionViewCellDelegate,
|
|
||||||
weak profileFieldAddEntryCollectionViewCellDelegate
|
let fieldCellRegistration = UICollectionView.CellRegistration<ProfileFieldCollectionViewCell, ProfileFieldItem> { cell, indexPath, item in
|
||||||
] collectionView, 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 {
|
switch item {
|
||||||
case .field(let field, let attribute):
|
case .field:
|
||||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ProfileFieldCollectionViewCell.self), for: indexPath) as! ProfileFieldCollectionViewCell
|
return collectionView.dequeueConfiguredReusableCell(
|
||||||
|
using: fieldCellRegistration,
|
||||||
// set key
|
for: indexPath,
|
||||||
do {
|
item: item
|
||||||
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()
|
|
||||||
)
|
)
|
||||||
.receive(on: RunLoop.main)
|
case .editField:
|
||||||
.sink { [weak cell] name, emojiMeta in
|
return collectionView.dequeueConfiguredReusableCell(
|
||||||
guard let cell = cell else { return }
|
using: editFieldCellRegistration,
|
||||||
do {
|
for: indexPath,
|
||||||
let mastodonContent = MastodonContent(content: name, emojis: emojiMeta)
|
item: item
|
||||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
)
|
||||||
cell.fieldView.titleMetaLabel.configure(content: metaContent)
|
case .addEntry:
|
||||||
} catch {
|
return collectionView.dequeueConfiguredReusableCell(
|
||||||
let content = PlaintextMetaContent(string: name)
|
using: addEntryCellRegistration,
|
||||||
cell.fieldView.titleMetaLabel.configure(content: content)
|
for: indexPath,
|
||||||
}
|
item: item
|
||||||
// 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()
|
|
||||||
)
|
)
|
||||||
.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 {
|
switch kind {
|
||||||
case UICollectionView.elementKindSectionHeader:
|
case UICollectionView.elementKindSectionHeader:
|
||||||
let reusableView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ProfileFieldCollectionViewHeaderFooterView.headerReuseIdentifer, for: indexPath) as! ProfileFieldCollectionViewHeaderFooterView
|
let reusableView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ProfileFieldCollectionViewHeaderFooterView.headerReuseIdentifer, for: indexPath) as! ProfileFieldCollectionViewHeaderFooterView
|
||||||
|
reusableView.frame.size.height = 20
|
||||||
return reusableView
|
return reusableView
|
||||||
case UICollectionView.elementKindSectionFooter:
|
case UICollectionView.elementKindSectionFooter:
|
||||||
let reusableView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ProfileFieldCollectionViewHeaderFooterView.footerReuseIdentifer, for: indexPath) as! ProfileFieldCollectionViewHeaderFooterView
|
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 Foundation
|
||||||
import CoreData
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
|
||||||
enum SearchHistoryItem {
|
enum SearchHistoryItem: Hashable {
|
||||||
case account(objectID: NSManagedObjectID)
|
case hashtag(ManagedObjectRecord<Tag>)
|
||||||
case hashtag(objectID: NSManagedObjectID)
|
case user(ManagedObjectRecord<MastodonUser>)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,28 +13,80 @@ enum SearchHistorySection: Hashable {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SearchHistorySection {
|
extension SearchHistorySection {
|
||||||
static func tableViewDiffableDataSource(
|
|
||||||
for tableView: UITableView,
|
struct Configuration {
|
||||||
dependency: NeedsDependency
|
weak var searchHistorySectionHeaderCollectionReusableViewDelegate: SearchHistorySectionHeaderCollectionReusableViewDelegate?
|
||||||
) -> UITableViewDiffableDataSource<SearchHistorySection, SearchHistoryItem> {
|
}
|
||||||
UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
|
|
||||||
|
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 {
|
switch item {
|
||||||
case .account(let objectID):
|
case .user(let record):
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchResultTableViewCell.self), for: indexPath) as! SearchResultTableViewCell
|
return collectionView.dequeueConfiguredReusableCell(
|
||||||
if let user = try? dependency.context.managedObjectContext.existingObject(with: objectID) as? MastodonUser {
|
using: userCellRegister,
|
||||||
cell.config(with: user)
|
for: indexPath, item: record)
|
||||||
}
|
case .hashtag(let record):
|
||||||
return cell
|
return collectionView.dequeueConfiguredReusableCell(
|
||||||
case .hashtag(let objectID):
|
using: hashtagCellRegister,
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchResultTableViewCell.self), for: indexPath) as! SearchResultTableViewCell
|
for: indexPath, item: record)
|
||||||
if let hashtag = try? dependency.context.managedObjectContext.existingObject(with: objectID) as? Tag {
|
}
|
||||||
cell.config(with: hashtag)
|
}
|
||||||
}
|
|
||||||
return cell
|
let trendHeaderRegister = UICollectionView.SupplementaryRegistration<SearchHistorySectionHeaderCollectionReusableView>(elementKind: UICollectionView.elementKindSectionHeader) { [weak dataSource] supplementaryView, elementKind, indexPath in
|
||||||
case .status:
|
supplementaryView.delegate = configuration.searchHistorySectionHeaderCollectionReusableViewDelegate
|
||||||
// Should not show status in the history list
|
|
||||||
return UITableViewCell()
|
guard let dataSource = dataSource else { return }
|
||||||
} // end switch
|
let sections = dataSource.snapshot().sectionIdentifiers
|
||||||
} // end UITableViewDiffableDataSource
|
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
|
} // 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.
|
// Created by sxiaojian on 2021/4/6.
|
||||||
//
|
//
|
||||||
|
|
||||||
import CoreData
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
|
||||||
enum SearchResultItem {
|
enum SearchResultItem: Hashable {
|
||||||
|
case user(ManagedObjectRecord<MastodonUser>)
|
||||||
|
case status(ManagedObjectRecord<Status>)
|
||||||
case hashtag(tag: Mastodon.Entity.Tag)
|
case hashtag(tag: Mastodon.Entity.Tag)
|
||||||
case account(account: Mastodon.Entity.Account)
|
|
||||||
case status(statusObjectID: NSManagedObjectID, attribute: Item.StatusAttribute)
|
|
||||||
case bottomLoader(attribute: BottomLoaderAttribute)
|
case bottomLoader(attribute: BottomLoaderAttribute)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +27,10 @@ extension SearchResultItem {
|
||||||
self.isNoResult = isEmptyResult
|
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
|
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.
|
// Created by sxiaojian on 2021/4/6.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
import Foundation
|
import Foundation
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
import UIKit
|
import UIKit
|
||||||
import CoreData
|
import CoreData
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
|
import MastodonAsset
|
||||||
|
import MastodonLocalization
|
||||||
|
import MastodonUI
|
||||||
|
|
||||||
enum SearchResultSection: Equatable, Hashable {
|
enum SearchResultSection: Hashable {
|
||||||
case main
|
case main
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SearchResultSection {
|
extension SearchResultSection {
|
||||||
|
|
||||||
|
static let logger = Logger(subsystem: "SearchResultSection", category: "logic")
|
||||||
|
|
||||||
|
struct Configuration {
|
||||||
|
weak var statusViewTableViewCellDelegate: StatusTableViewCellDelegate?
|
||||||
|
weak var userTableViewCellDelegate: UserTableViewCellDelegate?
|
||||||
|
}
|
||||||
|
|
||||||
static func tableViewDiffableDataSource(
|
static func tableViewDiffableDataSource(
|
||||||
for tableView: UITableView,
|
tableView: UITableView,
|
||||||
dependency: NeedsDependency,
|
context: AppContext,
|
||||||
statusTableViewCellDelegate: StatusTableViewCellDelegate
|
configuration: Configuration
|
||||||
) -> UITableViewDiffableDataSource<SearchResultSection, SearchResultItem> {
|
) -> UITableViewDiffableDataSource<SearchResultSection, SearchResultItem> {
|
||||||
UITableViewDiffableDataSource(tableView: tableView) { [
|
tableView.register(UserTableViewCell.self, forCellReuseIdentifier: String(describing: UserTableViewCell.self))
|
||||||
weak statusTableViewCellDelegate
|
tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.self))
|
||||||
] tableView, indexPath, item -> UITableViewCell? in
|
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 {
|
switch item {
|
||||||
case .account(let account):
|
case .user(let record):
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchResultTableViewCell.self), for: indexPath) as! SearchResultTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell
|
||||||
cell.config(with: account)
|
context.managedObjectContext.performAndWait {
|
||||||
return cell
|
guard let user = record.object(in: context.managedObjectContext) else { return }
|
||||||
case .hashtag(let tag):
|
configure(
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchResultTableViewCell.self), for: indexPath) as! SearchResultTableViewCell
|
context: context,
|
||||||
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,
|
|
||||||
tableView: tableView,
|
tableView: tableView,
|
||||||
timelineContext: .search,
|
cell: cell,
|
||||||
dependency: dependency,
|
viewModel: .init(value: .user(user)),
|
||||||
readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
|
configuration: configuration
|
||||||
status: status,
|
|
||||||
requestUserID: requestUserID,
|
|
||||||
statusItemAttribute: attribute
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
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
|
return cell
|
||||||
case .bottomLoader(let attribute):
|
case .bottomLoader(let attribute):
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) as! TimelineBottomLoaderTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) as! TimelineBottomLoaderTableViewCell
|
||||||
|
@ -63,7 +82,49 @@ extension SearchResultSection {
|
||||||
cell.loadMoreLabel.isHidden = true
|
cell.loadMoreLabel.isHidden = true
|
||||||
}
|
}
|
||||||
return cell
|
return cell
|
||||||
} // end switch
|
}
|
||||||
} // end UITableViewDiffableDataSource
|
} // end UITableViewDiffableDataSource
|
||||||
} // end func
|
} // 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 UIKit
|
||||||
import CoreData
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import MastodonAsset
|
||||||
|
import MastodonLocalization
|
||||||
|
|
||||||
enum SettingsItem {
|
enum SettingsItem {
|
||||||
case appearance(settingObjectID: NSManagedObjectID)
|
case appearance(record: ManagedObjectRecord<Setting>)
|
||||||
case notification(settingObjectID: NSManagedObjectID, switchMode: NotificationSwitchMode)
|
case preference(settingRecord: ManagedObjectRecord<Setting>, preferenceType: PreferenceType)
|
||||||
case preference(settingObjectID: NSManagedObjectID, preferenceType: PreferenceType)
|
case notification(settingRecord: ManagedObjectRecord<Setting>, switchMode: NotificationSwitchMode)
|
||||||
case boringZone(item: Link)
|
case boringZone(item: Link)
|
||||||
case spicyZone(item: Link)
|
case spicyZone(item: Link)
|
||||||
}
|
}
|
||||||
|
@ -19,9 +22,10 @@ enum SettingsItem {
|
||||||
extension SettingsItem {
|
extension SettingsItem {
|
||||||
|
|
||||||
enum AppearanceMode: String {
|
enum AppearanceMode: String {
|
||||||
case automatic
|
case system
|
||||||
|
case reallyDark
|
||||||
|
case sortaDark
|
||||||
case light
|
case light
|
||||||
case dark
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum NotificationSwitchMode: CaseIterable, Hashable {
|
enum NotificationSwitchMode: CaseIterable, Hashable {
|
||||||
|
@ -41,14 +45,12 @@ extension SettingsItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PreferenceType: CaseIterable {
|
enum PreferenceType: CaseIterable {
|
||||||
case darkMode
|
|
||||||
case disableAvatarAnimation
|
case disableAvatarAnimation
|
||||||
case disableEmojiAnimation
|
case disableEmojiAnimation
|
||||||
case useDefaultBrowser
|
case useDefaultBrowser
|
||||||
|
|
||||||
var title: String {
|
var title: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .darkMode: return L10n.Scene.Settings.Section.Preference.trueBlackDarkMode
|
|
||||||
case .disableAvatarAnimation: return L10n.Scene.Settings.Section.Preference.disableAvatarAnimation
|
case .disableAvatarAnimation: return L10n.Scene.Settings.Section.Preference.disableAvatarAnimation
|
||||||
case .disableEmojiAnimation: return L10n.Scene.Settings.Section.Preference.disableEmojiAnimation
|
case .disableEmojiAnimation: return L10n.Scene.Settings.Section.Preference.disableEmojiAnimation
|
||||||
case .useDefaultBrowser: return L10n.Scene.Settings.Section.Preference.usingDefaultBrowser
|
case .useDefaultBrowser: return L10n.Scene.Settings.Section.Preference.usingDefaultBrowser
|
||||||
|
@ -75,12 +77,12 @@ extension SettingsItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var textColor: UIColor {
|
var textColor: UIColor? {
|
||||||
switch self {
|
switch self {
|
||||||
case .accountSettings: return Asset.Colors.brandBlue.color
|
case .accountSettings: return nil // tintColor
|
||||||
case .github: return Asset.Colors.brandBlue.color
|
case .github: return nil
|
||||||
case .termsOfService: return Asset.Colors.brandBlue.color
|
case .termsOfService: return nil
|
||||||
case .privacyPolicy: return Asset.Colors.brandBlue.color
|
case .privacyPolicy: return nil
|
||||||
case .clearMediaCache: return .systemRed
|
case .clearMediaCache: return .systemRed
|
||||||
case .signOut: return .systemRed
|
case .signOut: return .systemRed
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,19 +8,21 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import CoreData
|
import CoreData
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
|
import MastodonAsset
|
||||||
|
import MastodonLocalization
|
||||||
|
|
||||||
enum SettingsSection: Hashable {
|
enum SettingsSection: Hashable {
|
||||||
case appearance
|
case appearance
|
||||||
case notifications
|
|
||||||
case preference
|
case preference
|
||||||
|
case notifications
|
||||||
case boringZone
|
case boringZone
|
||||||
case spicyZone
|
case spicyZone
|
||||||
|
|
||||||
var title: String {
|
var title: String {
|
||||||
switch self {
|
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 .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 .boringZone: return L10n.Scene.Settings.Section.BoringZone.title
|
||||||
case .spicyZone: return L10n.Scene.Settings.Section.SpicyZone.title
|
case .spicyZone: return L10n.Scene.Settings.Section.SpicyZone.title
|
||||||
}
|
}
|
||||||
|
@ -39,25 +41,38 @@ extension SettingsSection {
|
||||||
weak settingsToggleCellDelegate
|
weak settingsToggleCellDelegate
|
||||||
] tableView, indexPath, item -> UITableViewCell? in
|
] tableView, indexPath, item -> UITableViewCell? in
|
||||||
switch item {
|
switch item {
|
||||||
case .appearance(let objectID):
|
case .appearance(let record):
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsAppearanceTableViewCell.self), for: indexPath) as! SettingsAppearanceTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsAppearanceTableViewCell.self), for: indexPath) as! SettingsAppearanceTableViewCell
|
||||||
UserDefaults.shared.observe(\.customUserInterfaceStyle, options: [.initial, .new]) { [weak cell] defaults, _ in
|
managedObjectContext.performAndWait {
|
||||||
guard let cell = cell else { return }
|
guard let setting = record.object(in: managedObjectContext) else { return }
|
||||||
switch defaults.customUserInterfaceStyle {
|
cell.configure(setting: setting)
|
||||||
case .unspecified: cell.update(with: .automatic)
|
|
||||||
case .dark: cell.update(with: .dark)
|
|
||||||
case .light: cell.update(with: .light)
|
|
||||||
@unknown default:
|
|
||||||
assertionFailure()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.store(in: &cell.observations)
|
|
||||||
cell.delegate = settingsAppearanceTableViewCellDelegate
|
cell.delegate = settingsAppearanceTableViewCellDelegate
|
||||||
return cell
|
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
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsToggleTableViewCell.self), for: indexPath) as! SettingsToggleTableViewCell
|
||||||
managedObjectContext.performAndWait {
|
managedObjectContext.performAndWait {
|
||||||
let setting = managedObjectContext.object(with: objectID) as! Setting
|
guard let setting = record.object(in: managedObjectContext) else { return }
|
||||||
if let subscription = setting.activeSubscription {
|
if let subscription = setting.activeSubscription {
|
||||||
SettingsSection.configureSettingToggle(cell: cell, switchMode: switchMode, subscription: subscription)
|
SettingsSection.configureSettingToggle(cell: cell, switchMode: switchMode, subscription: subscription)
|
||||||
}
|
}
|
||||||
|
@ -75,32 +90,12 @@ extension SettingsSection {
|
||||||
}
|
}
|
||||||
cell.delegate = settingsToggleCellDelegate
|
cell.delegate = settingsToggleCellDelegate
|
||||||
return cell
|
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),
|
case .boringZone(let item),
|
||||||
.spicyZone(let item):
|
.spicyZone(let item):
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsLinkTableViewCell.self), for: indexPath) as! SettingsLinkTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsLinkTableViewCell.self), for: indexPath) as! SettingsLinkTableViewCell
|
||||||
cell.update(with: item)
|
cell.update(with: item)
|
||||||
return cell
|
return cell
|
||||||
}
|
} // end switch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,8 +112,6 @@ extension SettingsSection {
|
||||||
cell.textLabel?.text = preferenceType.title
|
cell.textLabel?.text = preferenceType.title
|
||||||
|
|
||||||
switch preferenceType {
|
switch preferenceType {
|
||||||
case .darkMode:
|
|
||||||
cell.switchButton.isOn = setting.preferredTrueBlackDarkMode
|
|
||||||
case .disableAvatarAnimation:
|
case .disableAvatarAnimation:
|
||||||
cell.switchButton.isOn = setting.preferredStaticAvatar
|
cell.switchButton.isOn = setting.preferredStaticAvatar
|
||||||
case .disableEmojiAnimation:
|
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 Foundation
|
||||||
import CoreData
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
|
||||||
enum UserItem: Hashable {
|
enum UserItem: Hashable {
|
||||||
case follower(objectID: NSManagedObjectID)
|
case user(record: ManagedObjectRecord<MastodonUser>)
|
||||||
case following(objectID: NSManagedObjectID)
|
|
||||||
case bottomLoader
|
case bottomLoader
|
||||||
case bottomHeader(text: String)
|
case bottomHeader(text: String)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,23 +19,30 @@ enum UserSection: Hashable {
|
||||||
extension UserSection {
|
extension UserSection {
|
||||||
|
|
||||||
static let logger = Logger(subsystem: "StatusSection", category: "logic")
|
static let logger = Logger(subsystem: "StatusSection", category: "logic")
|
||||||
|
|
||||||
|
struct Configuration {
|
||||||
|
weak var userTableViewCellDelegate: UserTableViewCellDelegate?
|
||||||
|
}
|
||||||
|
|
||||||
static func tableViewDiffableDataSource(
|
static func diffableDataSource(
|
||||||
for tableView: UITableView,
|
tableView: UITableView,
|
||||||
dependency: NeedsDependency,
|
context: AppContext,
|
||||||
managedObjectContext: NSManagedObjectContext
|
configuration: Configuration
|
||||||
) -> UITableViewDiffableDataSource<UserSection, UserItem> {
|
) -> UITableViewDiffableDataSource<UserSection, UserItem> {
|
||||||
UITableViewDiffableDataSource(tableView: tableView) { [
|
tableView.register(UserTableViewCell.self, forCellReuseIdentifier: String(describing: UserTableViewCell.self))
|
||||||
weak dependency
|
|
||||||
] tableView, indexPath, item -> UITableViewCell? in
|
return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
|
||||||
guard let dependency = dependency else { return UITableViewCell() }
|
|
||||||
switch item {
|
switch item {
|
||||||
case .follower(let objectID),
|
case .user(let record):
|
||||||
.following(let objectID):
|
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell
|
||||||
managedObjectContext.performAndWait {
|
context.managedObjectContext.performAndWait {
|
||||||
let user = managedObjectContext.object(with: objectID) as! MastodonUser
|
guard let user = record.object(in: context.managedObjectContext) else { return }
|
||||||
configure(cell: cell, user: user)
|
configure(
|
||||||
|
tableView: tableView,
|
||||||
|
cell: cell,
|
||||||
|
viewModel: .init(value: .user(user)),
|
||||||
|
configuration: configuration
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return cell
|
return cell
|
||||||
case .bottomLoader:
|
case .bottomLoader:
|
||||||
|
@ -55,10 +62,17 @@ extension UserSection {
|
||||||
extension UserSection {
|
extension UserSection {
|
||||||
|
|
||||||
static func configure(
|
static func configure(
|
||||||
|
tableView: UITableView,
|
||||||
cell: UserTableViewCell,
|
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 CoreDataStack
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
|
||||||
extension MastodonUser.Property {
|
extension MastodonUser {
|
||||||
init(entity: Mastodon.Entity.Account, domain: String, networkDate: Date) {
|
|
||||||
self.init(
|
public var displayNameWithFallback: String {
|
||||||
id: entity.id,
|
return !displayName.isEmpty ? displayName : username
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
extension MastodonUser: FieldContainer { }
|
|
||||||
|
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 Foundation
|
||||||
import MastodonSDK
|
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 {
|
extension Status {
|
||||||
enum SensitiveType {
|
enum SensitiveType {
|
||||||
case none
|
case none
|
||||||
case all
|
case all
|
||||||
case media(isSensitive: Bool)
|
case media(isSensitive: Bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
var sensitiveType: SensitiveType {
|
var sensitiveType: SensitiveType {
|
||||||
let spoilerText = self.spoilerText ?? ""
|
let spoilerText = self.spoilerText ?? ""
|
||||||
|
|
||||||
// cast .all sensitive when has spoiter text
|
// cast .all sensitive when has spoiter text
|
||||||
if !spoilerText.isEmpty {
|
if !spoilerText.isEmpty {
|
||||||
return .all
|
return .all
|
||||||
}
|
}
|
||||||
|
|
||||||
if let firstAttachment = mediaAttachments?.first {
|
if let firstAttachment = attachments.first {
|
||||||
// cast .media when has non audio media
|
// cast .media when has non audio media
|
||||||
if firstAttachment.type != .audio {
|
if firstAttachment.kind != .audio {
|
||||||
return .media(isSensitive: sensitive)
|
return .media(isSensitive: sensitive)
|
||||||
} else {
|
} else {
|
||||||
return .none
|
return .none
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// not sensitive
|
// not sensitive
|
||||||
return .none
|
return .none
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Status {
|
//extension Status {
|
||||||
var authorForUserProvider: MastodonUser {
|
// var authorForUserProvider: MastodonUser {
|
||||||
let author = (reblog ?? self).author
|
// let author = (reblog ?? self).author
|
||||||
return author
|
// return author
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
extension Status {
|
extension Status {
|
||||||
var statusURL: URL {
|
var statusURL: URL {
|
||||||
if let urlString = self.url,
|
if let urlString = self.url,
|
||||||
|
@ -80,7 +55,7 @@ extension Status {
|
||||||
return URL(string: "https://\(self.domain)/web/statuses/\(self.id)")!
|
return URL(string: "https://\(self.domain)/web/statuses/\(self.id)")!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var activityItems: [Any] {
|
var activityItems: [Any] {
|
||||||
var items: [Any] = []
|
var items: [Any] = []
|
||||||
items.append(self.statusURL)
|
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 {
|
extension Status {
|
||||||
var visibilityEnum: Mastodon.Entity.Status.Visibility? {
|
var asRecord: ManagedObjectRecord<Status> {
|
||||||
return visibility.flatMap { Mastodon.Entity.Status.Visibility(rawValue: $0) }
|
return .init(objectID: self.objectID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import Combine
|
||||||
import Alamofire
|
import Alamofire
|
||||||
import AlamofireImage
|
import AlamofireImage
|
||||||
import FLAnimatedImage
|
import FLAnimatedImage
|
||||||
|
import UIKit
|
||||||
|
|
||||||
private enum FLAnimatedImageViewAssociatedKeys {
|
private enum FLAnimatedImageViewAssociatedKeys {
|
||||||
static var activeAvatarRequestURL = "FLAnimatedImageViewAssociatedKeys.activeAvatarRequestURL"
|
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
|
// cancel task
|
||||||
activeAvatarRequestURL = nil
|
activeAvatarRequestURL = nil
|
||||||
avatarRequestCancellable?.cancel()
|
avatarRequestCancellable?.cancel()
|
||||||
|
@ -64,17 +70,17 @@ extension FLAnimatedImageView {
|
||||||
|
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
if self.activeAvatarRequestURL == url {
|
guard self.activeAvatarRequestURL == url else { return }
|
||||||
if let animatedImage = animatedImage {
|
if let animatedImage = animatedImage {
|
||||||
self.animatedImage = animatedImage
|
self.animatedImage = animatedImage
|
||||||
} else {
|
} else {
|
||||||
self.image = image
|
self.image = image
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
completion?(image)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case .failure:
|
case .failure:
|
||||||
break
|
completion?(nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
import MastodonAsset
|
||||||
|
import MastodonLocalization
|
||||||
|
|
||||||
extension Mastodon.API.Subscriptions.Policy {
|
extension Mastodon.API.Subscriptions.Policy {
|
||||||
var title: String {
|
var title: String {
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
import MastodonAsset
|
||||||
|
import MastodonLocalization
|
||||||
|
|
||||||
extension Mastodon.Entity.Error.Detail: LocalizedError {
|
extension Mastodon.Entity.Error.Detail: LocalizedError {
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import MastodonAsset
|
||||||
|
import MastodonLocalization
|
||||||
|
|
||||||
extension Mastodon.Entity.Notification.NotificationType {
|
extension Mastodon.Entity.Notification.NotificationType {
|
||||||
public var color: UIColor {
|
public var color: UIColor {
|
||||||
|
|
|
@ -16,3 +16,15 @@ extension Mastodon.Entity.Tag: Hashable {
|
||||||
return lhs.name == rhs.name
|
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 UIKit
|
||||||
|
import MastodonAsset
|
||||||
|
import MastodonLocalization
|
||||||
|
|
||||||
extension UITableView {
|
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 Foundation
|
||||||
import MastodonSDK
|
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
|
import MastodonSDK
|
||||||
|
import MastodonUI
|
||||||
|
|
||||||
struct MastodonAuthenticationBox {
|
struct MastodonAuthenticationBox: UserIdentifier {
|
||||||
|
let authenticationRecord: ManagedObjectRecord<MastodonAuthentication>
|
||||||
let domain: String
|
let domain: String
|
||||||
let userID: MastodonUser.ID
|
let userID: MastodonUser.ID
|
||||||
let appAuthorization: Mastodon.API.OAuth.Authorization
|
let appAuthorization: Mastodon.API.OAuth.Authorization
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>90</string>
|
<string>96</string>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>LSApplicationQueriesSchemes</key>
|
<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