Compare commits
No commits in common. "28c1a9092bba4cd6f836857b7b667e07d6910dba" and "141e8b96a5c5bae5eb420c6848ab22ad2d047c14" have entirely different histories.
28c1a9092b
...
141e8b96a5
|
@ -21,8 +21,7 @@ public class InstanceFeatures: ObservableObject {
|
||||||
@Published public private(set) var charsReservedPerURL = 23
|
@Published public private(set) var charsReservedPerURL = 23
|
||||||
@Published public private(set) var maxPollOptionChars: Int?
|
@Published public private(set) var maxPollOptionChars: Int?
|
||||||
@Published public private(set) var maxPollOptionsCount: Int?
|
@Published public private(set) var maxPollOptionsCount: Int?
|
||||||
@Published public private(set) var mediaAttachmentsConfiguration: InstanceV1.MediaAttachmentsConfiguration?
|
@Published public private(set) var mediaAttachmentsConfiguration: Instance.MediaAttachmentsConfiguration?
|
||||||
@Published public private(set) var translation: Bool = false
|
|
||||||
|
|
||||||
public var localOnlyPosts: Bool {
|
public var localOnlyPosts: Bool {
|
||||||
switch instanceType {
|
switch instanceType {
|
||||||
|
@ -241,7 +240,6 @@ public class InstanceFeatures: ObservableObject {
|
||||||
maxPollOptionsCount = pollsConfig.maxOptions
|
maxPollOptionsCount = pollsConfig.maxOptions
|
||||||
}
|
}
|
||||||
mediaAttachmentsConfiguration = instance.configuration?.mediaAttachments
|
mediaAttachmentsConfiguration = instance.configuration?.mediaAttachments
|
||||||
translation = instance.translation
|
|
||||||
|
|
||||||
_featuresUpdated.send()
|
_featuresUpdated.send()
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,39 +9,26 @@ import Foundation
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
public struct InstanceInfo {
|
public struct InstanceInfo {
|
||||||
public var version: String
|
public let version: String
|
||||||
public var maxStatusCharacters: Int?
|
public let maxStatusCharacters: Int?
|
||||||
public var configuration: InstanceV1.Configuration?
|
public let configuration: Instance.Configuration?
|
||||||
public var pollsConfiguration: InstanceV1.PollsConfiguration?
|
public let pollsConfiguration: Instance.PollsConfiguration?
|
||||||
public var translation: Bool
|
|
||||||
|
|
||||||
public init(
|
public init(version: String, maxStatusCharacters: Int?, configuration: Instance.Configuration?, pollsConfiguration: Instance.PollsConfiguration?) {
|
||||||
version: String,
|
|
||||||
maxStatusCharacters: Int?,
|
|
||||||
configuration: InstanceV1.Configuration?,
|
|
||||||
pollsConfiguration: InstanceV1.PollsConfiguration?,
|
|
||||||
translation: Bool
|
|
||||||
) {
|
|
||||||
self.version = version
|
self.version = version
|
||||||
self.maxStatusCharacters = maxStatusCharacters
|
self.maxStatusCharacters = maxStatusCharacters
|
||||||
self.configuration = configuration
|
self.configuration = configuration
|
||||||
self.pollsConfiguration = pollsConfiguration
|
self.pollsConfiguration = pollsConfiguration
|
||||||
self.translation = translation
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension InstanceInfo {
|
extension InstanceInfo {
|
||||||
public init(v1 instance: InstanceV1) {
|
public init(instance: Instance) {
|
||||||
self.init(
|
self.init(
|
||||||
version: instance.version,
|
version: instance.version,
|
||||||
maxStatusCharacters: instance.maxStatusCharacters,
|
maxStatusCharacters: instance.maxStatusCharacters,
|
||||||
configuration: instance.configuration,
|
configuration: instance.configuration,
|
||||||
pollsConfiguration: instance.pollsConfiguration,
|
pollsConfiguration: instance.pollsConfiguration
|
||||||
translation: false
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public mutating func update(v2: InstanceV2) {
|
|
||||||
translation = v2.configuration.translation.enabled
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -231,12 +231,8 @@ public class Client {
|
||||||
return Request<[Relationship]>(method: .get, path: "/api/v1/accounts/relationships", queryParameters: "id" => accounts)
|
return Request<[Relationship]>(method: .get, path: "/api/v1/accounts/relationships", queryParameters: "id" => accounts)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func getInstanceV1() -> Request<InstanceV1> {
|
public static func getInstance() -> Request<Instance> {
|
||||||
return Request<InstanceV1>(method: .get, path: "/api/v1/instance")
|
return Request<Instance>(method: .get, path: "/api/v1/instance")
|
||||||
}
|
|
||||||
|
|
||||||
public static func getInstanceV2() -> Request<InstanceV2> {
|
|
||||||
return Request<InstanceV2>(method: .get, path: "/api/v2/instance")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func getCustomEmoji() -> Request<[Emoji]> {
|
public static func getCustomEmoji() -> Request<[Emoji]> {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// InstanceV1.swift
|
// Instance.swift
|
||||||
// Pachyderm
|
// Pachyderm
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 9/9/18.
|
// Created by Shadowfacts on 9/9/18.
|
||||||
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct InstanceV1: Decodable, Sendable {
|
public struct Instance: Decodable, Sendable {
|
||||||
public let uri: String
|
public let uri: String
|
||||||
public let title: String
|
public let title: String
|
||||||
public let description: String
|
public let description: String
|
||||||
|
@ -92,7 +92,7 @@ public struct InstanceV1: Decodable, Sendable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension InstanceV1 {
|
extension Instance {
|
||||||
public struct Stats: Decodable, Sendable {
|
public struct Stats: Decodable, Sendable {
|
||||||
public let domainCount: Int?
|
public let domainCount: Int?
|
||||||
public let statusCount: Int?
|
public let statusCount: Int?
|
||||||
|
@ -106,7 +106,7 @@ extension InstanceV1 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension InstanceV1 {
|
extension Instance {
|
||||||
public struct Configuration: Codable, Sendable {
|
public struct Configuration: Codable, Sendable {
|
||||||
public let statuses: StatusesConfiguration
|
public let statuses: StatusesConfiguration
|
||||||
public let mediaAttachments: MediaAttachmentsConfiguration
|
public let mediaAttachments: MediaAttachmentsConfiguration
|
||||||
|
@ -121,8 +121,7 @@ extension InstanceV1 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension InstanceV1 {
|
extension Instance {
|
||||||
// note: also used by InstanceV2
|
|
||||||
public struct StatusesConfiguration: Codable, Sendable {
|
public struct StatusesConfiguration: Codable, Sendable {
|
||||||
public let maxCharacters: Int
|
public let maxCharacters: Int
|
||||||
public let maxMediaAttachments: Int
|
public let maxMediaAttachments: Int
|
||||||
|
@ -136,8 +135,7 @@ extension InstanceV1 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension InstanceV1 {
|
extension Instance {
|
||||||
// note: also used by InstanceV2
|
|
||||||
public struct MediaAttachmentsConfiguration: Codable, Sendable {
|
public struct MediaAttachmentsConfiguration: Codable, Sendable {
|
||||||
public let supportedMIMETypes: [String]
|
public let supportedMIMETypes: [String]
|
||||||
public let imageSizeLimit: Int
|
public let imageSizeLimit: Int
|
||||||
|
@ -157,8 +155,7 @@ extension InstanceV1 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension InstanceV1 {
|
extension Instance {
|
||||||
// note: also used by InstanceV2
|
|
||||||
public struct PollsConfiguration: Codable, Sendable {
|
public struct PollsConfiguration: Codable, Sendable {
|
||||||
public let maxOptions: Int
|
public let maxOptions: Int
|
||||||
public let maxCharactersPerOption: Int
|
public let maxCharactersPerOption: Int
|
||||||
|
@ -174,8 +171,7 @@ extension InstanceV1 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension InstanceV1 {
|
extension Instance {
|
||||||
// note: also used by InstanceV2
|
|
||||||
public struct Rule: Decodable, Identifiable, Sendable {
|
public struct Rule: Decodable, Identifiable, Sendable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let text: String
|
public let text: String
|
|
@ -1,125 +0,0 @@
|
||||||
//
|
|
||||||
// InstanceV2.swift
|
|
||||||
// Pachyderm
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 12/4/23.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
public struct InstanceV2: Decodable, Sendable {
|
|
||||||
public let domain: String
|
|
||||||
public let title: String
|
|
||||||
public let version: String
|
|
||||||
public let sourceURL: String
|
|
||||||
public let description: String
|
|
||||||
public let usage: Usage
|
|
||||||
public let thumbnail: Thumbnail
|
|
||||||
public let languages: [String]
|
|
||||||
public let configuration: Configuration
|
|
||||||
public let registrations: Registrations
|
|
||||||
public let contact: Contact
|
|
||||||
public let rules: [InstanceV1.Rule]
|
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
|
||||||
case domain
|
|
||||||
case title
|
|
||||||
case version
|
|
||||||
case sourceURL = "source_url"
|
|
||||||
case description
|
|
||||||
case usage
|
|
||||||
case thumbnail
|
|
||||||
case languages
|
|
||||||
case configuration
|
|
||||||
case registrations
|
|
||||||
case contact
|
|
||||||
case rules
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension InstanceV2 {
|
|
||||||
public struct Usage: Decodable, Sendable {
|
|
||||||
public let users: Users
|
|
||||||
}
|
|
||||||
public struct Users: Decodable, Sendable {
|
|
||||||
public let activeMonth: Int
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
|
||||||
case activeMonth = "active_month"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension InstanceV2 {
|
|
||||||
public struct Thumbnail: Decodable, Sendable {
|
|
||||||
public let url: String
|
|
||||||
public let blurhash: String?
|
|
||||||
public let versions: ThumbnailVersions
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct ThumbnailVersions: Decodable, Sendable {
|
|
||||||
public let oneX: String?
|
|
||||||
public let twoX: String?
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
|
||||||
case oneX = "@1x"
|
|
||||||
case twoX = "@2x"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension InstanceV2 {
|
|
||||||
public struct Configuration: Decodable, Sendable {
|
|
||||||
public let urls: URLs
|
|
||||||
public let accounts: Accounts
|
|
||||||
public let statuses: InstanceV1.StatusesConfiguration
|
|
||||||
public let mediaAttachments: InstanceV1.MediaAttachmentsConfiguration
|
|
||||||
public let polls: InstanceV1.PollsConfiguration
|
|
||||||
public let translation: Translation
|
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
|
||||||
case urls
|
|
||||||
case accounts
|
|
||||||
case statuses
|
|
||||||
case mediaAttachments = "media_attachments"
|
|
||||||
case polls
|
|
||||||
case translation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct URLs: Decodable, Sendable {
|
|
||||||
// the docs incorrectly say the key for this is "streaming_api"
|
|
||||||
public let streaming: String
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct Accounts: Decodable, Sendable {
|
|
||||||
public let maxFeaturedTags: Int
|
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
|
||||||
case maxFeaturedTags = "max_featured_tags"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct Translation: Decodable, Sendable {
|
|
||||||
public let enabled: Bool
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension InstanceV2 {
|
|
||||||
public struct Registrations: Decodable, Sendable {
|
|
||||||
public let enabled: Bool
|
|
||||||
public let approvalRequired: Bool
|
|
||||||
public let message: String?
|
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
|
||||||
case enabled
|
|
||||||
case approvalRequired = "approval_required"
|
|
||||||
case message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension InstanceV2 {
|
|
||||||
public struct Contact: Decodable, Sendable {
|
|
||||||
public let email: String
|
|
||||||
public let account: Account
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -177,10 +177,6 @@ public final class Status: StatusProtocol, Decodable, Sendable {
|
||||||
return Request(method: .get, path: "/api/v1/statuses/\(statusID)/history")
|
return Request(method: .get, path: "/api/v1/statuses/\(statusID)/history")
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func translate(_ statusID: String) -> Request<Translation> {
|
|
||||||
return Request(method: .post, path: "/api/v1/statuses/\(statusID)/translate")
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case id
|
case id
|
||||||
case uri
|
case uri
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
//
|
|
||||||
// Translation.swift
|
|
||||||
// Pachyderm
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 12/4/23.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
public struct Translation: Decodable, Sendable {
|
|
||||||
public let content: String
|
|
||||||
public let spoilerText: String?
|
|
||||||
public let detectedSourceLanguage: String
|
|
||||||
public let provider: String
|
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
|
||||||
case content
|
|
||||||
case spoilerText
|
|
||||||
case detectedSourceLanguage = "detected_source_language"
|
|
||||||
case provider
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -28,7 +28,7 @@ class ShareMastodonContext: ComposeMastodonContext, ObservableObject {
|
||||||
self.instanceFeatures = InstanceFeatures()
|
self.instanceFeatures = InstanceFeatures()
|
||||||
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
async let instance = try? await run(Client.getInstanceV1()).0
|
async let instance = try? await run(Client.getInstance()).0
|
||||||
async let nodeInfo: NodeInfo? = await withCheckedContinuation({ continuation in
|
async let nodeInfo: NodeInfo? = await withCheckedContinuation({ continuation in
|
||||||
self.client.nodeInfo { response in
|
self.client.nodeInfo { response in
|
||||||
switch response {
|
switch response {
|
||||||
|
@ -40,7 +40,7 @@ class ShareMastodonContext: ComposeMastodonContext, ObservableObject {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
guard let instance = await instance else { return }
|
guard let instance = await instance else { return }
|
||||||
self.instanceFeatures.update(instance: InstanceInfo(v1: instance), nodeInfo: await nodeInfo)
|
self.instanceFeatures.update(instance: InstanceInfo(instance: instance), nodeInfo: await nodeInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
|
|
|
@ -53,8 +53,7 @@ class MastodonController: ObservableObject {
|
||||||
let instanceFeatures = InstanceFeatures()
|
let instanceFeatures = InstanceFeatures()
|
||||||
|
|
||||||
@Published private(set) var account: AccountMO?
|
@Published private(set) var account: AccountMO?
|
||||||
@Published private(set) var instance: InstanceV1?
|
@Published private(set) var instance: Instance?
|
||||||
@Published private var instanceV2: InstanceV2?
|
|
||||||
@Published private(set) var instanceInfo: InstanceInfo!
|
@Published private(set) var instanceInfo: InstanceInfo!
|
||||||
@Published private(set) var nodeInfo: NodeInfo!
|
@Published private(set) var nodeInfo: NodeInfo!
|
||||||
@Published private(set) var lists: [List] = []
|
@Published private(set) var lists: [List] = []
|
||||||
|
@ -64,7 +63,7 @@ class MastodonController: ObservableObject {
|
||||||
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
private var pendingOwnInstanceRequestCallbacks = [(Result<InstanceV1, Client.Error>) -> Void]()
|
private var pendingOwnInstanceRequestCallbacks = [(Result<Instance, Client.Error>) -> Void]()
|
||||||
private var ownInstanceRequest: URLSessionTask?
|
private var ownInstanceRequest: URLSessionTask?
|
||||||
|
|
||||||
var loggedIn: Bool {
|
var loggedIn: Bool {
|
||||||
|
@ -108,14 +107,9 @@ class MastodonController: ObservableObject {
|
||||||
|
|
||||||
$instance
|
$instance
|
||||||
.compactMap { $0 }
|
.compactMap { $0 }
|
||||||
.combineLatest($instanceV2)
|
.sink { [unowned self] in
|
||||||
.sink {[unowned self] (instance, v2) in
|
self.updateActiveInstance(from: $0)
|
||||||
var info = InstanceInfo(v1: instance)
|
self.instanceInfo = InstanceInfo(instance: $0)
|
||||||
if let v2 {
|
|
||||||
info.update(v2: v2)
|
|
||||||
}
|
|
||||||
self.instanceInfo = info
|
|
||||||
self.updateActiveInstance(from: info)
|
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
@ -223,10 +217,6 @@ class MastodonController: ObservableObject {
|
||||||
|
|
||||||
_ = try await (ownAccount, ownInstance)
|
_ = try await (ownAccount, ownInstance)
|
||||||
|
|
||||||
if instanceFeatures.hasMastodonVersion(4, 0, 0) {
|
|
||||||
async let _ = try? getOwnInstanceV2()
|
|
||||||
}
|
|
||||||
|
|
||||||
loadLists()
|
loadLists()
|
||||||
_ = await loadFilters()
|
_ = await loadFilters()
|
||||||
await loadServerPreferences()
|
await loadServerPreferences()
|
||||||
|
@ -287,7 +277,7 @@ class MastodonController: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getOwnInstance(completion: ((InstanceV1) -> Void)? = nil) {
|
func getOwnInstance(completion: ((Instance) -> Void)? = nil) {
|
||||||
getOwnInstanceInternal(retryAttempt: 0) {
|
getOwnInstanceInternal(retryAttempt: 0) {
|
||||||
if case let .success(instance) = $0 {
|
if case let .success(instance) = $0 {
|
||||||
completion?(instance)
|
completion?(instance)
|
||||||
|
@ -296,7 +286,7 @@ class MastodonController: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func getOwnInstance() async throws -> InstanceV1 {
|
func getOwnInstance() async throws -> Instance {
|
||||||
return try await withCheckedThrowingContinuation({ continuation in
|
return try await withCheckedThrowingContinuation({ continuation in
|
||||||
getOwnInstanceInternal(retryAttempt: 0) { result in
|
getOwnInstanceInternal(retryAttempt: 0) { result in
|
||||||
continuation.resume(with: result)
|
continuation.resume(with: result)
|
||||||
|
@ -304,7 +294,7 @@ class MastodonController: ObservableObject {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getOwnInstanceInternal(retryAttempt: Int, completion: ((Result<InstanceV1, Client.Error>) -> Void)?) {
|
private func getOwnInstanceInternal(retryAttempt: Int, completion: ((Result<Instance, Client.Error>) -> Void)?) {
|
||||||
// this is main thread only to prevent concurrent access to ownInstanceRequest and pendingOwnInstanceRequestCallbacks
|
// this is main thread only to prevent concurrent access to ownInstanceRequest and pendingOwnInstanceRequestCallbacks
|
||||||
assert(Thread.isMainThread)
|
assert(Thread.isMainThread)
|
||||||
|
|
||||||
|
@ -316,7 +306,7 @@ class MastodonController: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ownInstanceRequest == nil {
|
if ownInstanceRequest == nil {
|
||||||
let request = Client.getInstanceV1()
|
let request = Client.getInstance()
|
||||||
ownInstanceRequest = run(request) { (response) in
|
ownInstanceRequest = run(request) { (response) in
|
||||||
switch response {
|
switch response {
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
|
@ -371,10 +361,6 @@ class MastodonController: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getOwnInstanceV2() async throws {
|
|
||||||
self.instanceV2 = try await client.run(Client.getInstanceV2()).0
|
|
||||||
}
|
|
||||||
|
|
||||||
// MainActor because the accountPreferences instance is bound to the view context
|
// MainActor because the accountPreferences instance is bound to the view context
|
||||||
@MainActor
|
@MainActor
|
||||||
private func loadServerPreferences() async {
|
private func loadServerPreferences() async {
|
||||||
|
@ -390,13 +376,13 @@ class MastodonController: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateActiveInstance(from info: InstanceInfo) {
|
private func updateActiveInstance(from instance: Instance) {
|
||||||
persistentContainer.performBackgroundTask { context in
|
persistentContainer.performBackgroundTask { context in
|
||||||
if let existing = try? context.fetch(ActiveInstance.fetchRequest()).first {
|
if let existing = try? context.fetch(ActiveInstance.fetchRequest()).first {
|
||||||
existing.update(from: info)
|
existing.update(from: instance)
|
||||||
} else {
|
} else {
|
||||||
let new = ActiveInstance(context: context)
|
let new = ActiveInstance(context: context)
|
||||||
new.update(from: info)
|
new.update(from: instance)
|
||||||
}
|
}
|
||||||
if context.hasChanges {
|
if context.hasChanges {
|
||||||
try? context.save()
|
try? context.save()
|
||||||
|
|
|
@ -22,20 +22,18 @@ public final class ActiveInstance: NSManagedObject {
|
||||||
@NSManaged public var maxStatusCharacters: Int
|
@NSManaged public var maxStatusCharacters: Int
|
||||||
@NSManaged private var configurationData: Data?
|
@NSManaged private var configurationData: Data?
|
||||||
@NSManaged private var pollsConfigurationData: Data?
|
@NSManaged private var pollsConfigurationData: Data?
|
||||||
@NSManaged public var translation: Bool
|
|
||||||
|
|
||||||
@LazilyDecoding(from: \ActiveInstance.configurationData, fallback: nil)
|
@LazilyDecoding(from: \ActiveInstance.configurationData, fallback: nil)
|
||||||
public var configuration: InstanceV1.Configuration?
|
public var configuration: Instance.Configuration?
|
||||||
|
|
||||||
@LazilyDecoding(from: \ActiveInstance.pollsConfigurationData, fallback: nil)
|
@LazilyDecoding(from: \ActiveInstance.pollsConfigurationData, fallback: nil)
|
||||||
public var pollsConfiguration: InstanceV1.PollsConfiguration?
|
public var pollsConfiguration: Instance.PollsConfiguration?
|
||||||
|
|
||||||
func update(from info: InstanceInfo) {
|
func update(from instance: Instance) {
|
||||||
self.version = info.version
|
self.version = instance.version
|
||||||
self.maxStatusCharacters = info.maxStatusCharacters ?? 500
|
self.maxStatusCharacters = instance.maxStatusCharacters ?? 500
|
||||||
self.configuration = info.configuration
|
self.configuration = instance.configuration
|
||||||
self.pollsConfiguration = info.pollsConfiguration
|
self.pollsConfiguration = instance.pollsConfiguration
|
||||||
self.translation = info.translation
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,8 +43,7 @@ extension InstanceInfo {
|
||||||
version: activeInstance.version,
|
version: activeInstance.version,
|
||||||
maxStatusCharacters: activeInstance.maxStatusCharacters,
|
maxStatusCharacters: activeInstance.maxStatusCharacters,
|
||||||
configuration: activeInstance.configuration,
|
configuration: activeInstance.configuration,
|
||||||
pollsConfiguration: activeInstance.pollsConfiguration,
|
pollsConfiguration: activeInstance.pollsConfiguration
|
||||||
translation: activeInstance.translation
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,7 +54,6 @@ public final class StatusMO: NSManagedObject, StatusProtocol {
|
||||||
@NSManaged public var reblog: StatusMO?
|
@NSManaged public var reblog: StatusMO?
|
||||||
@NSManaged public var localOnly: Bool
|
@NSManaged public var localOnly: Bool
|
||||||
@NSManaged public var lastFetchedAt: Date?
|
@NSManaged public var lastFetchedAt: Date?
|
||||||
@NSManaged public var language: String?
|
|
||||||
|
|
||||||
@LazilyDecoding(arrayFrom: \StatusMO.attachmentsData)
|
@LazilyDecoding(arrayFrom: \StatusMO.attachmentsData)
|
||||||
public var attachments: [Attachment]
|
public var attachments: [Attachment]
|
||||||
|
@ -140,7 +139,6 @@ extension StatusMO {
|
||||||
self.visibility = status.visibility
|
self.visibility = status.visibility
|
||||||
self.poll = status.poll
|
self.poll = status.poll
|
||||||
self.localOnly = status.localOnly ?? false
|
self.localOnly = status.localOnly ?? false
|
||||||
self.language = status.language
|
|
||||||
|
|
||||||
if let existing = container.account(for: status.account.id, in: context) {
|
if let existing = container.account(for: status.account.id, in: context) {
|
||||||
existing.updateFrom(apiAccount: status.account, container: container)
|
existing.updateFrom(apiAccount: status.account, container: container)
|
||||||
|
|
|
@ -41,7 +41,6 @@
|
||||||
<attribute name="configurationData" optional="YES" attributeType="Binary"/>
|
<attribute name="configurationData" optional="YES" attributeType="Binary"/>
|
||||||
<attribute name="maxStatusCharacters" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
<attribute name="maxStatusCharacters" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
<attribute name="pollsConfigurationData" optional="YES" attributeType="Binary"/>
|
<attribute name="pollsConfigurationData" optional="YES" attributeType="Binary"/>
|
||||||
<attribute name="translation" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
|
||||||
<attribute name="version" optional="YES" attributeType="String"/>
|
<attribute name="version" optional="YES" attributeType="String"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="Filter" representedClassName="FilterMO" syncable="YES">
|
<entity name="Filter" representedClassName="FilterMO" syncable="YES">
|
||||||
|
@ -110,7 +109,6 @@
|
||||||
<attribute name="id" attributeType="String"/>
|
<attribute name="id" attributeType="String"/>
|
||||||
<attribute name="inReplyToAccountID" optional="YES" attributeType="String"/>
|
<attribute name="inReplyToAccountID" optional="YES" attributeType="String"/>
|
||||||
<attribute name="inReplyToID" optional="YES" attributeType="String"/>
|
<attribute name="inReplyToID" optional="YES" attributeType="String"/>
|
||||||
<attribute name="language" optional="YES" attributeType="String"/>
|
|
||||||
<attribute name="lastFetchedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="lastFetchedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
<attribute name="localOnly" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
<attribute name="localOnly" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
<attribute name="mentionsData" attributeType="Binary"/>
|
<attribute name="mentionsData" attributeType="Binary"/>
|
||||||
|
|
|
@ -15,7 +15,6 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
private let mastodonController: MastodonController
|
private let mastodonController: MastodonController
|
||||||
private let mainStatusID: String
|
private let mainStatusID: String
|
||||||
private let mainStatusState: CollapseState
|
private let mainStatusState: CollapseState
|
||||||
private var mainStatusTranslation: Translation?
|
|
||||||
var statusIDToScrollToOnLoad: String
|
var statusIDToScrollToOnLoad: String
|
||||||
var showStatusesAutomatically = false
|
var showStatusesAutomatically = false
|
||||||
|
|
||||||
|
@ -89,14 +88,11 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
cell.updateUI(statusID: item.0, state: item.1, filterResult: .allow, precomputedContent: nil)
|
cell.updateUI(statusID: item.0, state: item.1, filterResult: .allow, precomputedContent: nil)
|
||||||
cell.setShowThreadLinks(prev: item.2, next: item.3)
|
cell.setShowThreadLinks(prev: item.2, next: item.3)
|
||||||
}
|
}
|
||||||
let mainStatusCell = UICollectionView.CellRegistration<ConversationMainStatusCollectionViewCell, (String, CollapseState, Translation?, Bool)> { [unowned self] cell, indexPath, item in
|
let mainStatusCell = UICollectionView.CellRegistration<ConversationMainStatusCollectionViewCell, (String, CollapseState, Bool)> { [unowned self] cell, indexPath, item in
|
||||||
cell.delegate = self
|
cell.delegate = self
|
||||||
cell.translateStatus = { [unowned self] in
|
|
||||||
self.translateMainStatus()
|
|
||||||
}
|
|
||||||
cell.showStatusAutomatically = self.showStatusesAutomatically
|
cell.showStatusAutomatically = self.showStatusesAutomatically
|
||||||
cell.updateUI(statusID: item.0, state: item.1, translation: item.2)
|
cell.updateUI(statusID: item.0, state: item.1)
|
||||||
cell.setShowThreadLinks(prev: item.3, next: false)
|
cell.setShowThreadLinks(prev: item.2, next: false)
|
||||||
}
|
}
|
||||||
let expandThreadCell = UICollectionView.CellRegistration<ExpandThreadCollectionViewCell, ([ConversationNode], Bool)> { cell, indexPath, item in
|
let expandThreadCell = UICollectionView.CellRegistration<ExpandThreadCollectionViewCell, ([ConversationNode], Bool)> { cell, indexPath, item in
|
||||||
cell.updateUI(childThreads: item.0, inline: item.1)
|
cell.updateUI(childThreads: item.0, inline: item.1)
|
||||||
|
@ -108,7 +104,7 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
switch itemIdentifier {
|
switch itemIdentifier {
|
||||||
case let .status(id: id, node: _, state: state, prevLink: prevLink, nextLink: nextLink):
|
case let .status(id: id, node: _, state: state, prevLink: prevLink, nextLink: nextLink):
|
||||||
if id == self.mainStatusID {
|
if id == self.mainStatusID {
|
||||||
return collectionView.dequeueConfiguredReusableCell(using: mainStatusCell, for: indexPath, item: (id, state, self.mainStatusTranslation, prevLink))
|
return collectionView.dequeueConfiguredReusableCell(using: mainStatusCell, for: indexPath, item: (id, state, prevLink))
|
||||||
} else {
|
} else {
|
||||||
return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (id, state, prevLink, nextLink))
|
return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (id, state, prevLink, nextLink))
|
||||||
}
|
}
|
||||||
|
@ -264,30 +260,6 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func translateMainStatus() {
|
|
||||||
Task { @MainActor in
|
|
||||||
let translation: Translation
|
|
||||||
do {
|
|
||||||
translation = try await mastodonController.run(Status.translate(mainStatusID)).0
|
|
||||||
} catch {
|
|
||||||
let config = ToastConfiguration(from: error, with: "Error Translating", in: self) { toast in
|
|
||||||
toast.dismissToast(animated: true)
|
|
||||||
self.translateMainStatus()
|
|
||||||
}
|
|
||||||
self.showToast(configuration: config, animated: true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mainStatusTranslation = translation
|
|
||||||
|
|
||||||
var snapshot = dataSource.snapshot()
|
|
||||||
snapshot.reconfigureItems(snapshot.itemIdentifiers(inSection: .mainStatus))
|
|
||||||
await MainActor.run {
|
|
||||||
dataSource.apply(snapshot, animatingDifferences: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ConversationCollectionViewController {
|
extension ConversationCollectionViewController {
|
||||||
|
|
|
@ -162,7 +162,7 @@ class InstanceSelectorTableViewController: UITableViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
let client = Client(baseURL: url, session: .appDefault)
|
let client = Client(baseURL: url, session: .appDefault)
|
||||||
let request = Client.getInstanceV1()
|
let request = Client.getInstance()
|
||||||
client.run(request) { (response) in
|
client.run(request) { (response) in
|
||||||
var snapshot = self.dataSource.snapshot()
|
var snapshot = self.dataSource.snapshot()
|
||||||
if snapshot.indexOfSection(.selected) != nil {
|
if snapshot.indexOfSection(.selected) != nil {
|
||||||
|
@ -309,7 +309,7 @@ extension InstanceSelectorTableViewController {
|
||||||
case recommendedInstances
|
case recommendedInstances
|
||||||
}
|
}
|
||||||
enum Item: Equatable, Hashable {
|
enum Item: Equatable, Hashable {
|
||||||
case selected(URL, InstanceV1)
|
case selected(URL, Instance)
|
||||||
case recommended(InstanceSelector.Instance)
|
case recommended(InstanceSelector.Instance)
|
||||||
|
|
||||||
static func ==(lhs: Item, rhs: Item) -> Bool {
|
static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||||
|
|
|
@ -61,34 +61,13 @@ class StatusEditCollectionViewCell: UICollectionViewListCell {
|
||||||
$0.addTarget(self, action: #selector(collapseButtonPressed), for: .touchUpInside)
|
$0.addTarget(self, action: #selector(collapseButtonPressed), for: .touchUpInside)
|
||||||
}
|
}
|
||||||
|
|
||||||
private lazy var contentContainer = StatusContentContainer(arrangedSubviews: [
|
private let contentContainer = StatusContentContainer<StatusEditContentTextView, StatusEditPollView>(useTopSpacer: false).configure {
|
||||||
contentTextView,
|
$0.contentTextView.defaultFont = TimelineStatusCollectionViewCell.contentFont
|
||||||
cardView,
|
$0.contentTextView.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont
|
||||||
attachmentsView,
|
$0.contentTextView.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle
|
||||||
pollView,
|
|
||||||
] as! [any StatusContentView], useTopSpacer: false).configure {
|
|
||||||
$0.setContentHuggingPriority(.defaultLow, for: .vertical)
|
$0.setContentHuggingPriority(.defaultLow, for: .vertical)
|
||||||
}
|
}
|
||||||
|
|
||||||
private let contentTextView = StatusEditContentTextView().configure {
|
|
||||||
$0.adjustsFontForContentSizeCategory = true
|
|
||||||
$0.isScrollEnabled = false
|
|
||||||
$0.backgroundColor = nil
|
|
||||||
$0.isEditable = false
|
|
||||||
$0.isSelectable = false
|
|
||||||
$0.defaultFont = TimelineStatusCollectionViewCell.contentFont
|
|
||||||
$0.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont
|
|
||||||
$0.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle
|
|
||||||
}
|
|
||||||
|
|
||||||
private let cardView = StatusCardView().configure {
|
|
||||||
$0.heightAnchor.constraint(equalToConstant: StatusContentContainer.cardViewHeight).isActive = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private let attachmentsView = AttachmentsContainerView()
|
|
||||||
|
|
||||||
private let pollView = StatusEditPollView()
|
|
||||||
|
|
||||||
weak var delegate: StatusEditCollectionViewCellDelegate?
|
weak var delegate: StatusEditCollectionViewCellDelegate?
|
||||||
private var mastodonController: MastodonController! { delegate?.apiController }
|
private var mastodonController: MastodonController! { delegate?.apiController }
|
||||||
|
|
||||||
|
@ -129,7 +108,7 @@ class StatusEditCollectionViewCell: UICollectionViewListCell {
|
||||||
}
|
}
|
||||||
str += "collapsed"
|
str += "collapsed"
|
||||||
} else {
|
} else {
|
||||||
str += AttributedString(contentTextView.attributedText)
|
str += AttributedString(contentContainer.contentTextView.attributedText)
|
||||||
|
|
||||||
if edit.attachments.count > 0 {
|
if edit.attachments.count > 0 {
|
||||||
let includeDescriptions: Bool
|
let includeDescriptions: Bool
|
||||||
|
@ -191,13 +170,13 @@ class StatusEditCollectionViewCell: UICollectionViewListCell {
|
||||||
|
|
||||||
timestampLabel.text = ConversationMainStatusCollectionViewCell.dateFormatter.string(from: edit.createdAt)
|
timestampLabel.text = ConversationMainStatusCollectionViewCell.dateFormatter.string(from: edit.createdAt)
|
||||||
|
|
||||||
contentTextView.setTextFrom(edit: edit, index: index)
|
contentContainer.contentTextView.setTextFrom(edit: edit, index: index)
|
||||||
contentTextView.navigationDelegate = delegate
|
contentContainer.contentTextView.navigationDelegate = delegate
|
||||||
attachmentsView.delegate = self
|
contentContainer.attachmentsView.delegate = self
|
||||||
attachmentsView.updateUI(attachments: edit.attachments)
|
contentContainer.attachmentsView.updateUI(attachments: edit.attachments)
|
||||||
pollView.isHidden = edit.poll == nil
|
contentContainer.pollView.isHidden = edit.poll == nil
|
||||||
pollView.updateUI(poll: edit.poll, emojis: edit.emojis)
|
contentContainer.pollView.updateUI(poll: edit.poll, emojis: edit.emojis)
|
||||||
cardView.isHidden = true
|
contentContainer.cardView.isHidden = true
|
||||||
|
|
||||||
contentWarningLabel.text = edit.spoilerText
|
contentWarningLabel.text = edit.spoilerText
|
||||||
contentWarningLabel.isHidden = edit.spoilerText.isEmpty
|
contentWarningLabel.isHidden = edit.spoilerText.isEmpty
|
||||||
|
@ -240,9 +219,9 @@ extension StatusEditCollectionViewCell: AttachmentViewDelegate {
|
||||||
guard let delegate else {
|
guard let delegate else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
let attachments = attachmentsView.attachments!
|
let attachments = contentContainer.attachmentsView.attachments!
|
||||||
let sourceViews = attachments.map {
|
let sourceViews = attachments.map {
|
||||||
attachmentsView.getAttachmentView(for: $0)
|
contentContainer.attachmentsView.getAttachmentView(for: $0)
|
||||||
}
|
}
|
||||||
let gallery = delegate.gallery(attachments: attachments, sourceViews: sourceViews, startIndex: index)
|
let gallery = delegate.gallery(attachments: attachments, sourceViews: sourceViews, startIndex: index)
|
||||||
return gallery
|
return gallery
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
class StatusEditPollView: UIStackView, StatusContentView {
|
class StatusEditPollView: UIStackView, StatusContentPollView {
|
||||||
|
|
||||||
private var titleLabels: [EmojiLabel] = []
|
private var titleLabels: [EmojiLabel] = []
|
||||||
|
|
||||||
|
|
|
@ -80,7 +80,6 @@ class AttachmentsContainerView: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
guard self.attachmentTokens != newTokens else {
|
guard self.attachmentTokens != newTokens else {
|
||||||
self.isHidden = attachments.isEmpty
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
|
||||||
weak var overrideMastodonController: MastodonController?
|
weak var overrideMastodonController: MastodonController?
|
||||||
var mastodonController: MastodonController? { overrideMastodonController ?? navigationDelegate?.apiController }
|
var mastodonController: MastodonController? { overrideMastodonController ?? navigationDelegate?.apiController }
|
||||||
|
|
||||||
private(set) var htmlConverter = HTMLConverter()
|
private var htmlConverter = HTMLConverter()
|
||||||
var defaultFont: UIFont {
|
var defaultFont: UIFont {
|
||||||
_read { yield htmlConverter.font }
|
_read { yield htmlConverter.font }
|
||||||
_modify { yield &htmlConverter.font }
|
_modify { yield &htmlConverter.font }
|
||||||
|
|
|
@ -16,7 +16,7 @@ class InstanceTableViewCell: UITableViewCell {
|
||||||
@IBOutlet weak var adultLabel: UILabel!
|
@IBOutlet weak var adultLabel: UILabel!
|
||||||
@IBOutlet weak var descriptionTextView: ContentTextView!
|
@IBOutlet weak var descriptionTextView: ContentTextView!
|
||||||
|
|
||||||
var instance: InstanceV1?
|
var instance: Instance?
|
||||||
var selectorInstance: InstanceSelector.Instance?
|
var selectorInstance: InstanceSelector.Instance?
|
||||||
|
|
||||||
var thumbnailURL: URL?
|
var thumbnailURL: URL?
|
||||||
|
@ -53,7 +53,7 @@ class InstanceTableViewCell: UITableViewCell {
|
||||||
updateThumbnail(url: instance.proxiedThumbnailURL)
|
updateThumbnail(url: instance.proxiedThumbnailURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUI(instance: InstanceV1) {
|
func updateUI(instance: Instance) {
|
||||||
self.instance = instance
|
self.instance = instance
|
||||||
self.selectorInstance = nil
|
self.selectorInstance = nil
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
class StatusPollView: UIView, StatusContentView {
|
class StatusPollView: UIView, StatusContentPollView {
|
||||||
|
|
||||||
private static let formatter: DateComponentsFormatter = {
|
private static let formatter: DateComponentsFormatter = {
|
||||||
let f = DateComponentsFormatter()
|
let f = DateComponentsFormatter()
|
||||||
|
|
|
@ -117,40 +117,18 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
||||||
$0.addTarget(self, action: #selector(collapseButtonPressed), for: .touchUpInside)
|
$0.addTarget(self, action: #selector(collapseButtonPressed), for: .touchUpInside)
|
||||||
}
|
}
|
||||||
|
|
||||||
private(set) lazy var contentContainer = StatusContentContainer(arrangedSubviews: [
|
let contentContainer = StatusContentContainer<StatusContentTextView, StatusPollView>(useTopSpacer: true).configure {
|
||||||
contentTextView,
|
$0.contentTextView.defaultFont = ConversationMainStatusCollectionViewCell.contentFont
|
||||||
cardView,
|
$0.contentTextView.monospaceFont = ConversationMainStatusCollectionViewCell.monospaceFont
|
||||||
attachmentsView,
|
$0.contentTextView.paragraphStyle = ConversationMainStatusCollectionViewCell.contentParagraphStyle
|
||||||
pollView,
|
$0.contentTextView.isSelectable = true
|
||||||
] as! [any StatusContentView], useTopSpacer: true).configure {
|
$0.contentTextView.dataDetectorTypes = [.flightNumber, .address, .shipmentTrackingNumber, .phoneNumber]
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
$0.contentTextView.dataDetectorTypes.formUnion([.money, .physicalValue])
|
||||||
|
}
|
||||||
$0.setContentHuggingPriority(.defaultLow, for: .vertical)
|
$0.setContentHuggingPriority(.defaultLow, for: .vertical)
|
||||||
}
|
}
|
||||||
|
|
||||||
let contentTextView = StatusContentTextView().configure {
|
|
||||||
$0.adjustsFontForContentSizeCategory = true
|
|
||||||
$0.isScrollEnabled = false
|
|
||||||
$0.backgroundColor = nil
|
|
||||||
$0.isEditable = false
|
|
||||||
$0.isSelectable = true
|
|
||||||
$0.defaultFont = ConversationMainStatusCollectionViewCell.contentFont
|
|
||||||
$0.monospaceFont = ConversationMainStatusCollectionViewCell.monospaceFont
|
|
||||||
$0.paragraphStyle = ConversationMainStatusCollectionViewCell.contentParagraphStyle
|
|
||||||
$0.dataDetectorTypes = [.flightNumber, .address, .shipmentTrackingNumber, .phoneNumber]
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
$0.dataDetectorTypes.formUnion([.money, .physicalValue])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var translateButton: TranslateButton?
|
|
||||||
|
|
||||||
let cardView = StatusCardView().configure {
|
|
||||||
$0.heightAnchor.constraint(equalToConstant: StatusContentContainer.cardViewHeight).isActive = true
|
|
||||||
}
|
|
||||||
|
|
||||||
let attachmentsView = AttachmentsContainerView()
|
|
||||||
|
|
||||||
let pollView = StatusPollView()
|
|
||||||
|
|
||||||
private lazy var favoritesCountButton = UIButton().configure {
|
private lazy var favoritesCountButton = UIButton().configure {
|
||||||
$0.titleLabel!.adjustsFontForContentSizeCategory = true
|
$0.titleLabel!.adjustsFontForContentSizeCategory = true
|
||||||
$0.addTarget(self, action: #selector(favoritesCountPressed), for: .touchUpInside)
|
$0.addTarget(self, action: #selector(favoritesCountPressed), for: .touchUpInside)
|
||||||
|
@ -311,7 +289,6 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
||||||
var mastodonController: MastodonController! { delegate?.apiController }
|
var mastodonController: MastodonController! { delegate?.apiController }
|
||||||
weak var delegate: StatusCollectionViewCellDelegate?
|
weak var delegate: StatusCollectionViewCellDelegate?
|
||||||
var showStatusAutomatically = false
|
var showStatusAutomatically = false
|
||||||
var translateStatus: (() -> Void)?
|
|
||||||
|
|
||||||
var statusID: String!
|
var statusID: String!
|
||||||
var statusState: CollapseState!
|
var statusState: CollapseState!
|
||||||
|
@ -341,9 +318,9 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
||||||
accountDetailAccessibilityElement,
|
accountDetailAccessibilityElement,
|
||||||
contentWarningLabel,
|
contentWarningLabel,
|
||||||
collapseButton,
|
collapseButton,
|
||||||
contentTextView,
|
contentContainer.contentTextView,
|
||||||
attachmentsView,
|
contentContainer.attachmentsView,
|
||||||
pollView,
|
contentContainer.pollView,
|
||||||
favoritesCountButton,
|
favoritesCountButton,
|
||||||
reblogsCountButton,
|
reblogsCountButton,
|
||||||
timestampAndClientLabel,
|
timestampAndClientLabel,
|
||||||
|
@ -371,7 +348,7 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
||||||
|
|
||||||
// MARK: Configure UI
|
// MARK: Configure UI
|
||||||
|
|
||||||
func updateUI(statusID: String, state: CollapseState, translation: Translation?) {
|
func updateUI(statusID: String, state: CollapseState) {
|
||||||
guard let status = mastodonController.persistentContainer.status(for: statusID) else {
|
guard let status = mastodonController.persistentContainer.status(for: statusID) else {
|
||||||
fatalError()
|
fatalError()
|
||||||
}
|
}
|
||||||
|
@ -381,17 +358,7 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
||||||
self.statusID = statusID
|
self.statusID = statusID
|
||||||
self.statusState = state
|
self.statusState = state
|
||||||
|
|
||||||
let attributedTranslatedContent: NSAttributedString? = translation.map {
|
doUpdateUI(status: status)
|
||||||
contentTextView.htmlConverter.convert($0.content)
|
|
||||||
}
|
|
||||||
doUpdateUI(status: status, precomputedContent: attributedTranslatedContent)
|
|
||||||
|
|
||||||
if !status.spoilerText.isEmpty,
|
|
||||||
let translated = translation?.spoilerText {
|
|
||||||
contentWarningLabel.text = translated
|
|
||||||
contentWarningLabel.setEmojis(status.emojis, identifier: "\(statusID)_translated")
|
|
||||||
}
|
|
||||||
|
|
||||||
accountDetailToContentWarningSpacer.isHidden = collapseButton.isHidden
|
accountDetailToContentWarningSpacer.isHidden = collapseButton.isHidden
|
||||||
contentWarningToCollapseButtonSpacer.isHidden = collapseButton.isHidden
|
contentWarningToCollapseButtonSpacer.isHidden = collapseButton.isHidden
|
||||||
|
|
||||||
|
@ -416,31 +383,6 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
||||||
} else {
|
} else {
|
||||||
editTimestampButton.isHidden = true
|
editTimestampButton.isHidden = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if mastodonController.instanceFeatures.translation,
|
|
||||||
let preferredLanguage = mastodonController.accountPreferences.serverDefaultLanguage,
|
|
||||||
preferredLanguage != status.language {
|
|
||||||
var config = UIButton.Configuration.tinted()
|
|
||||||
config.image = UIImage(systemName: "globe")!
|
|
||||||
if let translation {
|
|
||||||
let lang = Locale.current.localizedString(forLanguageCode: translation.detectedSourceLanguage) ?? translation.detectedSourceLanguage
|
|
||||||
config.title = "Translated from \(lang)"
|
|
||||||
} else {
|
|
||||||
config.title = "Translate"
|
|
||||||
}
|
|
||||||
|
|
||||||
if let translateButton {
|
|
||||||
translateButton.configuration = config
|
|
||||||
} else {
|
|
||||||
let button = TranslateButton(configuration: config)
|
|
||||||
button.addTarget(self, action: #selector(translatePressed), for: .touchUpInside)
|
|
||||||
translateButton = button
|
|
||||||
contentContainer.insertArrangedSubview(button, after: contentTextView)
|
|
||||||
}
|
|
||||||
translateButton!.isEnabled = translation == nil
|
|
||||||
} else if let translateButton {
|
|
||||||
contentContainer.removeArrangedSubview(translateButton)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createObservers() {
|
private func createObservers() {
|
||||||
|
@ -545,10 +487,6 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
||||||
delegate?.show(StatusEditHistoryViewController(statusID: statusID, mastodonController: mastodonController), sender: nil)
|
delegate?.show(StatusEditHistoryViewController(statusID: statusID, mastodonController: mastodonController), sender: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func translatePressed() {
|
|
||||||
translateStatus?()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ConversationMainStatusAccountDetailAccessibilityElement: UIAccessibilityElement {
|
private class ConversationMainStatusAccountDetailAccessibilityElement: UIAccessibilityElement {
|
||||||
|
@ -617,13 +555,3 @@ extension ConversationMainStatusCollectionViewCell: UIPointerInteractionDelegate
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TranslateButton: UIButton, StatusContentView {
|
|
||||||
var statusContentFillsHorizontally: Bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
func estimateHeight(effectiveWidth: CGFloat) -> CGFloat {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -24,11 +24,7 @@ protocol StatusCollectionViewCell: UICollectionViewCell, AttachmentViewDelegate
|
||||||
var usernameLabel: UILabel { get }
|
var usernameLabel: UILabel { get }
|
||||||
var contentWarningLabel: EmojiLabel { get }
|
var contentWarningLabel: EmojiLabel { get }
|
||||||
var collapseButton: StatusCollapseButton { get }
|
var collapseButton: StatusCollapseButton { get }
|
||||||
var contentContainer: StatusContentContainer { get }
|
var contentContainer: StatusContentContainer<StatusContentTextView, StatusPollView> { get }
|
||||||
var contentTextView: StatusContentTextView { get }
|
|
||||||
var pollView: StatusPollView { get }
|
|
||||||
var cardView: StatusCardView { get }
|
|
||||||
var attachmentsView: AttachmentsContainerView { get }
|
|
||||||
var replyButton: UIButton { get }
|
var replyButton: UIButton { get }
|
||||||
var favoriteButton: ToggleableButton { get }
|
var favoriteButton: ToggleableButton { get }
|
||||||
var reblogButton: ToggleableButton { get }
|
var reblogButton: ToggleableButton { get }
|
||||||
|
@ -94,20 +90,20 @@ extension StatusCollectionViewCell {
|
||||||
|
|
||||||
updateAccountUI(account: status.account)
|
updateAccountUI(account: status.account)
|
||||||
|
|
||||||
contentTextView.setTextFrom(status: status, precomputed: precomputedContent)
|
contentContainer.contentTextView.setTextFrom(status: status, precomputed: precomputedContent)
|
||||||
contentTextView.navigationDelegate = delegate
|
contentContainer.contentTextView.navigationDelegate = delegate
|
||||||
self.updateAttachmentsUI(status: status)
|
self.updateAttachmentsUI(status: status)
|
||||||
pollView.isHidden = status.poll == nil
|
contentContainer.pollView.isHidden = status.poll == nil
|
||||||
pollView.mastodonController = mastodonController
|
contentContainer.pollView.mastodonController = mastodonController
|
||||||
pollView.delegate = delegate
|
contentContainer.pollView.delegate = delegate
|
||||||
pollView.updateUI(status: status, poll: status.poll)
|
contentContainer.pollView.updateUI(status: status, poll: status.poll)
|
||||||
if Preferences.shared.showLinkPreviews {
|
if Preferences.shared.showLinkPreviews {
|
||||||
cardView.updateUI(status: status)
|
contentContainer.cardView.updateUI(status: status)
|
||||||
cardView.isHidden = status.card == nil
|
contentContainer.cardView.isHidden = status.card == nil
|
||||||
cardView.navigationDelegate = delegate
|
contentContainer.cardView.navigationDelegate = delegate
|
||||||
cardView.actionProvider = delegate
|
contentContainer.cardView.actionProvider = delegate
|
||||||
} else {
|
} else {
|
||||||
cardView.isHidden = true
|
contentContainer.cardView.isHidden = true
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUIForPreferences(status: status)
|
updateUIForPreferences(status: status)
|
||||||
|
@ -172,8 +168,8 @@ extension StatusCollectionViewCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateAttachmentsUI(status: StatusMO) {
|
func updateAttachmentsUI(status: StatusMO) {
|
||||||
attachmentsView.delegate = self
|
contentContainer.attachmentsView.delegate = self
|
||||||
attachmentsView.updateUI(attachments: status.attachments)
|
contentContainer.attachmentsView.updateUI(attachments: status.attachments)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateAccountUI(account: AccountMO) {
|
func updateAccountUI(account: AccountMO) {
|
||||||
|
@ -186,20 +182,20 @@ extension StatusCollectionViewCell {
|
||||||
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * Self.avatarImageViewSize
|
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * Self.avatarImageViewSize
|
||||||
|
|
||||||
let newCardHidden = !Preferences.shared.showLinkPreviews || status.card == nil
|
let newCardHidden = !Preferences.shared.showLinkPreviews || status.card == nil
|
||||||
if cardView.isHidden != newCardHidden {
|
if contentContainer.cardView.isHidden != newCardHidden {
|
||||||
delegate?.statusCellNeedsReconfigure(self, animated: false, completion: nil)
|
delegate?.statusCellNeedsReconfigure(self, animated: false, completion: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch Preferences.shared.attachmentBlurMode {
|
switch Preferences.shared.attachmentBlurMode {
|
||||||
case .never:
|
case .never:
|
||||||
attachmentsView.contentHidden = false
|
contentContainer.attachmentsView.contentHidden = false
|
||||||
case .always:
|
case .always:
|
||||||
attachmentsView.contentHidden = true
|
contentContainer.attachmentsView.contentHidden = true
|
||||||
default:
|
default:
|
||||||
if status.sensitive {
|
if status.sensitive {
|
||||||
attachmentsView.contentHidden = status.spoilerText.isEmpty || Preferences.shared.blurMediaBehindContentWarning
|
contentContainer.attachmentsView.contentHidden = status.spoilerText.isEmpty || Preferences.shared.blurMediaBehindContentWarning
|
||||||
} else {
|
} else {
|
||||||
attachmentsView.contentHidden = false
|
contentContainer.attachmentsView.contentHidden = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,8 +211,8 @@ extension StatusCollectionViewCell {
|
||||||
// only called when isGrayscale does not match the pref
|
// only called when isGrayscale does not match the pref
|
||||||
func updateGrayscaleableUI(status: StatusMO) {
|
func updateGrayscaleableUI(status: StatusMO) {
|
||||||
isGrayscale = Preferences.shared.grayscaleImages
|
isGrayscale = Preferences.shared.grayscaleImages
|
||||||
if contentTextView.hasEmojis {
|
if contentContainer.contentTextView.hasEmojis {
|
||||||
contentTextView.setEmojis(status.emojis, identifier: status.id)
|
contentContainer.contentTextView.setEmojis(status.emojis, identifier: status.id)
|
||||||
}
|
}
|
||||||
displayNameLabel.updateForAccountDisplayName(account: status.account)
|
displayNameLabel.updateForAccountDisplayName(account: status.account)
|
||||||
}
|
}
|
||||||
|
@ -239,10 +235,10 @@ extension StatusCollectionViewCell {
|
||||||
// do not include reply action here, because the cell already contains a button for it
|
// do not include reply action here, because the cell already contains a button for it
|
||||||
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: delegate?.actionsForStatus(status, source: .view(moreButton), includeStatusButtonActions: false) ?? [])
|
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: delegate?.actionsForStatus(status, source: .view(moreButton), includeStatusButtonActions: false) ?? [])
|
||||||
|
|
||||||
pollView.isHidden = status.poll == nil
|
contentContainer.pollView.isHidden = status.poll == nil
|
||||||
pollView.mastodonController = mastodonController
|
contentContainer.pollView.mastodonController = mastodonController
|
||||||
pollView.delegate = delegate
|
contentContainer.pollView.delegate = delegate
|
||||||
pollView.updateUI(status: status, poll: status.poll)
|
contentContainer.pollView.updateUI(status: status, poll: status.poll)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setShowThreadLinks(prev: Bool, next: Bool) {
|
func setShowThreadLinks(prev: Bool, next: Bool) {
|
||||||
|
@ -331,7 +327,7 @@ extension StatusCollectionViewCell {
|
||||||
func attachmentViewGallery(startingAt index: Int) -> GalleryViewController? {
|
func attachmentViewGallery(startingAt index: Int) -> GalleryViewController? {
|
||||||
guard let delegate = delegate,
|
guard let delegate = delegate,
|
||||||
let status = mastodonController.persistentContainer.status(for: statusID) else { return nil }
|
let status = mastodonController.persistentContainer.status(for: statusID) else { return nil }
|
||||||
let sourceViews = status.attachments.map(attachmentsView.getAttachmentView(for:))
|
let sourceViews = status.attachments.map(contentContainer.attachmentsView.getAttachmentView(for:))
|
||||||
let gallery = delegate.gallery(attachments: status.attachments, sourceViews: sourceViews, startIndex: index)
|
let gallery = delegate.gallery(attachments: status.attachments, sourceViews: sourceViews, startIndex: index)
|
||||||
// TODO: PiP
|
// TODO: PiP
|
||||||
// gallery.avPlayerViewControllerDelegate = self
|
// gallery.avPlayerViewControllerDelegate = self
|
||||||
|
|
|
@ -8,11 +8,45 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class StatusContentContainer: UIView {
|
protocol StatusContentPollView: UIView {
|
||||||
// TODO: this is a weird place for this
|
func estimateHeight(effectiveWidth: CGFloat) -> CGFloat
|
||||||
static var cardViewHeight: CGFloat { 90 }
|
}
|
||||||
|
|
||||||
|
class StatusContentContainer<ContentView: ContentTextView, PollView: StatusContentPollView>: UIView {
|
||||||
|
|
||||||
private var arrangedSubviews: [any StatusContentView]
|
private let useTopSpacer: Bool
|
||||||
|
private lazy var topSpacer = UIView().configure {
|
||||||
|
$0.backgroundColor = .clear
|
||||||
|
// other 4pt is provided by this view's own spacing
|
||||||
|
$0.heightAnchor.constraint(equalToConstant: 4).isActive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let contentTextView = ContentView().configure {
|
||||||
|
$0.adjustsFontForContentSizeCategory = true
|
||||||
|
$0.isScrollEnabled = false
|
||||||
|
$0.backgroundColor = nil
|
||||||
|
$0.isEditable = false
|
||||||
|
$0.isSelectable = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private static var cardViewHeight: CGFloat { 90 }
|
||||||
|
let cardView = StatusCardView().configure {
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
$0.heightAnchor.constraint(equalToConstant: StatusContentContainer.cardViewHeight),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
let attachmentsView = AttachmentsContainerView()
|
||||||
|
|
||||||
|
let pollView = PollView()
|
||||||
|
|
||||||
|
private var arrangedSubviews: [UIView] {
|
||||||
|
if useTopSpacer {
|
||||||
|
return [topSpacer, contentTextView, cardView, attachmentsView, pollView]
|
||||||
|
} else {
|
||||||
|
return [contentTextView, cardView, attachmentsView, pollView]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var isHiddenObservations: [NSKeyValueObservation] = []
|
private var isHiddenObservations: [NSKeyValueObservation] = []
|
||||||
|
|
||||||
|
@ -27,12 +61,8 @@ class StatusContentContainer: UIView {
|
||||||
subviews.filter { !$0.isHidden }.map(\.bounds.height).reduce(0, +)
|
subviews.filter { !$0.isHidden }.map(\.bounds.height).reduce(0, +)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(arrangedSubviews: [any StatusContentView], useTopSpacer: Bool) {
|
init(useTopSpacer: Bool) {
|
||||||
var arrangedSubviews = arrangedSubviews
|
self.useTopSpacer = useTopSpacer
|
||||||
if useTopSpacer {
|
|
||||||
arrangedSubviews.insert(TopSpacerView(), at: 0)
|
|
||||||
}
|
|
||||||
self.arrangedSubviews = arrangedSubviews
|
|
||||||
|
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
@ -40,14 +70,10 @@ class StatusContentContainer: UIView {
|
||||||
subview.translatesAutoresizingMaskIntoConstraints = false
|
subview.translatesAutoresizingMaskIntoConstraints = false
|
||||||
addSubview(subview)
|
addSubview(subview)
|
||||||
|
|
||||||
if subview.statusContentFillsHorizontally {
|
NSLayoutConstraint.activate([
|
||||||
NSLayoutConstraint.activate([
|
subview.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
subview.leadingAnchor.constraint(equalTo: leadingAnchor),
|
subview.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
subview.trailingAnchor.constraint(equalTo: trailingAnchor),
|
])
|
||||||
])
|
|
||||||
} else {
|
|
||||||
subview.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// this constraint needs to have low priority so that during the collapse/expand animation, the content container is the view that shrinks/expands
|
// this constraint needs to have low priority so that during the collapse/expand animation, the content container is the view that shrinks/expands
|
||||||
|
@ -56,21 +82,17 @@ class StatusContentContainer: UIView {
|
||||||
|
|
||||||
setNeedsUpdateConstraints()
|
setNeedsUpdateConstraints()
|
||||||
|
|
||||||
updateObservations()
|
isHiddenObservations = arrangedSubviews.map {
|
||||||
|
$0.observe(\.isHidden) { [unowned self] _, _ in
|
||||||
|
self.setNeedsUpdateConstraints()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateObservations() {
|
|
||||||
isHiddenObservations = arrangedSubviews.map {
|
|
||||||
$0.observeIsHidden { [unowned self] in
|
|
||||||
self.setNeedsUpdateConstraints()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func updateConstraints() {
|
override func updateConstraints() {
|
||||||
let visibleSubviews = IndexSet(arrangedSubviews.indices.filter { !arrangedSubviews[$0].isHidden })
|
let visibleSubviews = IndexSet(arrangedSubviews.indices.filter { !arrangedSubviews[$0].isHidden })
|
||||||
if self.visibleSubviews != visibleSubviews {
|
if self.visibleSubviews != visibleSubviews {
|
||||||
|
@ -107,31 +129,6 @@ class StatusContentContainer: UIView {
|
||||||
super.updateConstraints()
|
super.updateConstraints()
|
||||||
}
|
}
|
||||||
|
|
||||||
func insertArrangedSubview(_ view: any StatusContentView, after: any StatusContentView) {
|
|
||||||
view.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
addSubview(view)
|
|
||||||
if view.statusContentFillsHorizontally {
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
view.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
||||||
view.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
||||||
])
|
|
||||||
} else {
|
|
||||||
view.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
|
|
||||||
}
|
|
||||||
|
|
||||||
let index = arrangedSubviews.firstIndex(where: { $0 === after })!
|
|
||||||
arrangedSubviews.insert(view, at: index + 1)
|
|
||||||
setNeedsUpdateConstraints()
|
|
||||||
updateObservations()
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeArrangedSubview(_ view: any StatusContentView) {
|
|
||||||
view.removeFromSuperview()
|
|
||||||
arrangedSubviews.removeAll(where: { $0 === view })
|
|
||||||
setNeedsUpdateConstraints()
|
|
||||||
updateObservations()
|
|
||||||
}
|
|
||||||
|
|
||||||
func setCollapsed(_ collapsed: Bool) {
|
func setCollapsed(_ collapsed: Bool) {
|
||||||
guard collapsed != isCollapsed else {
|
guard collapsed != isCollapsed else {
|
||||||
return
|
return
|
||||||
|
@ -150,67 +147,18 @@ class StatusContentContainer: UIView {
|
||||||
// just roughly inline with the content height
|
// just roughly inline with the content height
|
||||||
func estimateVisibleSubviewHeight(effectiveWidth: CGFloat) -> CGFloat {
|
func estimateVisibleSubviewHeight(effectiveWidth: CGFloat) -> CGFloat {
|
||||||
var height: CGFloat = 0
|
var height: CGFloat = 0
|
||||||
for view in arrangedSubviews where !view.isHidden {
|
height += contentTextView.sizeThatFits(CGSize(width: effectiveWidth, height: UIView.layoutFittingCompressedSize.height)).height
|
||||||
height += view.estimateHeight(effectiveWidth: effectiveWidth)
|
if !cardView.isHidden {
|
||||||
|
height += StatusContentContainer.cardViewHeight
|
||||||
|
}
|
||||||
|
if !attachmentsView.isHidden {
|
||||||
|
height += effectiveWidth / attachmentsView.aspectRatio
|
||||||
|
}
|
||||||
|
if !pollView.isHidden {
|
||||||
|
let pollHeight = pollView.estimateHeight(effectiveWidth: effectiveWidth)
|
||||||
|
height += pollHeight
|
||||||
}
|
}
|
||||||
return height
|
return height
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusContentContainer {
|
|
||||||
private class TopSpacerView: UIView, StatusContentView {
|
|
||||||
init() {
|
|
||||||
super.init(frame: .zero)
|
|
||||||
|
|
||||||
backgroundColor = .clear
|
|
||||||
// other 4pt is provided by this view's own spacing
|
|
||||||
heightAnchor.constraint(equalToConstant: 4).isActive = true
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func estimateHeight(effectiveWidth: CGFloat) -> CGFloat {
|
|
||||||
4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension UIView {
|
|
||||||
func observeIsHidden(_ f: @escaping () -> Void) -> NSKeyValueObservation {
|
|
||||||
self.observe(\.isHidden) { _, _ in
|
|
||||||
f()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol StatusContentView: UIView {
|
|
||||||
var statusContentFillsHorizontally: Bool { get }
|
|
||||||
func estimateHeight(effectiveWidth: CGFloat) -> CGFloat
|
|
||||||
}
|
|
||||||
|
|
||||||
extension StatusContentView {
|
|
||||||
var statusContentFillsHorizontally: Bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ContentTextView: StatusContentView {
|
|
||||||
func estimateHeight(effectiveWidth: CGFloat) -> CGFloat {
|
|
||||||
sizeThatFits(CGSize(width: effectiveWidth, height: UIView.layoutFittingCompressedSize.height)).height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension StatusCardView: StatusContentView {
|
|
||||||
func estimateHeight(effectiveWidth: CGFloat) -> CGFloat {
|
|
||||||
StatusContentContainer.cardViewHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension AttachmentsContainerView: StatusContentView {
|
|
||||||
func estimateHeight(effectiveWidth: CGFloat) -> CGFloat {
|
|
||||||
effectiveWidth / aspectRatio
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -186,33 +186,24 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
$0.setContentCompressionResistancePriority(.required, for: .vertical)
|
$0.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||||
}
|
}
|
||||||
|
|
||||||
private(set) lazy var contentContainer = StatusContentContainer(arrangedSubviews: [
|
let contentContainer = StatusContentContainer<StatusContentTextView, StatusPollView>(useTopSpacer: false).configure {
|
||||||
contentTextView,
|
$0.contentTextView.defaultFont = TimelineStatusCollectionViewCell.contentFont
|
||||||
cardView,
|
$0.contentTextView.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont
|
||||||
attachmentsView,
|
$0.contentTextView.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle
|
||||||
pollView,
|
|
||||||
] as! [any StatusContentView], useTopSpacer: false).configure {
|
|
||||||
$0.setContentHuggingPriority(.defaultLow, for: .vertical)
|
$0.setContentHuggingPriority(.defaultLow, for: .vertical)
|
||||||
}
|
}
|
||||||
|
private var contentTextView: StatusContentTextView {
|
||||||
let contentTextView = StatusContentTextView().configure {
|
contentContainer.contentTextView
|
||||||
$0.adjustsFontForContentSizeCategory = true
|
|
||||||
$0.isScrollEnabled = false
|
|
||||||
$0.backgroundColor = nil
|
|
||||||
$0.isEditable = false
|
|
||||||
$0.isSelectable = false
|
|
||||||
$0.defaultFont = TimelineStatusCollectionViewCell.contentFont
|
|
||||||
$0.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont
|
|
||||||
$0.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle
|
|
||||||
}
|
}
|
||||||
|
private var cardView: StatusCardView {
|
||||||
let cardView = StatusCardView().configure {
|
contentContainer.cardView
|
||||||
$0.heightAnchor.constraint(equalToConstant: StatusContentContainer.cardViewHeight).isActive = true
|
}
|
||||||
|
private var attachmentsView: AttachmentsContainerView {
|
||||||
|
contentContainer.attachmentsView
|
||||||
|
}
|
||||||
|
private var pollView: StatusPollView {
|
||||||
|
contentContainer.pollView
|
||||||
}
|
}
|
||||||
|
|
||||||
let attachmentsView = AttachmentsContainerView()
|
|
||||||
|
|
||||||
let pollView = StatusPollView()
|
|
||||||
|
|
||||||
private var placeholderReplyButtonLeadingConstraint: NSLayoutConstraint!
|
private var placeholderReplyButtonLeadingConstraint: NSLayoutConstraint!
|
||||||
private lazy var actionsContainer = UIView().configure {
|
private lazy var actionsContainer = UIView().configure {
|
||||||
|
|
Loading…
Reference in New Issue