Add status edit history view
This commit is contained in:
parent
7d3c82f4b7
commit
db7c183d06
|
@ -24,6 +24,13 @@ public struct Mention: Codable, Sendable {
|
||||||
self.url = try container.decode(WebURL.self, forKey: .url)
|
self.url = try container.decode(WebURL.self, forKey: .url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public init(url: WebURL, username: String, acct: String, id: String) {
|
||||||
|
self.url = url
|
||||||
|
self.username = username
|
||||||
|
self.acct = acct
|
||||||
|
self.id = id
|
||||||
|
}
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case url
|
case url
|
||||||
case username
|
case username
|
||||||
|
|
|
@ -169,6 +169,10 @@ public final class Status: StatusProtocol, Decodable, Sendable {
|
||||||
return Request(method: .get, path: "/api/v1/statuses/\(statusID)/source")
|
return Request(method: .get, path: "/api/v1/statuses/\(statusID)/source")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func history(_ statusID: String) -> Request<[StatusEdit]> {
|
||||||
|
return Request(method: .get, path: "/api/v1/statuses/\(statusID)/history")
|
||||||
|
}
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case id
|
case id
|
||||||
case uri
|
case uri
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
//
|
||||||
|
// StatusEdit.swift
|
||||||
|
// Pachyderm
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 5/11/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct StatusEdit: Decodable {
|
||||||
|
public let content: String
|
||||||
|
public let spoilerText: String
|
||||||
|
public let sensitive: Bool
|
||||||
|
public let createdAt: Date
|
||||||
|
public let account: Account
|
||||||
|
public let poll: Poll?
|
||||||
|
public let attachments: [Attachment]
|
||||||
|
public let emojis: [Emoji]
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case content
|
||||||
|
case spoilerText = "spoiler_text"
|
||||||
|
case sensitive
|
||||||
|
case createdAt = "created_at"
|
||||||
|
case account = "account"
|
||||||
|
case poll
|
||||||
|
case attachments = "media_attachments"
|
||||||
|
case emojis
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Poll: Decodable {
|
||||||
|
public let options: [Option]
|
||||||
|
|
||||||
|
public struct Option: Decodable {
|
||||||
|
public let title: String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -297,6 +297,10 @@
|
||||||
D6D706A029466649000827ED /* ScrollingSegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D7069F29466649000827ED /* ScrollingSegmentedControl.swift */; };
|
D6D706A029466649000827ED /* ScrollingSegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D7069F29466649000827ED /* ScrollingSegmentedControl.swift */; };
|
||||||
D6D706A72948D4D0000827ED /* TimlineState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D706A62948D4D0000827ED /* TimlineState.swift */; };
|
D6D706A72948D4D0000827ED /* TimlineState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D706A62948D4D0000827ED /* TimlineState.swift */; };
|
||||||
D6D79F262A0C8D2700AB2315 /* FetchStatusSourceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F252A0C8D2700AB2315 /* FetchStatusSourceService.swift */; };
|
D6D79F262A0C8D2700AB2315 /* FetchStatusSourceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F252A0C8D2700AB2315 /* FetchStatusSourceService.swift */; };
|
||||||
|
D6D79F292A0D596B00AB2315 /* StatusEditHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F282A0D596B00AB2315 /* StatusEditHistoryViewController.swift */; };
|
||||||
|
D6D79F2B2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift */; };
|
||||||
|
D6D79F2D2A0D61B400AB2315 /* StatusEditContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2C2A0D61B400AB2315 /* StatusEditContentTextView.swift */; };
|
||||||
|
D6D79F2F2A0D6A7F00AB2315 /* StatusEditPollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2E2A0D6A7F00AB2315 /* StatusEditPollView.swift */; };
|
||||||
D6D94955298963A900C59229 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D94954298963A900C59229 /* Colors.swift */; };
|
D6D94955298963A900C59229 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D94954298963A900C59229 /* Colors.swift */; };
|
||||||
D6D9498D298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498C298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift */; };
|
D6D9498D298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498C298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift */; };
|
||||||
D6D9498F298EB79400C59229 /* CopyableLable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498E298EB79400C59229 /* CopyableLable.swift */; };
|
D6D9498F298EB79400C59229 /* CopyableLable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498E298EB79400C59229 /* CopyableLable.swift */; };
|
||||||
|
@ -692,6 +696,10 @@
|
||||||
D6D706A62948D4D0000827ED /* TimlineState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimlineState.swift; sourceTree = "<group>"; };
|
D6D706A62948D4D0000827ED /* TimlineState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimlineState.swift; sourceTree = "<group>"; };
|
||||||
D6D706A829498C82000827ED /* Tusker.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Tusker.xcconfig; sourceTree = "<group>"; };
|
D6D706A829498C82000827ED /* Tusker.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Tusker.xcconfig; sourceTree = "<group>"; };
|
||||||
D6D79F252A0C8D2700AB2315 /* FetchStatusSourceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchStatusSourceService.swift; sourceTree = "<group>"; };
|
D6D79F252A0C8D2700AB2315 /* FetchStatusSourceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchStatusSourceService.swift; sourceTree = "<group>"; };
|
||||||
|
D6D79F282A0D596B00AB2315 /* StatusEditHistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditHistoryViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
D6D79F2C2A0D61B400AB2315 /* StatusEditContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditContentTextView.swift; sourceTree = "<group>"; };
|
||||||
|
D6D79F2E2A0D6A7F00AB2315 /* StatusEditPollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditPollView.swift; sourceTree = "<group>"; };
|
||||||
D6D94954298963A900C59229 /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = "<group>"; };
|
D6D94954298963A900C59229 /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = "<group>"; };
|
||||||
D6D9498C298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingStatusCollectionViewCell.swift; sourceTree = "<group>"; };
|
D6D9498C298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingStatusCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D6D9498E298EB79400C59229 /* CopyableLable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableLable.swift; sourceTree = "<group>"; };
|
D6D9498E298EB79400C59229 /* CopyableLable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableLable.swift; sourceTree = "<group>"; };
|
||||||
|
@ -979,6 +987,7 @@
|
||||||
D65B4B522971F6E300DABDFB /* Report */,
|
D65B4B522971F6E300DABDFB /* Report */,
|
||||||
D6BC9DD8232D8BCA002CA326 /* Search */,
|
D6BC9DD8232D8BCA002CA326 /* Search */,
|
||||||
D6A3BC8C2321FF9B00FD64D5 /* Status Action Account List */,
|
D6A3BC8C2321FF9B00FD64D5 /* Status Action Account List */,
|
||||||
|
D6D79F272A0D595D00AB2315 /* Status Edit History */,
|
||||||
D641C781213DD7DD004B4513 /* Timeline */,
|
D641C781213DD7DD004B4513 /* Timeline */,
|
||||||
D6C693FA2162FE5D007D6A6D /* Utilities */,
|
D6C693FA2162FE5D007D6A6D /* Utilities */,
|
||||||
);
|
);
|
||||||
|
@ -1532,6 +1541,17 @@
|
||||||
path = Gestures;
|
path = Gestures;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D6D79F272A0D595D00AB2315 /* Status Edit History */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D6D79F282A0D596B00AB2315 /* StatusEditHistoryViewController.swift */,
|
||||||
|
D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift */,
|
||||||
|
D6D79F2C2A0D61B400AB2315 /* StatusEditContentTextView.swift */,
|
||||||
|
D6D79F2E2A0D6A7F00AB2315 /* StatusEditPollView.swift */,
|
||||||
|
);
|
||||||
|
path = "Status Edit History";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D6DEA0DB268400AF00FE896A /* Confirm Load More Cell */ = {
|
D6DEA0DB268400AF00FE896A /* Confirm Load More Cell */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1952,6 +1972,7 @@
|
||||||
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */,
|
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */,
|
||||||
D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */,
|
D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */,
|
||||||
D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */,
|
D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */,
|
||||||
|
D6D79F2B2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift in Sources */,
|
||||||
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */,
|
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */,
|
||||||
D6D9498D298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift in Sources */,
|
D6D9498D298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift in Sources */,
|
||||||
D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */,
|
D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */,
|
||||||
|
@ -2028,6 +2049,7 @@
|
||||||
D623A53D2635F5590095BD04 /* StatusPollView.swift in Sources */,
|
D623A53D2635F5590095BD04 /* StatusPollView.swift in Sources */,
|
||||||
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */,
|
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */,
|
||||||
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
|
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
|
||||||
|
D6D79F2F2A0D6A7F00AB2315 /* StatusEditPollView.swift in Sources */,
|
||||||
D65B4B5E2973040D00DABDFB /* ReportAddStatusView.swift in Sources */,
|
D65B4B5E2973040D00DABDFB /* ReportAddStatusView.swift in Sources */,
|
||||||
D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */,
|
D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */,
|
||||||
D6CA8CDE296387310050C433 /* SaveToPhotosActivity.swift in Sources */,
|
D6CA8CDE296387310050C433 /* SaveToPhotosActivity.swift in Sources */,
|
||||||
|
@ -2122,6 +2144,7 @@
|
||||||
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */,
|
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */,
|
||||||
D68A76F129539116001DA1B3 /* FlipView.swift in Sources */,
|
D68A76F129539116001DA1B3 /* FlipView.swift in Sources */,
|
||||||
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */,
|
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */,
|
||||||
|
D6D79F2D2A0D61B400AB2315 /* StatusEditContentTextView.swift in Sources */,
|
||||||
D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */,
|
D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */,
|
||||||
D61F75B5293BD97400C0B37F /* DeleteFilterService.swift in Sources */,
|
D61F75B5293BD97400C0B37F /* DeleteFilterService.swift in Sources */,
|
||||||
D6DFC6A0242C4CCC00ACC392 /* Weak.swift in Sources */,
|
D6DFC6A0242C4CCC00ACC392 /* Weak.swift in Sources */,
|
||||||
|
@ -2160,6 +2183,7 @@
|
||||||
D6E77D09286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift in Sources */,
|
D6E77D09286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift in Sources */,
|
||||||
D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */,
|
D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */,
|
||||||
D6BC8748219738E1006163F1 /* EnhancedTableViewController.swift in Sources */,
|
D6BC8748219738E1006163F1 /* EnhancedTableViewController.swift in Sources */,
|
||||||
|
D6D79F292A0D596B00AB2315 /* StatusEditHistoryViewController.swift in Sources */,
|
||||||
D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */,
|
D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */,
|
||||||
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */,
|
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */,
|
||||||
D61F75B3293BD89C00C0B37F /* UpdateFilterService.swift in Sources */,
|
D61F75B3293BD89C00C0B37F /* UpdateFilterService.swift in Sources */,
|
||||||
|
|
|
@ -18,6 +18,12 @@ public final class AccountMO: NSManagedObject, AccountProtocol {
|
||||||
return NSFetchRequest<AccountMO>(entityName: "Account")
|
return NSFetchRequest<AccountMO>(entityName: "Account")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@nonobjc public class func fetchRequest(url: URL) -> NSFetchRequest<AccountMO> {
|
||||||
|
let req = NSFetchRequest<AccountMO>(entityName: "Account")
|
||||||
|
req.predicate = NSPredicate(format: "url = %@", url as NSURL)
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
@NSManaged public var acct: String
|
@NSManaged public var acct: String
|
||||||
@NSManaged public var avatar: URL?
|
@NSManaged public var avatar: URL?
|
||||||
@NSManaged public var botCD: Bool
|
@NSManaged public var botCD: Bool
|
||||||
|
|
|
@ -0,0 +1,165 @@
|
||||||
|
//
|
||||||
|
// StatusEditCollectionViewCell.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 5/11/23.
|
||||||
|
// Copyright © 2023 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
protocol StatusEditCollectionViewCellDelegate: AnyObject, TuskerNavigationDelegate {
|
||||||
|
func statusEditCellNeedsReconfigure(_ cell: StatusEditCollectionViewCell, animated: Bool, completion: (() -> Void)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
class StatusEditCollectionViewCell: UICollectionViewListCell {
|
||||||
|
|
||||||
|
private lazy var contentVStack = UIStackView(arrangedSubviews: [
|
||||||
|
timestampLabel,
|
||||||
|
contentWarningLabel,
|
||||||
|
collapseButton,
|
||||||
|
contentContainer,
|
||||||
|
]).configure {
|
||||||
|
$0.axis = .vertical
|
||||||
|
$0.spacing = 4
|
||||||
|
$0.alignment = .fill
|
||||||
|
}
|
||||||
|
|
||||||
|
private lazy var timestampLabel = UILabel().configure {
|
||||||
|
$0.textColor = .secondaryLabel
|
||||||
|
$0.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).addingAttributes([
|
||||||
|
.traits: [
|
||||||
|
UIFontDescriptor.TraitKey.weight: UIFont.Weight.light.rawValue,
|
||||||
|
]
|
||||||
|
]), size: 0)
|
||||||
|
$0.adjustsFontForContentSizeCategory = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private lazy var contentWarningLabel = EmojiLabel().configure {
|
||||||
|
$0.numberOfLines = 0
|
||||||
|
$0.textColor = .secondaryLabel
|
||||||
|
$0.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).addingAttributes([
|
||||||
|
.traits: [
|
||||||
|
UIFontDescriptor.TraitKey.weight: UIFont.Weight.bold.rawValue
|
||||||
|
]
|
||||||
|
]), size: 0)
|
||||||
|
$0.adjustsFontForContentSizeCategory = true
|
||||||
|
$0.setContentHuggingPriority(.defaultHigh, for: .vertical)
|
||||||
|
$0.isUserInteractionEnabled = true
|
||||||
|
$0.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(collapseButtonPressed)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private lazy var collapseButton = StatusCollapseButton(configuration: {
|
||||||
|
var config = UIButton.Configuration.filled()
|
||||||
|
config.image = UIImage(systemName: "chevron.down")
|
||||||
|
return config
|
||||||
|
}()).configure {
|
||||||
|
$0.tintAdjustmentMode = .normal
|
||||||
|
$0.setContentHuggingPriority(.defaultHigh, for: .vertical)
|
||||||
|
$0.addTarget(self, action: #selector(collapseButtonPressed), for: .touchUpInside)
|
||||||
|
}
|
||||||
|
|
||||||
|
private let contentContainer = StatusContentContainer<StatusEditContentTextView, StatusEditPollView>(useTopSpacer: false).configure {
|
||||||
|
$0.contentTextView.defaultFont = TimelineStatusCollectionViewCell.contentFont
|
||||||
|
$0.contentTextView.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont
|
||||||
|
$0.contentTextView.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle
|
||||||
|
$0.setContentHuggingPriority(.defaultLow, for: .vertical)
|
||||||
|
}
|
||||||
|
|
||||||
|
weak var delegate: StatusEditCollectionViewCellDelegate?
|
||||||
|
private var mastodonController: MastodonController! { delegate?.apiController }
|
||||||
|
|
||||||
|
private var edit: StatusEdit!
|
||||||
|
private var statusState: CollapseState!
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
contentVStack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(contentVStack)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
contentVStack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
|
||||||
|
contentVStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -6),
|
||||||
|
contentVStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
|
||||||
|
contentVStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: accessibility
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: Configure UI
|
||||||
|
|
||||||
|
func updateUI(edit: StatusEdit, state: CollapseState, index: Int) {
|
||||||
|
self.edit = edit
|
||||||
|
self.statusState = state
|
||||||
|
|
||||||
|
timestampLabel.text = ConversationMainStatusCollectionViewCell.dateFormatter.string(from: edit.createdAt)
|
||||||
|
|
||||||
|
contentContainer.contentTextView.setTextFrom(edit: edit, index: index)
|
||||||
|
contentContainer.contentTextView.navigationDelegate = delegate
|
||||||
|
contentContainer.attachmentsView.delegate = self
|
||||||
|
contentContainer.attachmentsView.updateUI(attachments: edit.attachments)
|
||||||
|
contentContainer.pollView.isHidden = edit.poll == nil
|
||||||
|
contentContainer.pollView.updateUI(poll: edit.poll, emojis: edit.emojis)
|
||||||
|
contentContainer.cardView.isHidden = true
|
||||||
|
|
||||||
|
contentWarningLabel.text = edit.spoilerText
|
||||||
|
contentWarningLabel.isHidden = edit.spoilerText.isEmpty
|
||||||
|
if !contentWarningLabel.isHidden {
|
||||||
|
contentWarningLabel.setEmojis(edit.emojis, identifier: index)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = state.resolveFor(status: edit, height: {
|
||||||
|
layoutIfNeeded()
|
||||||
|
return contentContainer.visibleSubviewHeight
|
||||||
|
})
|
||||||
|
collapseButton.isHidden = !state.collapsible!
|
||||||
|
contentContainer.setCollapsed(state.collapsed!)
|
||||||
|
if state.collapsed! {
|
||||||
|
contentContainer.alpha = 0
|
||||||
|
// TODO: is this accessing the image view before the button's been laid out?
|
||||||
|
collapseButton.imageView!.transform = CGAffineTransform(rotationAngle: 0)
|
||||||
|
collapseButton.accessibilityLabel = NSLocalizedString("Expand Status", comment: "expand status button accessibility label")
|
||||||
|
} else {
|
||||||
|
contentContainer.alpha = 1
|
||||||
|
collapseButton.imageView!.transform = CGAffineTransform(rotationAngle: .pi)
|
||||||
|
collapseButton.accessibilityLabel = NSLocalizedString("Collapse Status", comment: "collapse status button accessibility label")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Interaction
|
||||||
|
|
||||||
|
@objc private func collapseButtonPressed() {
|
||||||
|
statusState.collapsed!.toggle()
|
||||||
|
contentContainer.layer.masksToBounds = true
|
||||||
|
delegate?.statusEditCellNeedsReconfigure(self, animated: true) {
|
||||||
|
self.contentContainer.layer.masksToBounds = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StatusEditCollectionViewCell: AttachmentViewDelegate {
|
||||||
|
func attachmentViewGallery(startingAt index: Int) -> GalleryViewController? {
|
||||||
|
guard let delegate else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let attachments = contentContainer.attachmentsView.attachments!
|
||||||
|
let sourceViews = attachments.map {
|
||||||
|
contentContainer.attachmentsView.getAttachmentView(for: $0)
|
||||||
|
}
|
||||||
|
let gallery = delegate.gallery(attachments: attachments, sourceViews: sourceViews, startIndex: index)
|
||||||
|
return gallery
|
||||||
|
}
|
||||||
|
|
||||||
|
func attachmentViewPresent(_ vc: UIViewController, animated: Bool) {
|
||||||
|
delegate?.present(vc, animated: animated)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
//
|
||||||
|
// StatusEditContentTextView.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 5/11/23.
|
||||||
|
// Copyright © 2023 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Pachyderm
|
||||||
|
import WebURL
|
||||||
|
|
||||||
|
class StatusEditContentTextView: ContentTextView {
|
||||||
|
|
||||||
|
func setTextFrom(edit: StatusEdit, index: Int) {
|
||||||
|
setTextFromHtml(edit.content)
|
||||||
|
setEmojis(edit.emojis, identifier: index)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mention links aren't included in the edit content, nothing else to do
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,208 @@
|
||||||
|
//
|
||||||
|
// StatusEditHistoryViewController.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 5/11/23.
|
||||||
|
// Copyright © 2023 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
class StatusEditHistoryViewController: UIViewController, CollectionViewController {
|
||||||
|
|
||||||
|
private let statusID: String
|
||||||
|
private let mastodonController: MastodonController
|
||||||
|
|
||||||
|
private var state = State.unloaded
|
||||||
|
|
||||||
|
private(set) var collectionView: UICollectionView!
|
||||||
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
|
init(statusID: String, mastodonController: MastodonController) {
|
||||||
|
self.statusID = statusID
|
||||||
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
|
||||||
|
title = "Edit History"
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
|
||||||
|
config.backgroundColor = .appGroupedBackground
|
||||||
|
config.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in
|
||||||
|
guard let item = self.dataSource.itemIdentifier(for: indexPath) else {
|
||||||
|
return sectionConfig
|
||||||
|
}
|
||||||
|
var config = sectionConfig
|
||||||
|
if item.hideSeparators {
|
||||||
|
config.topSeparatorVisibility = .hidden
|
||||||
|
config.bottomSeparatorVisibility = .hidden
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
|
||||||
|
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
|
section.contentInsetsReference = .readableContent
|
||||||
|
}
|
||||||
|
return section
|
||||||
|
}
|
||||||
|
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
|
collectionView.delegate = self
|
||||||
|
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(collectionView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
dataSource = createDataSource()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
||||||
|
let editCell = UICollectionView.CellRegistration<StatusEditCollectionViewCell, (StatusEdit, CollapseState, Int)> { [unowned self] cell, indexPath, itemIdentifier in
|
||||||
|
cell.delegate = self
|
||||||
|
cell.updateUI(edit: itemIdentifier.0, state: itemIdentifier.1, index: itemIdentifier.2)
|
||||||
|
}
|
||||||
|
let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, itemIdentifier in
|
||||||
|
cell.indicator.startAnimating()
|
||||||
|
}
|
||||||
|
return UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
|
||||||
|
switch itemIdentifier {
|
||||||
|
case .edit(let edit, let state, index: let index):
|
||||||
|
return collectionView.dequeueConfiguredReusableCell(using: editCell, for: indexPath, item: (edit, state, index))
|
||||||
|
case .loadingIndicator:
|
||||||
|
return collectionView.dequeueConfiguredReusableCell(using: loadingCell, for: indexPath, item: ())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
clearSelectionOnAppear(animated: animated)
|
||||||
|
|
||||||
|
if case .unloaded = state {
|
||||||
|
Task {
|
||||||
|
await load()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func load() async {
|
||||||
|
guard case .unloaded = state else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state = .loading
|
||||||
|
|
||||||
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
|
snapshot.appendSections([.edits])
|
||||||
|
snapshot.appendItems([.loadingIndicator])
|
||||||
|
await apply(snapshot)
|
||||||
|
|
||||||
|
let req = Status.history(statusID)
|
||||||
|
do {
|
||||||
|
let (edits, _) = try await mastodonController.run(req)
|
||||||
|
|
||||||
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
|
snapshot.appendSections([.edits])
|
||||||
|
// show them in reverse chronological order
|
||||||
|
snapshot.appendItems(edits.reversed().enumerated().map {
|
||||||
|
.edit($1, .unknown, index: $0)
|
||||||
|
})
|
||||||
|
await apply(snapshot)
|
||||||
|
|
||||||
|
state = .loaded
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
state = .unloaded
|
||||||
|
let config = ToastConfiguration(from: error, with: "Error Loading Edits", in: self) { [weak self] toast in
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
await self?.load()
|
||||||
|
}
|
||||||
|
self.showToast(configuration: config, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func apply(_ snapshot: NSDiffableDataSourceSnapshot<Section, Item>) async {
|
||||||
|
await MainActor.run {
|
||||||
|
self.dataSource.apply(snapshot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum State {
|
||||||
|
case unloaded
|
||||||
|
case loading
|
||||||
|
case loaded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StatusEditHistoryViewController {
|
||||||
|
enum Section {
|
||||||
|
case edits
|
||||||
|
}
|
||||||
|
enum Item: Hashable, Equatable {
|
||||||
|
case edit(StatusEdit, CollapseState, index: Int)
|
||||||
|
case loadingIndicator
|
||||||
|
|
||||||
|
static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||||
|
switch (lhs, rhs) {
|
||||||
|
case (.edit(_, _, let a), .edit(_, _, let b)):
|
||||||
|
return a == b
|
||||||
|
case (.loadingIndicator, .loadingIndicator):
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
switch self {
|
||||||
|
case .edit(_, _, index: let index):
|
||||||
|
hasher.combine(0)
|
||||||
|
hasher.combine(index)
|
||||||
|
case .loadingIndicator:
|
||||||
|
hasher.combine(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var hideSeparators: Bool {
|
||||||
|
switch self {
|
||||||
|
case .loadingIndicator:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StatusEditHistoryViewController: UICollectionViewDelegate {
|
||||||
|
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StatusEditHistoryViewController: TuskerNavigationDelegate {
|
||||||
|
nonisolated var apiController: MastodonController! { mastodonController }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StatusEditHistoryViewController: StatusEditCollectionViewCellDelegate {
|
||||||
|
func statusEditCellNeedsReconfigure(_ cell: StatusEditCollectionViewCell, animated: Bool, completion: (() -> Void)?) {
|
||||||
|
if let indexPath = collectionView.indexPath(for: cell) {
|
||||||
|
var snapshot = dataSource.snapshot()
|
||||||
|
snapshot.reconfigureItems([dataSource.itemIdentifier(for: indexPath)!])
|
||||||
|
dataSource.apply(snapshot, animatingDifferences: animated, completion: completion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
//
|
||||||
|
// StatusEditPollView.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 5/11/23.
|
||||||
|
// Copyright © 2023 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
class StatusEditPollView: UIStackView {
|
||||||
|
|
||||||
|
init() {
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
axis = .vertical
|
||||||
|
alignment = .leading
|
||||||
|
spacing = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
required init(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUI(poll: StatusEdit.Poll?, emojis: [Emoji]) {
|
||||||
|
arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||||
|
|
||||||
|
for option in poll?.options ?? [] {
|
||||||
|
// the edit poll doesn't actually include the multiple value
|
||||||
|
let icon = PollOptionCheckboxView(multiple: false)
|
||||||
|
icon.readOnly = false // this is a lie, but it's only used for stylistic changes
|
||||||
|
let label = EmojiLabel()
|
||||||
|
label.text = option.title
|
||||||
|
label.setEmojis(emojis, identifier: Optional<String>.none)
|
||||||
|
let stack = UIStackView(arrangedSubviews: [
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
])
|
||||||
|
stack.axis = .horizontal
|
||||||
|
stack.alignment = .center
|
||||||
|
stack.spacing = 8
|
||||||
|
addArrangedSubview(stack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -60,15 +60,15 @@ class AttachmentsContainerView: UIView {
|
||||||
|
|
||||||
// MARK: - User Interaface
|
// MARK: - User Interaface
|
||||||
|
|
||||||
func updateUI(status: StatusMO) {
|
func updateUI(attachments: [Attachment]) {
|
||||||
let showableAttachments = status.attachments.filter { AttachmentsContainerView.supportedAttachmentTypes.contains($0.kind) }
|
let attachments = attachments.filter { AttachmentsContainerView.supportedAttachmentTypes.contains($0.kind) }
|
||||||
let newTokens = showableAttachments.map { AttachmentToken(attachment: $0) }
|
let newTokens = attachments.map { AttachmentToken(attachment: $0) }
|
||||||
|
|
||||||
guard self.attachmentTokens != newTokens else {
|
guard self.attachmentTokens != newTokens else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.attachments = showableAttachments
|
self.attachments = attachments
|
||||||
self.attachmentTokens = newTokens
|
self.attachmentTokens = newTokens
|
||||||
|
|
||||||
attachmentViews.allObjects.forEach { $0.removeFromSuperview() }
|
attachmentViews.allObjects.forEach { $0.removeFromSuperview() }
|
||||||
|
|
|
@ -13,17 +13,18 @@ import WebURLFoundationExtras
|
||||||
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
|
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
|
||||||
|
|
||||||
protocol BaseEmojiLabel: AnyObject {
|
protocol BaseEmojiLabel: AnyObject {
|
||||||
var emojiIdentifier: String? { get set }
|
var emojiIdentifier: AnyHashable? { get set }
|
||||||
var emojiRequests: [ImageCache.Request] { get set }
|
var emojiRequests: [ImageCache.Request] { get set }
|
||||||
var emojiFont: UIFont { get }
|
var emojiFont: UIFont { get }
|
||||||
var emojiTextColor: UIColor { get }
|
var emojiTextColor: UIColor { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension BaseEmojiLabel {
|
extension BaseEmojiLabel {
|
||||||
func replaceEmojis(in attributedString: NSAttributedString, emojis: [Emoji], identifier: String?, setAttributedString: @escaping (_ attributedString: NSAttributedString, _ didReplaceEmojis: Bool) -> Void) {
|
func replaceEmojis<ID: Hashable>(in attributedString: NSAttributedString, emojis: [Emoji], identifier: ID?, setAttributedString: @escaping (_ attributedString: NSAttributedString, _ didReplaceEmojis: Bool) -> Void) {
|
||||||
// blergh
|
// blergh
|
||||||
precondition(Thread.isMainThread)
|
precondition(Thread.isMainThread)
|
||||||
|
|
||||||
|
let identifier = AnyHashable(identifier)
|
||||||
emojiIdentifier = identifier
|
emojiIdentifier = identifier
|
||||||
emojiRequests.forEach { $0.cancel() }
|
emojiRequests.forEach { $0.cancel() }
|
||||||
emojiRequests = []
|
emojiRequests = []
|
||||||
|
@ -138,7 +139,7 @@ extension BaseEmojiLabel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func replaceEmojis(in string: String, emojis: [Emoji], identifier: String?, setAttributedString: @escaping (_ attributedString: NSAttributedString, _ didReplaceEmojis: Bool) -> Void) {
|
func replaceEmojis<ID: Hashable>(in string: String, emojis: [Emoji], identifier: ID?, setAttributedString: @escaping (_ attributedString: NSAttributedString, _ didReplaceEmojis: Bool) -> Void) {
|
||||||
replaceEmojis(in: NSAttributedString(string: string), emojis: emojis, identifier: identifier, setAttributedString: setAttributedString)
|
replaceEmojis(in: NSAttributedString(string: string), emojis: emojis, identifier: identifier, setAttributedString: setAttributedString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
|
||||||
|
|
||||||
private(set) var hasEmojis = false
|
private(set) var hasEmojis = false
|
||||||
|
|
||||||
var emojiIdentifier: String?
|
var emojiIdentifier: AnyHashable?
|
||||||
var emojiRequests: [ImageCache.Request] = []
|
var emojiRequests: [ImageCache.Request] = []
|
||||||
var emojiFont: UIFont { defaultFont }
|
var emojiFont: UIFont { defaultFont }
|
||||||
var emojiTextColor: UIColor { defaultColor }
|
var emojiTextColor: UIColor { defaultColor }
|
||||||
|
@ -81,7 +81,7 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Emojis
|
// MARK: - Emojis
|
||||||
func setEmojis(_ emojis: [Emoji], identifier: String?) {
|
func setEmojis<ID: Hashable>(_ emojis: [Emoji], identifier: ID?) {
|
||||||
replaceEmojis(in: attributedText!, emojis: emojis, identifier: identifier) { attributedString, didReplaceEmojis in
|
replaceEmojis(in: attributedText!, emojis: emojis, identifier: identifier) { attributedString, didReplaceEmojis in
|
||||||
guard didReplaceEmojis else {
|
guard didReplaceEmojis else {
|
||||||
return
|
return
|
||||||
|
|
|
@ -13,16 +13,16 @@ class EmojiLabel: UILabel, BaseEmojiLabel {
|
||||||
|
|
||||||
private(set) var hasEmojis = false
|
private(set) var hasEmojis = false
|
||||||
|
|
||||||
var emojiIdentifier: String?
|
var emojiIdentifier: AnyHashable?
|
||||||
var emojiRequests: [ImageCache.Request] = []
|
var emojiRequests: [ImageCache.Request] = []
|
||||||
var emojiFont: UIFont { font }
|
var emojiFont: UIFont { font }
|
||||||
var emojiTextColor: UIColor { textColor }
|
var emojiTextColor: UIColor { textColor }
|
||||||
|
|
||||||
func setEmojis(_ emojis: [Emoji], identifier: String) {
|
func setEmojis<ID: Hashable>(_ emojis: [Emoji], identifier: ID?) {
|
||||||
guard emojis.count > 0, let attributedText = attributedText else { return }
|
guard emojis.count > 0, let attributedText = attributedText else { return }
|
||||||
|
|
||||||
replaceEmojis(in: attributedText, emojis: emojis, identifier: identifier) { [weak self] (newAttributedText, didReplaceEmojis) in
|
replaceEmojis(in: attributedText, emojis: emojis, identifier: identifier) { [weak self] (newAttributedText, didReplaceEmojis) in
|
||||||
guard let self = self, self.emojiIdentifier == identifier else { return }
|
guard let self = self, self.emojiIdentifier == AnyHashable(identifier) else { return }
|
||||||
self.hasEmojis = didReplaceEmojis
|
self.hasEmojis = didReplaceEmojis
|
||||||
self.attributedText = newAttributedText
|
self.attributedText = newAttributedText
|
||||||
self.setNeedsLayout()
|
self.setNeedsLayout()
|
||||||
|
|
|
@ -12,14 +12,14 @@ import Pachyderm
|
||||||
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
|
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
|
||||||
|
|
||||||
class MultiSourceEmojiLabel: UILabel, BaseEmojiLabel {
|
class MultiSourceEmojiLabel: UILabel, BaseEmojiLabel {
|
||||||
var emojiIdentifier: String?
|
var emojiIdentifier: AnyHashable?
|
||||||
var emojiRequests = [ImageCache.Request]()
|
var emojiRequests = [ImageCache.Request]()
|
||||||
var emojiFont: UIFont { font }
|
var emojiFont: UIFont { font }
|
||||||
var emojiTextColor: UIColor { textColor }
|
var emojiTextColor: UIColor { textColor }
|
||||||
|
|
||||||
var combiner: (([NSAttributedString]) -> NSAttributedString)?
|
var combiner: (([NSAttributedString]) -> NSAttributedString)?
|
||||||
|
|
||||||
func setEmojis(pairs: [(String, [Emoji])], identifier: String) {
|
func setEmojis<ID: Hashable>(pairs: [(String, [Emoji])], identifier: ID) {
|
||||||
guard pairs.count > 0 else { return }
|
guard pairs.count > 0 else { return }
|
||||||
|
|
||||||
self.emojiIdentifier = identifier
|
self.emojiIdentifier = identifier
|
||||||
|
@ -40,7 +40,7 @@ class MultiSourceEmojiLabel: UILabel, BaseEmojiLabel {
|
||||||
self.replaceEmojis(in: string, emojis: emojis, identifier: identifier) { (attributedString, _) in
|
self.replaceEmojis(in: string, emojis: emojis, identifier: identifier) { (attributedString, _) in
|
||||||
attributedStrings[index] = attributedString
|
attributedStrings[index] = attributedString
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
guard let self = self, self.emojiIdentifier == identifier else { return }
|
guard let self = self, self.emojiIdentifier == AnyHashable(identifier) else { return }
|
||||||
recombine()
|
recombine()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,6 +59,10 @@ class PollOptionCheckboxView: UIView {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override var intrinsicContentSize: CGSize {
|
||||||
|
CGSize(width: 20, height: 20)
|
||||||
|
}
|
||||||
|
|
||||||
private func updateStyle() {
|
private func updateStyle() {
|
||||||
imageView.isHidden = !isChecked
|
imageView.isHidden = !isChecked
|
||||||
if voted || readOnly {
|
if voted || readOnly {
|
||||||
|
|
|
@ -145,18 +145,23 @@ class StatusPollView: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func votePressed() {
|
@objc private func votePressed() {
|
||||||
|
guard let statusID,
|
||||||
|
let poll else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
optionsView.isEnabled = false
|
optionsView.isEnabled = false
|
||||||
voteButton.isEnabled = false
|
voteButton.isEnabled = false
|
||||||
voteButton.disabledTitle = "Voted"
|
voteButton.disabledTitle = "Voted"
|
||||||
|
|
||||||
UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
||||||
|
|
||||||
let request = Poll.vote(poll!.id, choices: optionsView.checkedOptionIndices)
|
let request = Poll.vote(poll.id, choices: optionsView.checkedOptionIndices)
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request) { (response) in
|
||||||
switch response {
|
switch response {
|
||||||
case let .failure(error):
|
case let .failure(error):
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.updateUI(status: self.mastodonController.persistentContainer.status(for: self.statusID)!, poll: self.poll)
|
self.updateUI(status: self.mastodonController.persistentContainer.status(for: statusID)!, poll: poll)
|
||||||
|
|
||||||
if let delegate = self.delegate {
|
if let delegate = self.delegate {
|
||||||
let config = ToastConfiguration(from: error, with: "Error Voting", in: delegate, retryAction: nil)
|
let config = ToastConfiguration(from: error, with: "Error Voting", in: delegate, retryAction: nil)
|
||||||
|
@ -167,7 +172,7 @@ class StatusPollView: UIView {
|
||||||
case let .success(poll, _):
|
case let .success(poll, _):
|
||||||
let container = self.mastodonController.persistentContainer
|
let container = self.mastodonController.persistentContainer
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
guard let status = container.status(for: self.statusID) else {
|
guard let status = container.status(for: statusID) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
status.poll = poll
|
status.poll = poll
|
||||||
|
|
|
@ -116,7 +116,7 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
||||||
$0.addTarget(self, action: #selector(collapseButtonPressed), for: .touchUpInside)
|
$0.addTarget(self, action: #selector(collapseButtonPressed), for: .touchUpInside)
|
||||||
}
|
}
|
||||||
|
|
||||||
let contentContainer = StatusContentContainer(useTopSpacer: true).configure {
|
let contentContainer = StatusContentContainer<StatusContentTextView, StatusPollView>(useTopSpacer: true).configure {
|
||||||
$0.contentTextView.defaultFont = ConversationMainStatusCollectionViewCell.contentFont
|
$0.contentTextView.defaultFont = ConversationMainStatusCollectionViewCell.contentFont
|
||||||
$0.contentTextView.monospaceFont = ConversationMainStatusCollectionViewCell.monospaceFont
|
$0.contentTextView.monospaceFont = ConversationMainStatusCollectionViewCell.monospaceFont
|
||||||
$0.contentTextView.paragraphStyle = ConversationMainStatusCollectionViewCell.contentParagraphStyle
|
$0.contentTextView.paragraphStyle = ConversationMainStatusCollectionViewCell.contentParagraphStyle
|
||||||
|
@ -154,8 +154,9 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
||||||
$0.adjustsFontForContentSizeCategory = true
|
$0.adjustsFontForContentSizeCategory = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private let editTimestampButton = UIButton().configure {
|
private lazy var editTimestampButton = UIButton().configure {
|
||||||
$0.titleLabel!.adjustsFontForContentSizeCategory = true
|
$0.titleLabel!.adjustsFontForContentSizeCategory = true
|
||||||
|
$0.addTarget(self, action: #selector(editTimestampPressed), for: .touchUpInside)
|
||||||
$0.isPointerInteractionEnabled = true
|
$0.isPointerInteractionEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -454,6 +455,10 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
||||||
toggleReblog()
|
toggleReblog()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func editTimestampPressed() {
|
||||||
|
delegate?.show(StatusEditHistoryViewController(statusID: statusID, mastodonController: mastodonController), sender: nil)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ConversationMainStatusAccountDetailAccessibilityElement: UIAccessibilityElement {
|
private class ConversationMainStatusAccountDetailAccessibilityElement: UIAccessibilityElement {
|
||||||
|
|
|
@ -24,7 +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 replyButton: UIButton { get }
|
var replyButton: UIButton { get }
|
||||||
var favoriteButton: UIButton { get }
|
var favoriteButton: UIButton { get }
|
||||||
var reblogButton: UIButton { get }
|
var reblogButton: UIButton { get }
|
||||||
|
@ -83,7 +83,7 @@ extension StatusCollectionViewCell {
|
||||||
contentContainer.contentTextView.setTextFrom(status: status, precomputed: precomputedContent)
|
contentContainer.contentTextView.setTextFrom(status: status, precomputed: precomputedContent)
|
||||||
contentContainer.contentTextView.navigationDelegate = delegate
|
contentContainer.contentTextView.navigationDelegate = delegate
|
||||||
contentContainer.attachmentsView.delegate = self
|
contentContainer.attachmentsView.delegate = self
|
||||||
contentContainer.attachmentsView.updateUI(status: status)
|
contentContainer.attachmentsView.updateUI(attachments: status.attachments)
|
||||||
contentContainer.pollView.isHidden = status.poll == nil
|
contentContainer.pollView.isHidden = status.poll == nil
|
||||||
contentContainer.pollView.mastodonController = mastodonController
|
contentContainer.pollView.mastodonController = mastodonController
|
||||||
contentContainer.pollView.delegate = delegate
|
contentContainer.pollView.delegate = delegate
|
||||||
|
@ -129,10 +129,12 @@ 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) ?? [])
|
||||||
|
|
||||||
if statusState.unknown {
|
let didResolve = statusState.resolveFor(status: status) {
|
||||||
// layout so that we can take the content height into consideration when deciding whether to collapse
|
// layout so that we can take the content height into consideration when deciding whether to collapse
|
||||||
layoutIfNeeded()
|
layoutIfNeeded()
|
||||||
statusState.resolveFor(status: status, height: contentContainer.visibleSubviewHeight)
|
return contentContainer.visibleSubviewHeight
|
||||||
|
}
|
||||||
|
if didResolve {
|
||||||
if statusState.collapsible! && showStatusAutomatically {
|
if statusState.collapsible! && showStatusAutomatically {
|
||||||
statusState.collapsed = false
|
statusState.collapsed = false
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class StatusContentContainer: UIView {
|
class StatusContentContainer<ContentView: ContentTextView, PollView: UIView>: UIView {
|
||||||
|
|
||||||
private var useTopSpacer = false
|
private var useTopSpacer = false
|
||||||
private let topSpacer = UIView().configure {
|
private let topSpacer = UIView().configure {
|
||||||
|
@ -17,7 +17,7 @@ class StatusContentContainer: UIView {
|
||||||
$0.heightAnchor.constraint(equalToConstant: 4).isActive = true
|
$0.heightAnchor.constraint(equalToConstant: 4).isActive = true
|
||||||
}
|
}
|
||||||
|
|
||||||
let contentTextView = StatusContentTextView().configure {
|
let contentTextView = ContentView().configure {
|
||||||
$0.adjustsFontForContentSizeCategory = true
|
$0.adjustsFontForContentSizeCategory = true
|
||||||
$0.isScrollEnabled = false
|
$0.isScrollEnabled = false
|
||||||
$0.backgroundColor = nil
|
$0.backgroundColor = nil
|
||||||
|
@ -33,7 +33,7 @@ class StatusContentContainer: UIView {
|
||||||
|
|
||||||
let attachmentsView = AttachmentsContainerView()
|
let attachmentsView = AttachmentsContainerView()
|
||||||
|
|
||||||
let pollView = StatusPollView()
|
let pollView = PollView()
|
||||||
|
|
||||||
private var arrangedSubviews: [UIView] {
|
private var arrangedSubviews: [UIView] {
|
||||||
if useTopSpacer {
|
if useTopSpacer {
|
||||||
|
|
|
@ -178,7 +178,7 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
$0.addTarget(self, action: #selector(collapseButtonPressed), for: .touchUpInside)
|
$0.addTarget(self, action: #selector(collapseButtonPressed), for: .touchUpInside)
|
||||||
}
|
}
|
||||||
|
|
||||||
let contentContainer = StatusContentContainer(useTopSpacer: false).configure {
|
let contentContainer = StatusContentContainer<StatusContentTextView, StatusPollView>(useTopSpacer: false).configure {
|
||||||
$0.contentTextView.defaultFont = TimelineStatusCollectionViewCell.contentFont
|
$0.contentTextView.defaultFont = TimelineStatusCollectionViewCell.contentFont
|
||||||
$0.contentTextView.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont
|
$0.contentTextView.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont
|
||||||
$0.contentTextView.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle
|
$0.contentTextView.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle
|
||||||
|
|
Loading…
Reference in New Issue