Compare commits
7 Commits
ae8191ca0e
...
b37e5fffbf
Author | SHA1 | Date |
---|---|---|
Shadowfacts | b37e5fffbf | |
Shadowfacts | 8c27a9368f | |
Shadowfacts | 735659dee6 | |
Shadowfacts | bf02b185ed | |
Shadowfacts | 4ccf5d21a4 | |
Shadowfacts | 9ac1c43511 | |
Shadowfacts | 76b9496fe6 |
|
@ -24,7 +24,9 @@ public final class CollapseState: Sendable {
|
|||
}
|
||||
|
||||
public func copy() -> CollapseState {
|
||||
return CollapseState(collapsible: self.collapsible, collapsed: self.collapsed)
|
||||
let new = CollapseState(collapsible: self.collapsible, collapsed: self.collapsed)
|
||||
new.statusPropertiesHash = self.statusPropertiesHash
|
||||
return new
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
|
|
|
@ -301,6 +301,7 @@
|
|||
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 */; };
|
||||
D6D79F532A0FFE3200AB2315 /* ToggleableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F522A0FFE3200AB2315 /* ToggleableButton.swift */; };
|
||||
D6D94955298963A900C59229 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D94954298963A900C59229 /* Colors.swift */; };
|
||||
D6D9498D298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498C298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift */; };
|
||||
D6D9498F298EB79400C59229 /* CopyableLable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498E298EB79400C59229 /* CopyableLable.swift */; };
|
||||
|
@ -700,6 +701,7 @@
|
|||
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>"; };
|
||||
D6D79F522A0FFE3200AB2315 /* ToggleableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleableButton.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>"; };
|
||||
D6D9498E298EB79400C59229 /* CopyableLable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableLable.swift; sourceTree = "<group>"; };
|
||||
|
@ -1371,6 +1373,7 @@
|
|||
D65B4B69297777D900DABDFB /* StatusNotFoundView.swift */,
|
||||
04ED00B021481ED800567C53 /* SteppedProgressView.swift */,
|
||||
D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */,
|
||||
D6D79F522A0FFE3200AB2315 /* ToggleableButton.swift */,
|
||||
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */,
|
||||
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */,
|
||||
D6A3BC872321F78000FD64D5 /* Account Cell */,
|
||||
|
@ -1945,6 +1948,7 @@
|
|||
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */,
|
||||
D6ADB6EC28EA73CB009924AB /* StatusContentContainer.swift in Sources */,
|
||||
D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */,
|
||||
D6D79F532A0FFE3200AB2315 /* ToggleableButton.swift in Sources */,
|
||||
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */,
|
||||
D6ADB6F028ED1F25009924AB /* CachedImageView.swift in Sources */,
|
||||
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */,
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
wasCreatedForAppExtension = "YES"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D6A4531229EF64BA00032932"
|
||||
BuildableName = "ShareExtension.appex"
|
||||
BlueprintName = "ShareExtension"
|
||||
ReferencedContainer = "container:Tusker.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D6D4DDCB212518A000E1C4BB"
|
||||
BuildableName = "Tusker.app"
|
||||
BlueprintName = "Tusker"
|
||||
ReferencedContainer = "container:Tusker.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "1"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D6A4531229EF64BA00032932"
|
||||
BuildableName = "ShareExtension.appex"
|
||||
BlueprintName = "ShareExtension"
|
||||
ReferencedContainer = "container:Tusker.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D6D4DDCB212518A000E1C4BB"
|
||||
BuildableName = "Tusker.app"
|
||||
BlueprintName = "Tusker"
|
||||
ReferencedContainer = "container:Tusker.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
<AdditionalOption
|
||||
key = "MallocStackLogging"
|
||||
value = ""
|
||||
isEnabled = "YES">
|
||||
</AdditionalOption>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
askForAppToLaunch = "Yes"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
<RemoteRunnable
|
||||
runnableDebuggingMode = "1"
|
||||
BundleIdentifier = "com.apple.mobileslideshow"
|
||||
RemotePath = "/Applications/MobileSlideShow.app">
|
||||
</RemoteRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
|
@ -88,6 +88,10 @@
|
|||
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "-com.apple.CoreData.CloudKitDebug 0"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "-UIFocusLoggingEnabled YES"
|
||||
isEnabled = "NO">
|
||||
|
|
|
@ -19,7 +19,7 @@ extension StatusEdit: CollapseStateResolving {}
|
|||
extension CollapseState {
|
||||
|
||||
func resolveFor(status: CollapseStateResolving, height: () -> CGFloat, textLength: Int? = nil) -> Bool {
|
||||
lazy var newHash = hashStatusProperties(status: status)
|
||||
let newHash = hashStatusProperties(status: status)
|
||||
guard unknown || statusPropertiesHash != newHash else {
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ protocol Configurable {
|
|||
func configure(_ closure: (T) -> Void) -> T
|
||||
}
|
||||
extension Configurable where Self: UIView {
|
||||
@inline(__always)
|
||||
func configure(_ closure: (Self) -> Void) -> Self {
|
||||
closure(self)
|
||||
return self
|
||||
|
|
|
@ -117,8 +117,8 @@ class StatusEditCollectionViewCell: UICollectionViewListCell {
|
|||
}
|
||||
|
||||
_ = state.resolveFor(status: edit, height: {
|
||||
layoutIfNeeded()
|
||||
return contentContainer.visibleSubviewHeight
|
||||
let width = self.bounds.width - 2*16
|
||||
return contentContainer.estimateVisibleSubviewHeight(effectiveWidth: width)
|
||||
})
|
||||
collapseButton.isHidden = !state.collapsible!
|
||||
contentContainer.setCollapsed(state.collapsed!)
|
||||
|
|
|
@ -9,7 +9,9 @@
|
|||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
class StatusEditPollView: UIStackView {
|
||||
class StatusEditPollView: UIStackView, StatusContentPollView {
|
||||
|
||||
private var titleLabels: [EmojiLabel] = []
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
|
@ -25,6 +27,7 @@ class StatusEditPollView: UIStackView {
|
|||
|
||||
func updateUI(poll: StatusEdit.Poll?, emojis: [Emoji]) {
|
||||
arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
titleLabels = []
|
||||
|
||||
for option in poll?.options ?? [] {
|
||||
// the edit poll doesn't actually include the multiple value
|
||||
|
@ -33,6 +36,7 @@ class StatusEditPollView: UIStackView {
|
|||
let label = EmojiLabel()
|
||||
label.text = option.title
|
||||
label.setEmojis(emojis, identifier: Optional<String>.none)
|
||||
titleLabels.append(label)
|
||||
let stack = UIStackView(arrangedSubviews: [
|
||||
icon,
|
||||
label,
|
||||
|
@ -44,4 +48,14 @@ class StatusEditPollView: UIStackView {
|
|||
}
|
||||
}
|
||||
|
||||
func estimateHeight(effectiveWidth: CGFloat) -> CGFloat {
|
||||
var height: CGFloat = 0
|
||||
height += CGFloat(arrangedSubviews.count - 1) * 4
|
||||
let labelWidth = effectiveWidth /* checkbox size: */ - 20 /* spacing: */ - 8
|
||||
for titleLabel in titleLabels {
|
||||
height += titleLabel.sizeThatFits(CGSize(width: labelWidth, height: UIView.layoutFittingCompressedSize.height)).height
|
||||
}
|
||||
return height
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -22,6 +22,15 @@ class AttachmentsContainerView: UIView {
|
|||
let attachmentStacks: NSHashTable<UIStackView> = .weakObjects()
|
||||
var moreView: UIView?
|
||||
private var aspectRatioConstraint: NSLayoutConstraint?
|
||||
private(set) var aspectRatio: CGFloat = 16/9 {
|
||||
didSet {
|
||||
if aspectRatio != aspectRatioConstraint?.multiplier {
|
||||
aspectRatioConstraint?.isActive = false
|
||||
aspectRatioConstraint = widthAnchor.constraint(equalTo: heightAnchor, multiplier: aspectRatio)
|
||||
aspectRatioConstraint!.isActive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var blurView: UIVisualEffectView?
|
||||
var hideButtonView: UIVisualEffectView?
|
||||
|
@ -93,7 +102,8 @@ class AttachmentsContainerView: UIView {
|
|||
fillView(attachmentView)
|
||||
sendSubviewToBack(attachmentView)
|
||||
accessibilityElements.append(attachmentView)
|
||||
if let attachmentAspectRatio = attachmentView.attachmentAspectRatio {
|
||||
if Preferences.shared.showUncroppedMediaInline,
|
||||
let attachmentAspectRatio = attachmentView.attachmentAspectRatio {
|
||||
aspectRatio = attachmentAspectRatio
|
||||
}
|
||||
case 2:
|
||||
|
@ -266,18 +276,7 @@ class AttachmentsContainerView: UIView {
|
|||
accessibilityElements.append(moreView)
|
||||
}
|
||||
|
||||
if Preferences.shared.showUncroppedMediaInline {
|
||||
if aspectRatioConstraint?.multiplier != aspectRatio {
|
||||
aspectRatioConstraint?.isActive = false
|
||||
aspectRatioConstraint = widthAnchor.constraint(equalTo: heightAnchor, multiplier: aspectRatio)
|
||||
aspectRatioConstraint!.isActive = true
|
||||
}
|
||||
} else {
|
||||
if aspectRatioConstraint == nil {
|
||||
aspectRatioConstraint = widthAnchor.constraint(equalTo: heightAnchor, multiplier: 16/9)
|
||||
aspectRatioConstraint!.isActive = true
|
||||
}
|
||||
}
|
||||
self.aspectRatio = aspectRatio
|
||||
} else {
|
||||
self.isHidden = true
|
||||
}
|
||||
|
|
|
@ -11,24 +11,27 @@ import Pachyderm
|
|||
|
||||
class PollOptionView: UIView {
|
||||
|
||||
private let unselectedBackgroundColor = UIColor(white: 0.5, alpha: 0.25)
|
||||
private static let unselectedBackgroundColor = UIColor(white: 0.5, alpha: 0.25)
|
||||
|
||||
let checkbox: PollOptionCheckboxView
|
||||
private(set) var label: EmojiLabel!
|
||||
private(set) var checkbox: PollOptionCheckboxView?
|
||||
|
||||
init(poll: Poll, option: Poll.Option, mastodonController: MastodonController) {
|
||||
checkbox = PollOptionCheckboxView(multiple: poll.multiple)
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
let minHeight: CGFloat = 35
|
||||
layer.cornerRadius = 0.1 * minHeight
|
||||
layer.cornerCurve = .continuous
|
||||
backgroundColor = unselectedBackgroundColor
|
||||
backgroundColor = PollOptionView.unselectedBackgroundColor
|
||||
|
||||
checkbox.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(checkbox)
|
||||
let showCheckbox = poll.ownVotes?.isEmpty == false || !poll.effectiveExpired
|
||||
if showCheckbox {
|
||||
checkbox = PollOptionCheckboxView(multiple: poll.multiple)
|
||||
checkbox!.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(checkbox!)
|
||||
}
|
||||
|
||||
let label = EmojiLabel()
|
||||
label = EmojiLabel()
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
label.numberOfLines = 0
|
||||
label.font = .preferredFont(forTextStyle: .callout)
|
||||
|
@ -88,12 +91,8 @@ class PollOptionView: UIView {
|
|||
NSLayoutConstraint.activate([
|
||||
minHeightConstraint,
|
||||
|
||||
checkbox.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
checkbox.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
|
||||
|
||||
label.topAnchor.constraint(equalTo: topAnchor, constant: 4),
|
||||
label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4),
|
||||
label.leadingAnchor.constraint(equalTo: checkbox.trailingAnchor, constant: 8),
|
||||
label.trailingAnchor.constraint(equalTo: percentLabel.leadingAnchor, constant: -4),
|
||||
|
||||
percentLabel.topAnchor.constraint(equalTo: topAnchor),
|
||||
|
@ -101,6 +100,16 @@ class PollOptionView: UIView {
|
|||
percentLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
|
||||
])
|
||||
|
||||
if let checkbox {
|
||||
NSLayoutConstraint.activate([
|
||||
checkbox.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
checkbox.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
|
||||
label.leadingAnchor.constraint(equalTo: checkbox.trailingAnchor, constant: 8),
|
||||
])
|
||||
} else {
|
||||
label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8).isActive = true
|
||||
}
|
||||
|
||||
isAccessibilityElement = true
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ class PollOptionsView: UIControl {
|
|||
var mastodonController: MastodonController!
|
||||
|
||||
var checkedOptionIndices: [Int] {
|
||||
options.enumerated().filter { $0.element.checkbox.isChecked }.map(\.offset)
|
||||
options.enumerated().filter { $0.element.checkbox?.isChecked == true }.map(\.offset)
|
||||
}
|
||||
var checkedOptionsChanged: (() -> Void)?
|
||||
|
||||
|
@ -32,7 +32,7 @@ class PollOptionsView: UIControl {
|
|||
|
||||
override var isEnabled: Bool {
|
||||
didSet {
|
||||
options.forEach { $0.checkbox.readOnly = !isEnabled }
|
||||
options.forEach { $0.checkbox?.readOnly = !isEnabled }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,9 +65,11 @@ class PollOptionsView: UIControl {
|
|||
|
||||
options = poll.options.enumerated().map { (index, opt) in
|
||||
let optionView = PollOptionView(poll: poll, option: opt, mastodonController: mastodonController)
|
||||
optionView.checkbox.readOnly = !isEnabled
|
||||
optionView.checkbox.isChecked = poll.ownVotes?.contains(index) ?? false
|
||||
optionView.checkbox.voted = poll.voted ?? false
|
||||
if let checkbox = optionView.checkbox {
|
||||
checkbox.readOnly = !isEnabled
|
||||
checkbox.isChecked = poll.ownVotes?.contains(index) ?? false
|
||||
checkbox.voted = poll.voted ?? false
|
||||
}
|
||||
stack.addArrangedSubview(optionView)
|
||||
return optionView
|
||||
}
|
||||
|
@ -75,15 +77,25 @@ class PollOptionsView: UIControl {
|
|||
accessibilityElements = options
|
||||
}
|
||||
|
||||
func estimateHeight(effectiveWidth: CGFloat) -> CGFloat {
|
||||
var height: CGFloat = 0
|
||||
height += CGFloat(options.count - 1) * stack.spacing
|
||||
for option in options {
|
||||
// this isn't the actual width, but it's close enough for the estimate
|
||||
height += option.label.sizeThatFits(CGSize(width: effectiveWidth, height: UIView.layoutFittingCompressedSize.height)).height
|
||||
}
|
||||
return height
|
||||
}
|
||||
|
||||
private func selectOption(_ option: PollOptionView) {
|
||||
if poll.multiple {
|
||||
option.checkbox.isChecked.toggle()
|
||||
option.checkbox?.isChecked.toggle()
|
||||
} else {
|
||||
for opt in options {
|
||||
if opt === option {
|
||||
opt.checkbox.isChecked = true
|
||||
opt.checkbox?.isChecked = true
|
||||
} else {
|
||||
opt.checkbox.isChecked = false
|
||||
opt.checkbox?.isChecked = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
class StatusPollView: UIView {
|
||||
class StatusPollView: UIView, StatusContentPollView {
|
||||
|
||||
private static let formatter: DateComponentsFormatter = {
|
||||
let f = DateComponentsFormatter()
|
||||
|
@ -140,6 +140,11 @@ class StatusPollView: UIView {
|
|||
voteButton.isEnabled = false
|
||||
}
|
||||
|
||||
func estimateHeight(effectiveWidth: CGFloat) -> CGFloat {
|
||||
guard let poll else { return 0 }
|
||||
return optionsView.estimateHeight(effectiveWidth: effectiveWidth) + infoLabel.sizeThatFits(UIView.layoutFittingExpandedSize).height
|
||||
}
|
||||
|
||||
private func checkedOptionsChanged() {
|
||||
voteButton.isEnabled = optionsView.checkedOptionIndices.count > 0
|
||||
}
|
||||
|
|
|
@ -191,13 +191,13 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
|||
$0.addInteraction(UIPointerInteraction(delegate: self))
|
||||
}
|
||||
|
||||
private(set) lazy var favoriteButton = UIButton().configure {
|
||||
private(set) lazy var favoriteButton = ToggleableButton(activeColor: UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1)).configure {
|
||||
$0.setImage(UIImage(systemName: "star.fill"), for: .normal)
|
||||
$0.addTarget(self, action: #selector(favoritePressed), for: .touchUpInside)
|
||||
$0.addInteraction(UIPointerInteraction(delegate: self))
|
||||
}
|
||||
|
||||
private(set) lazy var reblogButton = UIButton().configure {
|
||||
private(set) lazy var reblogButton = ToggleableButton(activeColor: UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1)).configure {
|
||||
$0.setImage(UIImage(systemName: "repeat"), for: .normal)
|
||||
$0.addTarget(self, action: #selector(reblogPressed), for: .touchUpInside)
|
||||
$0.addInteraction(UIPointerInteraction(delegate: self))
|
||||
|
@ -348,24 +348,6 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
|||
accountDetailAccessibilityElement.navigationDelegate = delegate
|
||||
accountDetailAccessibilityElement.accountID = accountID
|
||||
|
||||
let metaButtonAttributes = AttributeContainer([
|
||||
.font: ConversationMainStatusCollectionViewCell.metaFont
|
||||
])
|
||||
|
||||
let favoritesFormat = NSLocalizedString("favorites count", comment: "conv main status favorites button label")
|
||||
var favoritesConfig = UIButton.Configuration.plain()
|
||||
favoritesConfig.baseForegroundColor = .secondaryLabel
|
||||
favoritesConfig.attributedTitle = AttributedString(String.localizedStringWithFormat(favoritesFormat, status.favouritesCount), attributes: metaButtonAttributes)
|
||||
favoritesConfig.contentInsets = .zero
|
||||
favoritesCountButton.configuration = favoritesConfig
|
||||
|
||||
let reblogsFormat = NSLocalizedString("reblogs count", comment: "conv main status reblogs button label")
|
||||
var reblogsConfig = UIButton.Configuration.plain()
|
||||
reblogsConfig.baseForegroundColor = .secondaryLabel
|
||||
reblogsConfig.attributedTitle = AttributedString(String.localizedStringWithFormat(reblogsFormat, status.reblogsCount), attributes: metaButtonAttributes)
|
||||
reblogsConfig.contentInsets = .zero
|
||||
reblogsCountButton.configuration = reblogsConfig
|
||||
|
||||
var timestampAndClientText = ConversationMainStatusCollectionViewCell.dateFormatter.string(from: status.createdAt)
|
||||
if let application = status.applicationName {
|
||||
timestampAndClientText += " • \(application)"
|
||||
|
@ -376,7 +358,9 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
|||
editTimestampButton.isHidden = false
|
||||
var config = UIButton.Configuration.plain()
|
||||
config.baseForegroundColor = .secondaryLabel
|
||||
config.attributedTitle = AttributedString("Edited on \(ConversationMainStatusCollectionViewCell.dateFormatter.string(from: editedAt))", attributes: metaButtonAttributes)
|
||||
config.attributedTitle = AttributedString("Edited on \(ConversationMainStatusCollectionViewCell.dateFormatter.string(from: editedAt))", attributes: AttributeContainer([
|
||||
.font: ConversationMainStatusCollectionViewCell.metaFont
|
||||
]))
|
||||
config.contentInsets = .zero
|
||||
editTimestampButton.configuration = config
|
||||
} else {
|
||||
|
@ -392,6 +376,33 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
|||
baseCreateObservers()
|
||||
}
|
||||
|
||||
func updateStatusState(status: StatusMO) {
|
||||
baseUpdateStatusState(status: status)
|
||||
|
||||
let attributes = AttributeContainer([
|
||||
.font: ConversationMainStatusCollectionViewCell.metaFont
|
||||
])
|
||||
|
||||
let favoritesFormat = NSLocalizedString("favorites count", comment: "conv main status favorites button label")
|
||||
var favoritesConfig = UIButton.Configuration.plain()
|
||||
favoritesConfig.baseForegroundColor = .secondaryLabel
|
||||
favoritesConfig.attributedTitle = AttributedString(String.localizedStringWithFormat(favoritesFormat, status.favouritesCount), attributes: attributes)
|
||||
favoritesConfig.contentInsets = .zero
|
||||
favoritesCountButton.configuration = favoritesConfig
|
||||
|
||||
let reblogsFormat = NSLocalizedString("reblogs count", comment: "conv main status reblogs button label")
|
||||
var reblogsConfig = UIButton.Configuration.plain()
|
||||
reblogsConfig.baseForegroundColor = .secondaryLabel
|
||||
reblogsConfig.attributedTitle = AttributedString(String.localizedStringWithFormat(reblogsFormat, status.reblogsCount), attributes: attributes)
|
||||
reblogsConfig.contentInsets = .zero
|
||||
reblogsCountButton.configuration = reblogsConfig
|
||||
}
|
||||
|
||||
func estimateContentHeight() -> CGFloat {
|
||||
let width = bounds.width - 2*16
|
||||
return contentContainer.estimateVisibleSubviewHeight(effectiveWidth: width)
|
||||
}
|
||||
|
||||
func updateUIForPreferences(status: StatusMO) {
|
||||
baseUpdateUIForPreferences(status: status)
|
||||
}
|
||||
|
|
|
@ -26,8 +26,8 @@ protocol StatusCollectionViewCell: UICollectionViewCell, AttachmentViewDelegate
|
|||
var collapseButton: StatusCollapseButton { get }
|
||||
var contentContainer: StatusContentContainer<StatusContentTextView, StatusPollView> { get }
|
||||
var replyButton: UIButton { get }
|
||||
var favoriteButton: UIButton { get }
|
||||
var reblogButton: UIButton { get }
|
||||
var favoriteButton: ToggleableButton { get }
|
||||
var reblogButton: ToggleableButton { get }
|
||||
var moreButton: UIButton { get }
|
||||
var prevThreadLinkView: UIView? { get set }
|
||||
var nextThreadLinkView: UIView? { get set }
|
||||
|
@ -45,6 +45,8 @@ protocol StatusCollectionViewCell: UICollectionViewCell, AttachmentViewDelegate
|
|||
var cancellables: Set<AnyCancellable> { get set }
|
||||
|
||||
func updateUIForPreferences(status: StatusMO)
|
||||
func updateStatusState(status: StatusMO)
|
||||
func estimateContentHeight() -> CGFloat
|
||||
}
|
||||
|
||||
// MARK: UI Configuration
|
||||
|
@ -58,7 +60,13 @@ extension StatusCollectionViewCell {
|
|||
.receive(on: DispatchQueue.main)
|
||||
.filter { [unowned self] in $0 == self.statusID }
|
||||
.sink { [unowned self] _ in
|
||||
self.delegate?.statusCellNeedsReconfigure(self, animated: true, completion: nil)
|
||||
if let status = self.mastodonController.persistentContainer.status(for: self.statusID) {
|
||||
// update immediately w/o animation
|
||||
self.favoriteButton.active = status.favourited
|
||||
self.reblogButton.active = status.reblogged
|
||||
|
||||
self.delegate?.statusCellNeedsReconfigure(self, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
|
@ -98,6 +106,7 @@ extension StatusCollectionViewCell {
|
|||
}
|
||||
|
||||
updateUIForPreferences(status: status)
|
||||
updateStatusState(status: status)
|
||||
|
||||
contentWarningLabel.text = status.spoilerText
|
||||
contentWarningLabel.isHidden = status.spoilerText.isEmpty
|
||||
|
@ -105,35 +114,17 @@ extension StatusCollectionViewCell {
|
|||
contentWarningLabel.setEmojis(status.emojis, identifier: statusID)
|
||||
}
|
||||
|
||||
replyButton.isEnabled = mastodonController.loggedIn
|
||||
|
||||
favoriteButton.isEnabled = mastodonController.loggedIn
|
||||
if status.favourited {
|
||||
favoriteButton.tintColor = UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1)
|
||||
favoriteButton.accessibilityLabel = NSLocalizedString("Undo Favorite", comment: "undo favorite button accessibility label")
|
||||
} else {
|
||||
favoriteButton.tintColor = nil
|
||||
favoriteButton.accessibilityLabel = NSLocalizedString("Favorite", comment: "favorite button accessibility label")
|
||||
}
|
||||
|
||||
reblogButton.isEnabled = reblogEnabled(status: status)
|
||||
if status.reblogged {
|
||||
reblogButton.tintColor = UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1)
|
||||
reblogButton.accessibilityLabel = NSLocalizedString("Undo Reblog", comment: "undo reblog button accessibility label")
|
||||
} else {
|
||||
reblogButton.tintColor = nil
|
||||
reblogButton.accessibilityLabel = NSLocalizedString("Reblog", comment: "reblog button accessibility label")
|
||||
}
|
||||
replyButton.isEnabled = mastodonController.loggedIn
|
||||
favoriteButton.isEnabled = mastodonController.loggedIn
|
||||
|
||||
// keep menu in sync with changed states e.g. bookmarked, muted
|
||||
// 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) ?? [])
|
||||
|
||||
let didResolve = statusState.resolveFor(status: status) {
|
||||
// layout so that we can take the content height into consideration when deciding whether to collapse
|
||||
layoutIfNeeded()
|
||||
return contentContainer.visibleSubviewHeight
|
||||
}
|
||||
let didResolve = statusState.resolveFor(status: status, height: self.estimateContentHeight)
|
||||
// let didResolve = statusState.resolveFor(status: status) {
|
||||
//// // layout so that we can take the content height into consideration when deciding whether to collapse
|
||||
//// layoutIfNeeded()
|
||||
//// return contentContainer.visibleSubviewHeight
|
||||
// return contentContainer.estimateVisibleSubviewHeight(effectiveWidth: )
|
||||
// }
|
||||
if didResolve {
|
||||
if statusState.collapsible! && showStatusAutomatically {
|
||||
statusState.collapsed = false
|
||||
|
@ -157,7 +148,9 @@ extension StatusCollectionViewCell {
|
|||
guard mastodonController.loggedIn else {
|
||||
return false
|
||||
}
|
||||
if status.visibility == .direct || status.visibility == .private {
|
||||
if status.visibility == .direct {
|
||||
return false
|
||||
} else if status.visibility == .private {
|
||||
if mastodonController.instanceFeatures.boostToOriginalAudience,
|
||||
status.account.id == mastodonController.account?.id {
|
||||
return true
|
||||
|
@ -212,6 +205,30 @@ extension StatusCollectionViewCell {
|
|||
displayNameLabel.updateForAccountDisplayName(account: status.account)
|
||||
}
|
||||
|
||||
func baseUpdateStatusState(status: StatusMO) {
|
||||
favoriteButton.active = status.favourited
|
||||
if status.favourited {
|
||||
favoriteButton.accessibilityLabel = NSLocalizedString("Undo Favorite", comment: "undo favorite button accessibility label")
|
||||
} else {
|
||||
favoriteButton.accessibilityLabel = NSLocalizedString("Favorite", comment: "favorite button accessibility label")
|
||||
}
|
||||
reblogButton.active = status.reblogged
|
||||
if status.reblogged {
|
||||
reblogButton.accessibilityLabel = NSLocalizedString("Undo Reblog", comment: "undo reblog button accessibility label")
|
||||
} else {
|
||||
reblogButton.accessibilityLabel = NSLocalizedString("Reblog", comment: "reblog button accessibility label")
|
||||
}
|
||||
|
||||
// keep menu in sync with changed states e.g. bookmarked, muted
|
||||
// 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) ?? [])
|
||||
|
||||
contentContainer.pollView.isHidden = status.poll == nil
|
||||
contentContainer.pollView.mastodonController = mastodonController
|
||||
contentContainer.pollView.delegate = delegate
|
||||
contentContainer.pollView.updateUI(status: status, poll: status.poll)
|
||||
}
|
||||
|
||||
func setShowThreadLinks(prev: Bool, next: Bool) {
|
||||
if prev {
|
||||
if let prevThreadLinkView {
|
||||
|
|
|
@ -8,7 +8,11 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
class StatusContentContainer<ContentView: ContentTextView, PollView: UIView>: UIView {
|
||||
protocol StatusContentPollView: UIView {
|
||||
func estimateHeight(effectiveWidth: CGFloat) -> CGFloat
|
||||
}
|
||||
|
||||
class StatusContentContainer<ContentView: ContentTextView, PollView: StatusContentPollView>: UIView {
|
||||
|
||||
private var useTopSpacer = false
|
||||
private let topSpacer = UIView().configure {
|
||||
|
@ -25,9 +29,10 @@ class StatusContentContainer<ContentView: ContentTextView, PollView: UIView>: UI
|
|||
$0.isSelectable = false
|
||||
}
|
||||
|
||||
private static var cardViewHeight: CGFloat { 90 }
|
||||
let cardView = StatusCardView().configure {
|
||||
NSLayoutConstraint.activate([
|
||||
$0.heightAnchor.constraint(equalToConstant: 90),
|
||||
$0.heightAnchor.constraint(equalToConstant: StatusContentContainer.cardViewHeight),
|
||||
])
|
||||
}
|
||||
|
||||
|
@ -132,4 +137,22 @@ class StatusContentContainer<ContentView: ContentTextView, PollView: UIView>: UI
|
|||
zeroHeightConstraint.isActive = collapsed
|
||||
}
|
||||
|
||||
// used only for collapsing automatically based on height, doesn't need to be accurate
|
||||
// just roughly inline with the content height
|
||||
func estimateVisibleSubviewHeight(effectiveWidth: CGFloat) -> CGFloat {
|
||||
var height: CGFloat = 0
|
||||
height += contentTextView.sizeThatFits(CGSize(width: effectiveWidth, height: UIView.layoutFittingCompressedSize.height)).height
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -238,13 +238,13 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
|||
$0.addInteraction(UIPointerInteraction(delegate: self))
|
||||
}
|
||||
|
||||
private(set) lazy var favoriteButton = UIButton().configure {
|
||||
private(set) lazy var favoriteButton = ToggleableButton(activeColor: UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1)).configure {
|
||||
$0.setImage(UIImage(systemName: "star.fill"), for: .normal)
|
||||
$0.addTarget(self, action: #selector(favoritePressed), for: .touchUpInside)
|
||||
$0.addInteraction(UIPointerInteraction(delegate: self))
|
||||
}
|
||||
|
||||
private(set) lazy var reblogButton = UIButton().configure {
|
||||
private(set) lazy var reblogButton = ToggleableButton(activeColor: UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1)).configure {
|
||||
$0.setImage(UIImage(systemName: "repeat"), for: .normal)
|
||||
$0.addTarget(self, action: #selector(reblogPressed), for: .touchUpInside)
|
||||
$0.addInteraction(UIPointerInteraction(delegate: self))
|
||||
|
@ -625,6 +625,15 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
|||
}
|
||||
}
|
||||
|
||||
func updateStatusState(status: StatusMO) {
|
||||
baseUpdateStatusState(status: status)
|
||||
}
|
||||
|
||||
func estimateContentHeight() -> CGFloat {
|
||||
let width = bounds.width /* leading spacing: */ - 16 /* avatar: */ - 50 /* spacing: 8 */ - 8 /* trailing spacing: */ - 16
|
||||
return contentContainer.estimateVisibleSubviewHeight(effectiveWidth: width)
|
||||
}
|
||||
|
||||
private func updateTimestamp() {
|
||||
guard let mastodonController,
|
||||
let status = mastodonController.persistentContainer.status(for: statusID) else {
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// ToggleableButton.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 5/13/23.
|
||||
// Copyright © 2023 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class ToggleableButton: UIButton {
|
||||
|
||||
let activeColor: UIColor
|
||||
|
||||
var active: Bool {
|
||||
didSet {
|
||||
tintColor = active ? activeColor : nil
|
||||
}
|
||||
}
|
||||
|
||||
init(activeColor: UIColor) {
|
||||
self.activeColor = activeColor
|
||||
self.active = false
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue