Compare commits

...

7 Commits

17 changed files with 143 additions and 137 deletions

View File

@ -1,5 +1,15 @@
# Changelog # Changelog
## 2024.1 (117)
Features/Improvements:
- Add See Results button to polls
Bugfixes:
- Fix race condition when presenting gallery for 4th of more than 4 attachments
- Fix gallery interactive dismissal not working for 4th or later attachments on posts with more than 4 attachments
- Pixelfed: Fix crash when there are multiple follow notifications from the same account
- macOS: Fix gallery being positioned incorrectly when Reduce Motion is on
## 2024.1 (116) ## 2024.1 (116)
Features/Improvements: Features/Improvements:
- Display message on empty list timelines - Display message on empty list timelines

View File

@ -52,6 +52,9 @@ class GalleryDismissAnimationController: NSObject, UIViewControllerAnimatedTrans
appliedSourceToDestTransform = false appliedSourceToDestTransform = false
} }
to.view.frame = container.bounds
from.view.frame = container.bounds
let content = itemViewController.takeContent() let content = itemViewController.takeContent()
content.view.translatesAutoresizingMaskIntoConstraints = true content.view.translatesAutoresizingMaskIntoConstraints = true
content.view.layer.masksToBounds = true content.view.layer.masksToBounds = true
@ -112,6 +115,8 @@ class GalleryDismissAnimationController: NSObject, UIViewControllerAnimatedTrans
return return
} }
toVC.view.frame = transitionContext.containerView.bounds
fromVC.view.frame = transitionContext.containerView.bounds
transitionContext.containerView.addSubview(toVC.view) transitionContext.containerView.addSubview(toVC.view)
transitionContext.containerView.addSubview(fromVC.view) transitionContext.containerView.addSubview(fromVC.view)

View File

@ -109,8 +109,6 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
itemViewController.addContent() itemViewController.addContent()
transitionContext.completeTransition(true) transitionContext.completeTransition(true)
to.presentationAnimationCompleted()
} }
animator.startAnimation() animator.startAnimation()
@ -121,8 +119,9 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
return return
} }
transitionContext.containerView.addSubview(to.view)
to.view.alpha = 0 to.view.alpha = 0
to.view.frame = transitionContext.containerView.bounds
transitionContext.containerView.addSubview(to.view)
let duration = transitionDuration(using: transitionContext) let duration = transitionDuration(using: transitionContext)
let animator = UIViewPropertyAnimator(duration: duration, curve: .easeInOut) let animator = UIViewPropertyAnimator(duration: duration, curve: .easeInOut)
@ -131,8 +130,6 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
} }
animator.addCompletion { _ in animator.addCompletion { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled) transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
to.presentationAnimationCompleted()
} }
animator.startAnimation() animator.startAnimation()
} }

View File

@ -68,6 +68,17 @@ public class GalleryViewController: UIPageViewController {
setViewControllers([makeItemVC(index: initialItemIndex)], direction: .forward, animated: false) setViewControllers([makeItemVC(index: initialItemIndex)], direction: .forward, animated: false)
} }
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if animated {
// Wait until the transition is no longer in-progress, otherwise things will just get deferred again.
DispatchQueue.main.async {
self.presentationAnimationCompleted()
}
}
}
public override func viewWillDisappear(_ animated: Bool) { public override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated) super.viewWillDisappear(animated)

View File

@ -255,7 +255,6 @@
D6B81F442560390300F6E31D /* MenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B81F432560390300F6E31D /* MenuController.swift */; }; D6B81F442560390300F6E31D /* MenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B81F432560390300F6E31D /* MenuController.swift */; };
D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */; }; D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */; };
D6B93667281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B93666281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift */; }; D6B93667281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B93666281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift */; };
D6B9366B281EE77E00237D0E /* PollVoteButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B9366A281EE77E00237D0E /* PollVoteButton.swift */; };
D6B9366D2828445000237D0E /* SavedInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B9366C2828444F00237D0E /* SavedInstance.swift */; }; D6B9366D2828445000237D0E /* SavedInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B9366C2828444F00237D0E /* SavedInstance.swift */; };
D6B9366F2828452F00237D0E /* SavedHashtag.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B9366E2828452F00237D0E /* SavedHashtag.swift */; }; D6B9366F2828452F00237D0E /* SavedHashtag.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B9366E2828452F00237D0E /* SavedHashtag.swift */; };
D6B936712829F72900237D0E /* NSManagedObjectContext+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */; }; D6B936712829F72900237D0E /* NSManagedObjectContext+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */; };
@ -658,7 +657,6 @@
D6B81F432560390300F6E31D /* MenuController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuController.swift; sourceTree = "<group>"; }; D6B81F432560390300F6E31D /* MenuController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuController.swift; sourceTree = "<group>"; };
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Visibility.swift"; sourceTree = "<group>"; }; D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Visibility.swift"; sourceTree = "<group>"; };
D6B93666281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSidebarMyProfileCollectionViewCell.swift; sourceTree = "<group>"; }; D6B93666281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSidebarMyProfileCollectionViewCell.swift; sourceTree = "<group>"; };
D6B9366A281EE77E00237D0E /* PollVoteButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollVoteButton.swift; sourceTree = "<group>"; };
D6B9366C2828444F00237D0E /* SavedInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedInstance.swift; sourceTree = "<group>"; }; D6B9366C2828444F00237D0E /* SavedInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedInstance.swift; sourceTree = "<group>"; };
D6B9366E2828452F00237D0E /* SavedHashtag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedHashtag.swift; sourceTree = "<group>"; }; D6B9366E2828452F00237D0E /* SavedHashtag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedHashtag.swift; sourceTree = "<group>"; };
D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Helpers.swift"; sourceTree = "<group>"; }; D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Helpers.swift"; sourceTree = "<group>"; };
@ -898,7 +896,6 @@
D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */, D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */,
D623A5402635FB3C0095BD04 /* PollOptionView.swift */, D623A5402635FB3C0095BD04 /* PollOptionView.swift */,
D623A542263634100095BD04 /* PollOptionCheckboxView.swift */, D623A542263634100095BD04 /* PollOptionCheckboxView.swift */,
D6B9366A281EE77E00237D0E /* PollVoteButton.swift */,
); );
path = Poll; path = Poll;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2201,7 +2198,6 @@
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */, D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */,
04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */, 04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */,
D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */, D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */,
D6B9366B281EE77E00237D0E /* PollVoteButton.swift in Sources */,
D68A76DA29511CA6001DA1B3 /* AccountPreferences.swift in Sources */, D68A76DA29511CA6001DA1B3 /* AccountPreferences.swift in Sources */,
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */, D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */,
D6895DC428D65342006341DA /* ConfirmReblogStatusPreviewView.swift in Sources */, D6895DC428D65342006341DA /* ConfirmReblogStatusPreviewView.swift in Sources */,

View File

@ -15,10 +15,12 @@ import AVFoundation
class StatusAttachmentsGalleryDataSource: GalleryDataSource { class StatusAttachmentsGalleryDataSource: GalleryDataSource {
let attachments: [Attachment] let attachments: [Attachment]
let sourceViews: NSHashTable<AttachmentView> let sourceViews: NSHashTable<AttachmentView>
weak var moreView: UIView?
init(attachments: [Attachment], sourceViews: NSHashTable<AttachmentView>) { init(attachments: [Attachment], sourceViews: NSHashTable<AttachmentView>, moreView: UIView?) {
self.attachments = attachments self.attachments = attachments
self.sourceViews = sourceViews self.sourceViews = sourceViews
self.moreView = moreView
} }
func galleryItemsCount() -> Int { func galleryItemsCount() -> Int {
@ -39,6 +41,21 @@ class StatusAttachmentsGalleryDataSource: GalleryDataSource {
// TODO: if automatically play gifs is off, this will start the source view playing too // TODO: if automatically play gifs is off, this will start the source view playing too
gifController: view.gifController gifController: view.gifController
) )
} else if let entry = ImageCache.attachments.get(attachment.url, loadOriginal: true) {
let gifController: GIFController? =
if attachment.url.pathExtension == "gif",
let data = entry.data {
GIFController(gifData: data)
} else {
nil
}
return ImageGalleryContentViewController(
url: attachment.url,
caption: attachment.description,
originalData: entry.data,
image: entry.image,
gifController: gifController
)
} else { } else {
return LoadingGalleryContentViewController { return LoadingGalleryContentViewController {
let (data, image) = await ImageCache.attachments.get(attachment.url, loadOriginal: true) let (data, image) = await ImageCache.attachments.get(attachment.url, loadOriginal: true)
@ -92,8 +109,12 @@ class StatusAttachmentsGalleryDataSource: GalleryDataSource {
} }
func galleryContentTransitionSourceView(forItemAt index: Int) -> UIView? { func galleryContentTransitionSourceView(forItemAt index: Int) -> UIView? {
let attachment = attachments[index] if attachments.count > 4 && index >= 3 {
return attachmentView(for: attachment) return moreView
} else {
let attachment = attachments[index]
return attachmentView(for: attachment)
}
} }
func galleryApplicationActivities(forItemAt index: Int) -> [UIActivity]? { func galleryApplicationActivities(forItemAt index: Int) -> [UIActivity]? {

View File

@ -135,9 +135,11 @@ class ActionNotificationGroupCollectionViewCell: UICollectionViewListCell {
updateTimestamp() updateTimestamp()
let people = group.notifications.compactMap { let people = group.notifications
mastodonController.persistentContainer.account(for: $0.account.id) .uniques(by: \.account.id)
} .compactMap {
mastodonController.persistentContainer.account(for: $0.account.id)
}
let visibleAvatars = Array(people.lazy.compactMap(\.avatar).prefix(10)) let visibleAvatars = Array(people.lazy.compactMap(\.avatar).prefix(10))
for (index, avatarURL) in visibleAvatars.enumerated() { for (index, avatarURL) in visibleAvatars.enumerated() {

View File

@ -109,7 +109,11 @@ class FollowNotificationGroupCollectionViewCell: UICollectionViewListCell {
} }
self.group = group self.group = group
let people = group.notifications.compactMap { mastodonController.persistentContainer.account(for: $0.account.id) } let people = group.notifications
.uniques(by: \.account.id)
.compactMap {
mastodonController.persistentContainer.account(for: $0.account.id)
}
actionLabel.setEmojis(pairs: people.map { actionLabel.setEmojis(pairs: people.map {
($0.displayOrUserName, $0.emojis) ($0.displayOrUserName, $0.emojis)

View File

@ -627,11 +627,11 @@ extension NotificationsCollectionViewController: UICollectionViewDelegate {
case .favourite, .reblog: case .favourite, .reblog:
let type = group.kind == .favourite ? StatusActionAccountListViewController.ActionType.favorite : .reblog let type = group.kind == .favourite ? StatusActionAccountListViewController.ActionType.favorite : .reblog
let statusID = group.notifications.first!.status!.id let statusID = group.notifications.first!.status!.id
let accountIDs = group.notifications.map(\.account.id) let accountIDs = group.notifications.map(\.account.id).uniques()
let vc = StatusActionAccountListViewController(actionType: type, statusID: statusID, statusState: .unknown, accountIDs: accountIDs, mastodonController: mastodonController) let vc = StatusActionAccountListViewController(actionType: type, statusID: statusID, statusState: .unknown, accountIDs: accountIDs, mastodonController: mastodonController)
show(vc) show(vc)
case .follow: case .follow:
let accountIDs = group.notifications.map(\.account.id) let accountIDs = group.notifications.map(\.account.id).uniques()
switch accountIDs.count { switch accountIDs.count {
case 0: case 0:
collectionView.deselectItem(at: indexPath, animated: true) collectionView.deselectItem(at: indexPath, animated: true)
@ -670,11 +670,11 @@ extension NotificationsCollectionViewController: UICollectionViewDelegate {
return UIContextMenuConfiguration(previewProvider: { return UIContextMenuConfiguration(previewProvider: {
let type = group.kind == .favourite ? StatusActionAccountListViewController.ActionType.favorite : .reblog let type = group.kind == .favourite ? StatusActionAccountListViewController.ActionType.favorite : .reblog
let statusID = group.notifications.first!.status!.id let statusID = group.notifications.first!.status!.id
let accountIDs = group.notifications.map(\.account.id) let accountIDs = group.notifications.map(\.account.id).uniques()
return StatusActionAccountListViewController(actionType: type, statusID: statusID, statusState: .unknown, accountIDs: accountIDs, mastodonController: self.mastodonController) return StatusActionAccountListViewController(actionType: type, statusID: statusID, statusState: .unknown, accountIDs: accountIDs, mastodonController: self.mastodonController)
}) })
case .follow: case .follow:
let accountIDs = group.notifications.map(\.account.id) let accountIDs = group.notifications.map(\.account.id).uniques()
return UIContextMenuConfiguration { return UIContextMenuConfiguration {
if accountIDs.count == 1 { if accountIDs.count == 1 {
return ProfileViewController(accountID: accountIDs.first!, mastodonController: self.mastodonController) return ProfileViewController(accountID: accountIDs.first!, mastodonController: self.mastodonController)

View File

@ -238,7 +238,8 @@ extension StatusEditCollectionViewCell: AttachmentViewDelegate {
func attachmentViewGallery(startingAt index: Int) -> UIViewController? { func attachmentViewGallery(startingAt index: Int) -> UIViewController? {
let attachments = attachmentsView.attachments! let attachments = attachmentsView.attachments!
let sourceViews = attachmentsView.attachmentViews.copy() as! NSHashTable<AttachmentView> let sourceViews = attachmentsView.attachmentViews.copy() as! NSHashTable<AttachmentView>
return GalleryVC.GalleryViewController(dataSource: StatusAttachmentsGalleryDataSource(attachments: attachments, sourceViews: sourceViews), initialItemIndex: index) let dataSource = StatusAttachmentsGalleryDataSource(attachments: attachments, sourceViews: sourceViews, moreView: attachmentsView.moreView)
return GalleryVC.GalleryViewController(dataSource: dataSource, initialItemIndex: index)
} }
func attachmentViewPresent(_ vc: UIViewController, animated: Bool) { func attachmentViewPresent(_ vc: UIViewController, animated: Bool) {

View File

@ -18,7 +18,7 @@ class AttachmentsContainerView: UIView {
let attachmentViews: NSHashTable<AttachmentView> = .weakObjects() let attachmentViews: NSHashTable<AttachmentView> = .weakObjects()
let attachmentStacks: NSHashTable<UIStackView> = .weakObjects() let attachmentStacks: NSHashTable<UIStackView> = .weakObjects()
var moreView: UIView? private(set) var moreView: UIView?
private var aspectRatioConstraint: NSLayoutConstraint? private var aspectRatioConstraint: NSLayoutConstraint?
private(set) var aspectRatio: CGFloat = 16/9 { private(set) var aspectRatio: CGFloat = 16/9 {
didSet { didSet {

View File

@ -75,7 +75,7 @@ class PollOptionView: UIView {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
func updateUI(poll: Poll, option: Poll.Option, ownVoted: Bool, mastodonController: MastodonController) { func updateUI(poll: Poll, option: Poll.Option, ownVoted: Bool, showResults: Bool, mastodonController: MastodonController) {
let showCheckbox = poll.ownVotes?.isEmpty == false || !poll.effectiveExpired let showCheckbox = poll.ownVotes?.isEmpty == false || !poll.effectiveExpired
if showCheckbox { if showCheckbox {
checkbox.isChecked = ownVoted checkbox.isChecked = ownVoted
@ -100,7 +100,7 @@ class PollOptionView: UIView {
accessibilityLabel = option.title accessibilityLabel = option.title
if (poll.voted ?? false) || poll.effectiveExpired, if showResults,
let optionVotes = option.votesCount { let optionVotes = option.votesCount {
let frac: CGFloat let frac: CGFloat
if poll.multiple, if poll.multiple,

View File

@ -65,7 +65,7 @@ class PollOptionsView: UIControl {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
func updateUI(poll: Poll) { func updateUI(poll: Poll, showResults: Bool) {
self.poll = poll self.poll = poll
if poll.options.count > options.count { if poll.options.count > options.count {
@ -81,7 +81,7 @@ class PollOptionsView: UIControl {
} }
for (index, (view, opt)) in zip(options, poll.options).enumerated() { for (index, (view, opt)) in zip(options, poll.options).enumerated() {
view.updateUI(poll: poll, option: opt, ownVoted: poll.ownVotes?.contains(index) ?? false, mastodonController: mastodonController) view.updateUI(poll: poll, option: opt, ownVoted: poll.ownVotes?.contains(index) ?? false, showResults: showResults, mastodonController: mastodonController)
view.checkboxIfInitialized?.readOnly = !isEnabled view.checkboxIfInitialized?.readOnly = !isEnabled
} }
} }

View File

@ -1,81 +0,0 @@
//
// PollVoteButton.swift
// Tusker
//
// Created by Shadowfacts on 5/1/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import UIKit
/// Wraps a UILabel and UIButton to allow setting disabled titles on Catalyst, where `setTitle(_:for:)` only works for the normal state.
class PollVoteButton: UIView {
var disabledTitle: String = "" {
didSet {
update()
}
}
var isEnabled = true {
didSet {
update()
}
}
private var button = UIButton(type: .system)
#if targetEnvironment(macCatalyst)
private var label = UILabel()
#endif
override init(frame: CGRect) {
super.init(frame: frame)
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitleColor(.secondaryLabel, for: .disabled)
button.contentHorizontalAlignment = .trailing
embedSubview(button)
#if targetEnvironment(macCatalyst)
label.textColor = .secondaryLabel
label.translatesAutoresizingMaskIntoConstraints = false
label.textAlignment = .right
embedSubview(label)
#endif
update()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func addTarget(_ target: Any, action: Selector) {
button.addTarget(target, action: action, for: .touchUpInside)
}
func setFont(_ font: UIFont) {
button.titleLabel!.font = font
#if targetEnvironment(macCatalyst)
label.font = font
#endif
}
private func update() {
button.isEnabled = isEnabled
if isEnabled {
#if targetEnvironment(macCatalyst)
label.isHidden = true
button.isHidden = false
#endif
button.setTitle("Vote", for: .normal)
} else {
#if targetEnvironment(macCatalyst)
label.text = disabledTitle
label.isHidden = false
button.isHidden = true
#else
button.setTitle(disabledTitle, for: .disabled)
#endif
}
}
}

View File

@ -15,7 +15,7 @@ class StatusPollView: UIView, StatusContentView {
let f = DateComponentsFormatter() let f = DateComponentsFormatter()
f.includesTimeRemainingPhrase = true f.includesTimeRemainingPhrase = true
f.maximumUnitCount = 1 f.maximumUnitCount = 1
f.unitsStyle = .full f.unitsStyle = .abbreviated
f.allowedUnits = [.weekOfMonth, .day, .hour, .minute] f.allowedUnits = [.weekOfMonth, .day, .hour, .minute]
return f return f
}() }()
@ -25,9 +25,10 @@ class StatusPollView: UIView, StatusContentView {
private var statusID: String! private var statusID: String!
private(set) var poll: Poll? private(set) var poll: Poll?
private var showingResults = false
private var optionsView: PollOptionsView! private var optionsView: PollOptionsView!
private var voteButton: PollVoteButton! private var voteButton: UIButton!
private var infoLabel: UILabel! private var infoLabel: UILabel!
private var canVote = true private var canVote = true
@ -63,11 +64,11 @@ class StatusPollView: UIView, StatusContentView {
infoLabel.adjustsFontSizeToFitWidth = true infoLabel.adjustsFontSizeToFitWidth = true
addSubview(infoLabel) addSubview(infoLabel)
voteButton = PollVoteButton() voteButton = UIButton(configuration: .plain())
voteButton.translatesAutoresizingMaskIntoConstraints = false voteButton.translatesAutoresizingMaskIntoConstraints = false
voteButton.addTarget(self, action: #selector(votePressed)) voteButton.addTarget(self, action: #selector(votePressed), for: .touchUpInside)
voteButton.setFont(infoLabel.font)
voteButton.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) voteButton.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
voteButton.setContentHuggingPriority(.defaultHigh, for: .horizontal)
addSubview(voteButton) addSubview(voteButton)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
@ -76,20 +77,24 @@ class StatusPollView: UIView, StatusContentView {
optionsView.topAnchor.constraint(equalTo: topAnchor), optionsView.topAnchor.constraint(equalTo: topAnchor),
infoLabel.leadingAnchor.constraint(equalTo: leadingAnchor), infoLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
infoLabel.topAnchor.constraint(equalTo: optionsView.bottomAnchor), infoLabel.topAnchor.constraint(equalTo: optionsView.bottomAnchor, constant: 4),
infoLabel.bottomAnchor.constraint(equalTo: bottomAnchor), infoLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
infoLabel.trailingAnchor.constraint(equalTo: voteButton.leadingAnchor, constant: -8), infoLabel.trailingAnchor.constraint(equalTo: voteButton.leadingAnchor, constant: -8),
voteButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 44), voteButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 44),
voteButton.trailingAnchor.constraint(equalTo: trailingAnchor), voteButton.trailingAnchor.constraint(equalTo: trailingAnchor),
voteButton.topAnchor.constraint(equalTo: optionsView.bottomAnchor), voteButton.topAnchor.constraint(equalTo: infoLabel.topAnchor),
voteButton.bottomAnchor.constraint(equalTo: bottomAnchor), voteButton.bottomAnchor.constraint(equalTo: infoLabel.bottomAnchor),
]) ])
accessibilityElements = [optionsView!, infoLabel!, voteButton!] accessibilityElements = [optionsView!, infoLabel!, voteButton!]
} }
func updateUI(status: StatusMO, poll: Poll?) { func updateUI(status: StatusMO, poll: Poll?) {
if statusID != status.id {
showingResults = false
}
self.statusID = status.id self.statusID = status.id
self.poll = poll self.poll = poll
@ -104,19 +109,17 @@ class StatusPollView: UIView, StatusContentView {
optionsView.mastodonController = mastodonController optionsView.mastodonController = mastodonController
optionsView.isEnabled = canVote optionsView.isEnabled = canVote
optionsView.updateUI(poll: poll) optionsView.updateUI(poll: poll, showResults: showingResults || !canVote)
var expired = false
let expiryText: String? let expiryText: String?
if let expiresAt = poll.expiresAt { if let expiresAt = poll.expiresAt {
if expiresAt > Date() { if expiresAt > Date() {
expiryText = StatusPollView.formatter.string(from: Date(), to: expiresAt) expiryText = StatusPollView.formatter.string(from: Date(), to: expiresAt)
} else { } else {
expired = true
expiryText = nil expiryText = nil
} }
} else { } else {
expiryText = "Does not expire" expiryText = "No expiry"
} }
let format = NSLocalizedString("poll votes count", comment: "poll total votes count") let format = NSLocalizedString("poll votes count", comment: "poll total votes count")
@ -125,20 +128,7 @@ class StatusPollView: UIView, StatusContentView {
infoLabel.text! += ", \(expiryText)" infoLabel.text! += ", \(expiryText)"
} }
if expired { updateVoteButton(status: status, poll: poll)
voteButton.disabledTitle = "Expired"
} else if poll.voted ?? false {
if status.account.id == mastodonController.account?.id {
voteButton.isHidden = true
} else {
voteButton.disabledTitle = "Voted"
}
} else if poll.multiple {
voteButton.disabledTitle = "Select multiple"
} else {
voteButton.disabledTitle = "Select one"
}
voteButton.isEnabled = false
} }
func estimateHeight(effectiveWidth: CGFloat) -> CGFloat { func estimateHeight(effectiveWidth: CGFloat) -> CGFloat {
@ -146,11 +136,54 @@ class StatusPollView: UIView, StatusContentView {
return optionsView.estimateHeight(effectiveWidth: effectiveWidth) + infoLabel.sizeThatFits(UIView.layoutFittingExpandedSize).height return optionsView.estimateHeight(effectiveWidth: effectiveWidth) + infoLabel.sizeThatFits(UIView.layoutFittingExpandedSize).height
} }
private func updateVoteButton(status: StatusMO, poll: Poll) {
let buttonTitle: String
let buttonEnabled: Bool
if let expiresAt = poll.expiresAt,
expiresAt <= Date() {
buttonTitle = "Expired"
buttonEnabled = false
} else if poll.voted ?? false {
if status.account.id == mastodonController.account?.id {
voteButton.isHidden = true
return
} else {
buttonTitle = "Voted"
buttonEnabled = false
}
} else if optionsView.checkedOptionIndices.isEmpty {
buttonTitle = "See Results"
buttonEnabled = true
} else {
buttonTitle = "Vote"
buttonEnabled = true
}
var config = UIButton.Configuration.plain()
config.attributedTitle = AttributedString(buttonTitle)
config.attributedTitle!.font = infoLabel.font
config.contentInsets = .zero
// Necessary on Catalyst for some reason.
config.baseForegroundColor = .tintColor
voteButton.configuration = config
voteButton.isEnabled = buttonEnabled
}
private func checkedOptionsChanged() { private func checkedOptionsChanged() {
voteButton.isEnabled = optionsView.checkedOptionIndices.count > 0 if let status = mastodonController.persistentContainer.status(for: statusID),
let poll = status.poll {
updateVoteButton(status: status, poll: poll)
}
} }
@objc private func votePressed() { @objc private func votePressed() {
if !optionsView.checkedOptionIndices.isEmpty {
doVote()
} else {
showResults()
}
}
private func doVote() {
guard let statusID, guard let statusID,
let poll else { let poll else {
return return
@ -158,7 +191,6 @@ class StatusPollView: UIView, StatusContentView {
optionsView.isEnabled = false optionsView.isEnabled = false
voteButton.isEnabled = false voteButton.isEnabled = false
voteButton.disabledTitle = "Voted"
#if !os(visionOS) #if !os(visionOS)
UIImpactFeedbackGenerator(style: .light).impactOccurred() UIImpactFeedbackGenerator(style: .light).impactOccurred()
@ -191,4 +223,11 @@ class StatusPollView: UIView, StatusContentView {
} }
} }
private func showResults() {
showingResults = true
if let status = mastodonController.persistentContainer.status(for: statusID) {
updateUI(status: status, poll: status.poll)
}
}
} }

View File

@ -334,7 +334,8 @@ extension StatusCollectionViewCell {
return nil return nil
} }
let sourceViews = attachmentsView.attachmentViews.copy() as! NSHashTable<AttachmentView> let sourceViews = attachmentsView.attachmentViews.copy() as! NSHashTable<AttachmentView>
return GalleryVC.GalleryViewController(dataSource: StatusAttachmentsGalleryDataSource(attachments: status.attachments, sourceViews: sourceViews), initialItemIndex: index) let dataSource = StatusAttachmentsGalleryDataSource(attachments: status.attachments, sourceViews: sourceViews, moreView: attachmentsView.moreView)
return GalleryVC.GalleryViewController(dataSource: dataSource, initialItemIndex: index)
} }
func attachmentViewPresent(_ vc: UIViewController, animated: Bool) { func attachmentViewPresent(_ vc: UIViewController, animated: Bool) {

View File

@ -10,7 +10,7 @@
// https://help.apple.com/xcode/#/dev745c5c974 // https://help.apple.com/xcode/#/dev745c5c974
MARKETING_VERSION = 2024.1 MARKETING_VERSION = 2024.1
CURRENT_PROJECT_VERSION = 116 CURRENT_PROJECT_VERSION = 117
CURRENT_PROJECT_VERSION = $(inherited)$(CURRENT_PROJECT_VERSION_BUILD_SUFFIX_$(CONFIGURATION)) CURRENT_PROJECT_VERSION = $(inherited)$(CURRENT_PROJECT_VERSION_BUILD_SUFFIX_$(CONFIGURATION))
CURRENT_PROJECT_VERSION_BUILD_SUFFIX_Debug=-dev CURRENT_PROJECT_VERSION_BUILD_SUFFIX_Debug=-dev