Compare commits

..

7 Commits

17 changed files with 341 additions and 95 deletions

View File

@ -24,7 +24,9 @@ public final class CollapseState: Sendable {
} }
public func copy() -> CollapseState { 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) { public func hash(into hasher: inout Hasher) {

View File

@ -301,6 +301,7 @@
D6D79F2B2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift */; }; D6D79F2B2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift */; };
D6D79F2D2A0D61B400AB2315 /* StatusEditContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2C2A0D61B400AB2315 /* StatusEditContentTextView.swift */; }; D6D79F2D2A0D61B400AB2315 /* StatusEditContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2C2A0D61B400AB2315 /* StatusEditContentTextView.swift */; };
D6D79F2F2A0D6A7F00AB2315 /* StatusEditPollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2E2A0D6A7F00AB2315 /* StatusEditPollView.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 */; }; 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 */; };
@ -700,6 +701,7 @@
D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditCollectionViewCell.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>"; }; 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>"; }; 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>"; }; 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>"; };
@ -1371,6 +1373,7 @@
D65B4B69297777D900DABDFB /* StatusNotFoundView.swift */, D65B4B69297777D900DABDFB /* StatusNotFoundView.swift */,
04ED00B021481ED800567C53 /* SteppedProgressView.swift */, 04ED00B021481ED800567C53 /* SteppedProgressView.swift */,
D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */, D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */,
D6D79F522A0FFE3200AB2315 /* ToggleableButton.swift */,
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */, D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */,
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */, D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */,
D6A3BC872321F78000FD64D5 /* Account Cell */, D6A3BC872321F78000FD64D5 /* Account Cell */,
@ -1945,6 +1948,7 @@
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */, D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */,
D6ADB6EC28EA73CB009924AB /* StatusContentContainer.swift in Sources */, D6ADB6EC28EA73CB009924AB /* StatusContentContainer.swift in Sources */,
D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */, D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */,
D6D79F532A0FFE3200AB2315 /* ToggleableButton.swift in Sources */,
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */, D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */,
D6ADB6F028ED1F25009924AB /* CachedImageView.swift in Sources */, D6ADB6F028ED1F25009924AB /* CachedImageView.swift in Sources */,
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */, D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */,

View File

@ -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>

View File

@ -88,6 +88,10 @@
argument = "-com.apple.CoreData.ConcurrencyDebug 1" argument = "-com.apple.CoreData.ConcurrencyDebug 1"
isEnabled = "YES"> isEnabled = "YES">
</CommandLineArgument> </CommandLineArgument>
<CommandLineArgument
argument = "-com.apple.CoreData.CloudKitDebug 0"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument <CommandLineArgument
argument = "-UIFocusLoggingEnabled YES" argument = "-UIFocusLoggingEnabled YES"
isEnabled = "NO"> isEnabled = "NO">

View File

@ -19,7 +19,7 @@ extension StatusEdit: CollapseStateResolving {}
extension CollapseState { extension CollapseState {
func resolveFor(status: CollapseStateResolving, height: () -> CGFloat, textLength: Int? = nil) -> Bool { 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 { guard unknown || statusPropertiesHash != newHash else {
return false return false
} }

View File

@ -13,6 +13,7 @@ protocol Configurable {
func configure(_ closure: (T) -> Void) -> T func configure(_ closure: (T) -> Void) -> T
} }
extension Configurable where Self: UIView { extension Configurable where Self: UIView {
@inline(__always)
func configure(_ closure: (Self) -> Void) -> Self { func configure(_ closure: (Self) -> Void) -> Self {
closure(self) closure(self)
return self return self

View File

@ -117,8 +117,8 @@ class StatusEditCollectionViewCell: UICollectionViewListCell {
} }
_ = state.resolveFor(status: edit, height: { _ = state.resolveFor(status: edit, height: {
layoutIfNeeded() let width = self.bounds.width - 2*16
return contentContainer.visibleSubviewHeight return contentContainer.estimateVisibleSubviewHeight(effectiveWidth: width)
}) })
collapseButton.isHidden = !state.collapsible! collapseButton.isHidden = !state.collapsible!
contentContainer.setCollapsed(state.collapsed!) contentContainer.setCollapsed(state.collapsed!)

View File

@ -9,7 +9,9 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
class StatusEditPollView: UIStackView { class StatusEditPollView: UIStackView, StatusContentPollView {
private var titleLabels: [EmojiLabel] = []
init() { init() {
super.init(frame: .zero) super.init(frame: .zero)
@ -25,6 +27,7 @@ class StatusEditPollView: UIStackView {
func updateUI(poll: StatusEdit.Poll?, emojis: [Emoji]) { func updateUI(poll: StatusEdit.Poll?, emojis: [Emoji]) {
arrangedSubviews.forEach { $0.removeFromSuperview() } arrangedSubviews.forEach { $0.removeFromSuperview() }
titleLabels = []
for option in poll?.options ?? [] { for option in poll?.options ?? [] {
// the edit poll doesn't actually include the multiple value // the edit poll doesn't actually include the multiple value
@ -33,6 +36,7 @@ class StatusEditPollView: UIStackView {
let label = EmojiLabel() let label = EmojiLabel()
label.text = option.title label.text = option.title
label.setEmojis(emojis, identifier: Optional<String>.none) label.setEmojis(emojis, identifier: Optional<String>.none)
titleLabels.append(label)
let stack = UIStackView(arrangedSubviews: [ let stack = UIStackView(arrangedSubviews: [
icon, icon,
label, 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
}
} }

View File

@ -22,6 +22,15 @@ class AttachmentsContainerView: UIView {
let attachmentStacks: NSHashTable<UIStackView> = .weakObjects() let attachmentStacks: NSHashTable<UIStackView> = .weakObjects()
var moreView: UIView? var moreView: UIView?
private var aspectRatioConstraint: NSLayoutConstraint? 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 blurView: UIVisualEffectView?
var hideButtonView: UIVisualEffectView? var hideButtonView: UIVisualEffectView?
@ -93,7 +102,8 @@ class AttachmentsContainerView: UIView {
fillView(attachmentView) fillView(attachmentView)
sendSubviewToBack(attachmentView) sendSubviewToBack(attachmentView)
accessibilityElements.append(attachmentView) accessibilityElements.append(attachmentView)
if let attachmentAspectRatio = attachmentView.attachmentAspectRatio { if Preferences.shared.showUncroppedMediaInline,
let attachmentAspectRatio = attachmentView.attachmentAspectRatio {
aspectRatio = attachmentAspectRatio aspectRatio = attachmentAspectRatio
} }
case 2: case 2:
@ -266,18 +276,7 @@ class AttachmentsContainerView: UIView {
accessibilityElements.append(moreView) accessibilityElements.append(moreView)
} }
if Preferences.shared.showUncroppedMediaInline { self.aspectRatio = aspectRatio
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
}
}
} else { } else {
self.isHidden = true self.isHidden = true
} }

View File

@ -11,24 +11,27 @@ import Pachyderm
class PollOptionView: UIView { 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) { init(poll: Poll, option: Poll.Option, mastodonController: MastodonController) {
checkbox = PollOptionCheckboxView(multiple: poll.multiple)
super.init(frame: .zero) super.init(frame: .zero)
let minHeight: CGFloat = 35 let minHeight: CGFloat = 35
layer.cornerRadius = 0.1 * minHeight layer.cornerRadius = 0.1 * minHeight
layer.cornerCurve = .continuous layer.cornerCurve = .continuous
backgroundColor = unselectedBackgroundColor backgroundColor = PollOptionView.unselectedBackgroundColor
checkbox.translatesAutoresizingMaskIntoConstraints = false let showCheckbox = poll.ownVotes?.isEmpty == false || !poll.effectiveExpired
addSubview(checkbox) if showCheckbox {
checkbox = PollOptionCheckboxView(multiple: poll.multiple)
checkbox!.translatesAutoresizingMaskIntoConstraints = false
addSubview(checkbox!)
}
let label = EmojiLabel() label = EmojiLabel()
label.translatesAutoresizingMaskIntoConstraints = false label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0 label.numberOfLines = 0
label.font = .preferredFont(forTextStyle: .callout) label.font = .preferredFont(forTextStyle: .callout)
@ -88,12 +91,8 @@ class PollOptionView: UIView {
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
minHeightConstraint, minHeightConstraint,
checkbox.centerYAnchor.constraint(equalTo: centerYAnchor),
checkbox.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
label.topAnchor.constraint(equalTo: topAnchor, constant: 4), label.topAnchor.constraint(equalTo: topAnchor, constant: 4),
label.bottomAnchor.constraint(equalTo: bottomAnchor, 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), label.trailingAnchor.constraint(equalTo: percentLabel.leadingAnchor, constant: -4),
percentLabel.topAnchor.constraint(equalTo: topAnchor), percentLabel.topAnchor.constraint(equalTo: topAnchor),
@ -101,6 +100,16 @@ class PollOptionView: UIView {
percentLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8), 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 isAccessibilityElement = true
} }

View File

@ -14,7 +14,7 @@ class PollOptionsView: UIControl {
var mastodonController: MastodonController! var mastodonController: MastodonController!
var checkedOptionIndices: [Int] { 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)? var checkedOptionsChanged: (() -> Void)?
@ -32,7 +32,7 @@ class PollOptionsView: UIControl {
override var isEnabled: Bool { override var isEnabled: Bool {
didSet { 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 options = poll.options.enumerated().map { (index, opt) in
let optionView = PollOptionView(poll: poll, option: opt, mastodonController: mastodonController) let optionView = PollOptionView(poll: poll, option: opt, mastodonController: mastodonController)
optionView.checkbox.readOnly = !isEnabled if let checkbox = optionView.checkbox {
optionView.checkbox.isChecked = poll.ownVotes?.contains(index) ?? false checkbox.readOnly = !isEnabled
optionView.checkbox.voted = poll.voted ?? false checkbox.isChecked = poll.ownVotes?.contains(index) ?? false
checkbox.voted = poll.voted ?? false
}
stack.addArrangedSubview(optionView) stack.addArrangedSubview(optionView)
return optionView return optionView
} }
@ -75,15 +77,25 @@ class PollOptionsView: UIControl {
accessibilityElements = options 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) { private func selectOption(_ option: PollOptionView) {
if poll.multiple { if poll.multiple {
option.checkbox.isChecked.toggle() option.checkbox?.isChecked.toggle()
} else { } else {
for opt in options { for opt in options {
if opt === option { if opt === option {
opt.checkbox.isChecked = true opt.checkbox?.isChecked = true
} else { } else {
opt.checkbox.isChecked = false opt.checkbox?.isChecked = false
} }
} }
} }

View File

@ -9,7 +9,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
class StatusPollView: UIView { class StatusPollView: UIView, StatusContentPollView {
private static let formatter: DateComponentsFormatter = { private static let formatter: DateComponentsFormatter = {
let f = DateComponentsFormatter() let f = DateComponentsFormatter()
@ -140,6 +140,11 @@ class StatusPollView: UIView {
voteButton.isEnabled = false 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() { private func checkedOptionsChanged() {
voteButton.isEnabled = optionsView.checkedOptionIndices.count > 0 voteButton.isEnabled = optionsView.checkedOptionIndices.count > 0
} }

View File

@ -191,13 +191,13 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
$0.addInteraction(UIPointerInteraction(delegate: self)) $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.setImage(UIImage(systemName: "star.fill"), for: .normal)
$0.addTarget(self, action: #selector(favoritePressed), for: .touchUpInside) $0.addTarget(self, action: #selector(favoritePressed), for: .touchUpInside)
$0.addInteraction(UIPointerInteraction(delegate: self)) $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.setImage(UIImage(systemName: "repeat"), for: .normal)
$0.addTarget(self, action: #selector(reblogPressed), for: .touchUpInside) $0.addTarget(self, action: #selector(reblogPressed), for: .touchUpInside)
$0.addInteraction(UIPointerInteraction(delegate: self)) $0.addInteraction(UIPointerInteraction(delegate: self))
@ -348,24 +348,6 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
accountDetailAccessibilityElement.navigationDelegate = delegate accountDetailAccessibilityElement.navigationDelegate = delegate
accountDetailAccessibilityElement.accountID = accountID 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) var timestampAndClientText = ConversationMainStatusCollectionViewCell.dateFormatter.string(from: status.createdAt)
if let application = status.applicationName { if let application = status.applicationName {
timestampAndClientText += "\(application)" timestampAndClientText += "\(application)"
@ -376,7 +358,9 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
editTimestampButton.isHidden = false editTimestampButton.isHidden = false
var config = UIButton.Configuration.plain() var config = UIButton.Configuration.plain()
config.baseForegroundColor = .secondaryLabel 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 config.contentInsets = .zero
editTimestampButton.configuration = config editTimestampButton.configuration = config
} else { } else {
@ -392,6 +376,33 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
baseCreateObservers() 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) { func updateUIForPreferences(status: StatusMO) {
baseUpdateUIForPreferences(status: status) baseUpdateUIForPreferences(status: status)
} }

View File

@ -26,8 +26,8 @@ protocol StatusCollectionViewCell: UICollectionViewCell, AttachmentViewDelegate
var collapseButton: StatusCollapseButton { get } var collapseButton: StatusCollapseButton { get }
var contentContainer: StatusContentContainer<StatusContentTextView, StatusPollView> { get } var contentContainer: StatusContentContainer<StatusContentTextView, StatusPollView> { get }
var replyButton: UIButton { get } var replyButton: UIButton { get }
var favoriteButton: UIButton { get } var favoriteButton: ToggleableButton { get }
var reblogButton: UIButton { get } var reblogButton: ToggleableButton { get }
var moreButton: UIButton { get } var moreButton: UIButton { get }
var prevThreadLinkView: UIView? { get set } var prevThreadLinkView: UIView? { get set }
var nextThreadLinkView: UIView? { get set } var nextThreadLinkView: UIView? { get set }
@ -45,6 +45,8 @@ protocol StatusCollectionViewCell: UICollectionViewCell, AttachmentViewDelegate
var cancellables: Set<AnyCancellable> { get set } var cancellables: Set<AnyCancellable> { get set }
func updateUIForPreferences(status: StatusMO) func updateUIForPreferences(status: StatusMO)
func updateStatusState(status: StatusMO)
func estimateContentHeight() -> CGFloat
} }
// MARK: UI Configuration // MARK: UI Configuration
@ -58,7 +60,13 @@ extension StatusCollectionViewCell {
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.filter { [unowned self] in $0 == self.statusID } .filter { [unowned self] in $0 == self.statusID }
.sink { [unowned self] _ in .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) .store(in: &cancellables)
@ -98,6 +106,7 @@ extension StatusCollectionViewCell {
} }
updateUIForPreferences(status: status) updateUIForPreferences(status: status)
updateStatusState(status: status)
contentWarningLabel.text = status.spoilerText contentWarningLabel.text = status.spoilerText
contentWarningLabel.isHidden = status.spoilerText.isEmpty contentWarningLabel.isHidden = status.spoilerText.isEmpty
@ -105,35 +114,17 @@ extension StatusCollectionViewCell {
contentWarningLabel.setEmojis(status.emojis, identifier: statusID) 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) reblogButton.isEnabled = reblogEnabled(status: status)
if status.reblogged { replyButton.isEnabled = mastodonController.loggedIn
reblogButton.tintColor = UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1) favoriteButton.isEnabled = mastodonController.loggedIn
reblogButton.accessibilityLabel = NSLocalizedString("Undo Reblog", comment: "undo reblog button accessibility label")
} else {
reblogButton.tintColor = nil
reblogButton.accessibilityLabel = NSLocalizedString("Reblog", comment: "reblog button accessibility label")
}
// keep menu in sync with changed states e.g. bookmarked, muted let didResolve = statusState.resolveFor(status: status, height: self.estimateContentHeight)
// do not include reply action here, because the cell already contains a button for it // let didResolve = statusState.resolveFor(status: status) {
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: delegate?.actionsForStatus(status, source: .view(moreButton), includeStatusButtonActions: false) ?? []) //// // layout so that we can take the content height into consideration when deciding whether to collapse
//// layoutIfNeeded()
let didResolve = statusState.resolveFor(status: status) { //// return contentContainer.visibleSubviewHeight
// layout so that we can take the content height into consideration when deciding whether to collapse // return contentContainer.estimateVisibleSubviewHeight(effectiveWidth: )
layoutIfNeeded() // }
return contentContainer.visibleSubviewHeight
}
if didResolve { if didResolve {
if statusState.collapsible! && showStatusAutomatically { if statusState.collapsible! && showStatusAutomatically {
statusState.collapsed = false statusState.collapsed = false
@ -157,7 +148,9 @@ extension StatusCollectionViewCell {
guard mastodonController.loggedIn else { guard mastodonController.loggedIn else {
return false return false
} }
if status.visibility == .direct || status.visibility == .private { if status.visibility == .direct {
return false
} else if status.visibility == .private {
if mastodonController.instanceFeatures.boostToOriginalAudience, if mastodonController.instanceFeatures.boostToOriginalAudience,
status.account.id == mastodonController.account?.id { status.account.id == mastodonController.account?.id {
return true return true
@ -212,6 +205,30 @@ extension StatusCollectionViewCell {
displayNameLabel.updateForAccountDisplayName(account: status.account) 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) { func setShowThreadLinks(prev: Bool, next: Bool) {
if prev { if prev {
if let prevThreadLinkView { if let prevThreadLinkView {

View File

@ -8,7 +8,11 @@
import UIKit 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 var useTopSpacer = false
private let topSpacer = UIView().configure { private let topSpacer = UIView().configure {
@ -25,9 +29,10 @@ class StatusContentContainer<ContentView: ContentTextView, PollView: UIView>: UI
$0.isSelectable = false $0.isSelectable = false
} }
private static var cardViewHeight: CGFloat { 90 }
let cardView = StatusCardView().configure { let cardView = StatusCardView().configure {
NSLayoutConstraint.activate([ 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 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
}
} }

View File

@ -238,13 +238,13 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
$0.addInteraction(UIPointerInteraction(delegate: self)) $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.setImage(UIImage(systemName: "star.fill"), for: .normal)
$0.addTarget(self, action: #selector(favoritePressed), for: .touchUpInside) $0.addTarget(self, action: #selector(favoritePressed), for: .touchUpInside)
$0.addInteraction(UIPointerInteraction(delegate: self)) $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.setImage(UIImage(systemName: "repeat"), for: .normal)
$0.addTarget(self, action: #selector(reblogPressed), for: .touchUpInside) $0.addTarget(self, action: #selector(reblogPressed), for: .touchUpInside)
$0.addInteraction(UIPointerInteraction(delegate: self)) $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() { private func updateTimestamp() {
guard let mastodonController, guard let mastodonController,
let status = mastodonController.persistentContainer.status(for: statusID) else { let status = mastodonController.persistentContainer.status(for: statusID) else {

View File

@ -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")
}
}