Compare commits
No commits in common. "51db0066acecd5198e367893eb211a9a98381efa" and "e522e30ce50729393c93e490dc441ccd8ed71452" have entirely different histories.
51db0066ac
...
e522e30ce5
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -1,15 +1,5 @@
|
||||||
# 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
|
||||||
|
|
|
@ -52,9 +52,6 @@ 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
|
||||||
|
@ -115,8 +112,6 @@ 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)
|
||||||
|
|
||||||
|
|
|
@ -109,6 +109,8 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
|
||||||
itemViewController.addContent()
|
itemViewController.addContent()
|
||||||
|
|
||||||
transitionContext.completeTransition(true)
|
transitionContext.completeTransition(true)
|
||||||
|
|
||||||
|
to.presentationAnimationCompleted()
|
||||||
}
|
}
|
||||||
|
|
||||||
animator.startAnimation()
|
animator.startAnimation()
|
||||||
|
@ -119,9 +121,8 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
to.view.alpha = 0
|
|
||||||
to.view.frame = transitionContext.containerView.bounds
|
|
||||||
transitionContext.containerView.addSubview(to.view)
|
transitionContext.containerView.addSubview(to.view)
|
||||||
|
to.view.alpha = 0
|
||||||
|
|
||||||
let duration = transitionDuration(using: transitionContext)
|
let duration = transitionDuration(using: transitionContext)
|
||||||
let animator = UIViewPropertyAnimator(duration: duration, curve: .easeInOut)
|
let animator = UIViewPropertyAnimator(duration: duration, curve: .easeInOut)
|
||||||
|
@ -130,6 +131,8 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
|
||||||
}
|
}
|
||||||
animator.addCompletion { _ in
|
animator.addCompletion { _ in
|
||||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
||||||
|
|
||||||
|
to.presentationAnimationCompleted()
|
||||||
}
|
}
|
||||||
animator.startAnimation()
|
animator.startAnimation()
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,17 +68,6 @@ 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)
|
||||||
|
|
||||||
|
|
|
@ -255,6 +255,7 @@
|
||||||
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 */; };
|
||||||
|
@ -657,6 +658,7 @@
|
||||||
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>"; };
|
||||||
|
@ -896,6 +898,7 @@
|
||||||
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>";
|
||||||
|
@ -2198,6 +2201,7 @@
|
||||||
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 */,
|
||||||
|
|
|
@ -15,12 +15,10 @@ 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>, moreView: UIView?) {
|
init(attachments: [Attachment], sourceViews: NSHashTable<AttachmentView>) {
|
||||||
self.attachments = attachments
|
self.attachments = attachments
|
||||||
self.sourceViews = sourceViews
|
self.sourceViews = sourceViews
|
||||||
self.moreView = moreView
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func galleryItemsCount() -> Int {
|
func galleryItemsCount() -> Int {
|
||||||
|
@ -41,21 +39,6 @@ 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)
|
||||||
|
@ -109,12 +92,8 @@ class StatusAttachmentsGalleryDataSource: GalleryDataSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
func galleryContentTransitionSourceView(forItemAt index: Int) -> UIView? {
|
func galleryContentTransitionSourceView(forItemAt index: Int) -> UIView? {
|
||||||
if attachments.count > 4 && index >= 3 {
|
let attachment = attachments[index]
|
||||||
return moreView
|
return attachmentView(for: attachment)
|
||||||
} else {
|
|
||||||
let attachment = attachments[index]
|
|
||||||
return attachmentView(for: attachment)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func galleryApplicationActivities(forItemAt index: Int) -> [UIActivity]? {
|
func galleryApplicationActivities(forItemAt index: Int) -> [UIActivity]? {
|
||||||
|
|
|
@ -135,11 +135,9 @@ class ActionNotificationGroupCollectionViewCell: UICollectionViewListCell {
|
||||||
|
|
||||||
updateTimestamp()
|
updateTimestamp()
|
||||||
|
|
||||||
let people = group.notifications
|
let people = group.notifications.compactMap {
|
||||||
.uniques(by: \.account.id)
|
mastodonController.persistentContainer.account(for: $0.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() {
|
||||||
|
|
|
@ -109,11 +109,7 @@ class FollowNotificationGroupCollectionViewCell: UICollectionViewListCell {
|
||||||
}
|
}
|
||||||
self.group = group
|
self.group = group
|
||||||
|
|
||||||
let people = group.notifications
|
let people = group.notifications.compactMap { mastodonController.persistentContainer.account(for: $0.account.id) }
|
||||||
.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)
|
||||||
|
|
|
@ -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).uniques()
|
let accountIDs = group.notifications.map(\.account.id)
|
||||||
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).uniques()
|
let accountIDs = group.notifications.map(\.account.id)
|
||||||
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).uniques()
|
let accountIDs = group.notifications.map(\.account.id)
|
||||||
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).uniques()
|
let accountIDs = group.notifications.map(\.account.id)
|
||||||
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)
|
||||||
|
|
|
@ -238,8 +238,7 @@ 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>
|
||||||
let dataSource = StatusAttachmentsGalleryDataSource(attachments: attachments, sourceViews: sourceViews, moreView: attachmentsView.moreView)
|
return GalleryVC.GalleryViewController(dataSource: StatusAttachmentsGalleryDataSource(attachments: attachments, sourceViews: sourceViews), initialItemIndex: index)
|
||||||
return GalleryVC.GalleryViewController(dataSource: dataSource, initialItemIndex: index)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func attachmentViewPresent(_ vc: UIViewController, animated: Bool) {
|
func attachmentViewPresent(_ vc: UIViewController, animated: Bool) {
|
||||||
|
|
|
@ -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()
|
||||||
private(set) var moreView: UIView?
|
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 {
|
||||||
|
|
|
@ -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, showResults: Bool, mastodonController: MastodonController) {
|
func updateUI(poll: Poll, option: Poll.Option, ownVoted: 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 showResults,
|
if (poll.voted ?? false) || poll.effectiveExpired,
|
||||||
let optionVotes = option.votesCount {
|
let optionVotes = option.votesCount {
|
||||||
let frac: CGFloat
|
let frac: CGFloat
|
||||||
if poll.multiple,
|
if poll.multiple,
|
||||||
|
|
|
@ -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, showResults: Bool) {
|
func updateUI(poll: Poll) {
|
||||||
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, showResults: showResults, mastodonController: mastodonController)
|
view.updateUI(poll: poll, option: opt, ownVoted: poll.ownVotes?.contains(index) ?? false, mastodonController: mastodonController)
|
||||||
view.checkboxIfInitialized?.readOnly = !isEnabled
|
view.checkboxIfInitialized?.readOnly = !isEnabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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 = .abbreviated
|
f.unitsStyle = .full
|
||||||
f.allowedUnits = [.weekOfMonth, .day, .hour, .minute]
|
f.allowedUnits = [.weekOfMonth, .day, .hour, .minute]
|
||||||
return f
|
return f
|
||||||
}()
|
}()
|
||||||
|
@ -25,10 +25,9 @@ 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: UIButton!
|
private var voteButton: PollVoteButton!
|
||||||
private var infoLabel: UILabel!
|
private var infoLabel: UILabel!
|
||||||
|
|
||||||
private var canVote = true
|
private var canVote = true
|
||||||
|
@ -64,11 +63,11 @@ class StatusPollView: UIView, StatusContentView {
|
||||||
infoLabel.adjustsFontSizeToFitWidth = true
|
infoLabel.adjustsFontSizeToFitWidth = true
|
||||||
addSubview(infoLabel)
|
addSubview(infoLabel)
|
||||||
|
|
||||||
voteButton = UIButton(configuration: .plain())
|
voteButton = PollVoteButton()
|
||||||
voteButton.translatesAutoresizingMaskIntoConstraints = false
|
voteButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
voteButton.addTarget(self, action: #selector(votePressed), for: .touchUpInside)
|
voteButton.addTarget(self, action: #selector(votePressed))
|
||||||
|
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([
|
||||||
|
@ -77,24 +76,20 @@ 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, constant: 4),
|
infoLabel.topAnchor.constraint(equalTo: optionsView.bottomAnchor),
|
||||||
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: infoLabel.topAnchor),
|
voteButton.topAnchor.constraint(equalTo: optionsView.bottomAnchor),
|
||||||
voteButton.bottomAnchor.constraint(equalTo: infoLabel.bottomAnchor),
|
voteButton.bottomAnchor.constraint(equalTo: 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
|
||||||
|
|
||||||
|
@ -109,17 +104,19 @@ class StatusPollView: UIView, StatusContentView {
|
||||||
|
|
||||||
optionsView.mastodonController = mastodonController
|
optionsView.mastodonController = mastodonController
|
||||||
optionsView.isEnabled = canVote
|
optionsView.isEnabled = canVote
|
||||||
optionsView.updateUI(poll: poll, showResults: showingResults || !canVote)
|
optionsView.updateUI(poll: poll)
|
||||||
|
|
||||||
|
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 = "No expiry"
|
expiryText = "Does not expire"
|
||||||
}
|
}
|
||||||
|
|
||||||
let format = NSLocalizedString("poll votes count", comment: "poll total votes count")
|
let format = NSLocalizedString("poll votes count", comment: "poll total votes count")
|
||||||
|
@ -128,7 +125,20 @@ class StatusPollView: UIView, StatusContentView {
|
||||||
infoLabel.text! += ", \(expiryText)"
|
infoLabel.text! += ", \(expiryText)"
|
||||||
}
|
}
|
||||||
|
|
||||||
updateVoteButton(status: status, poll: poll)
|
if expired {
|
||||||
|
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 {
|
||||||
|
@ -136,54 +146,11 @@ 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() {
|
||||||
if let status = mastodonController.persistentContainer.status(for: statusID),
|
voteButton.isEnabled = optionsView.checkedOptionIndices.count > 0
|
||||||
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
|
||||||
|
@ -191,6 +158,7 @@ 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()
|
||||||
|
@ -223,11 +191,4 @@ class StatusPollView: UIView, StatusContentView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showResults() {
|
|
||||||
showingResults = true
|
|
||||||
if let status = mastodonController.persistentContainer.status(for: statusID) {
|
|
||||||
updateUI(status: status, poll: status.poll)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -334,8 +334,7 @@ extension StatusCollectionViewCell {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
let sourceViews = attachmentsView.attachmentViews.copy() as! NSHashTable<AttachmentView>
|
let sourceViews = attachmentsView.attachmentViews.copy() as! NSHashTable<AttachmentView>
|
||||||
let dataSource = StatusAttachmentsGalleryDataSource(attachments: status.attachments, sourceViews: sourceViews, moreView: attachmentsView.moreView)
|
return GalleryVC.GalleryViewController(dataSource: StatusAttachmentsGalleryDataSource(attachments: status.attachments, sourceViews: sourceViews), initialItemIndex: index)
|
||||||
return GalleryVC.GalleryViewController(dataSource: dataSource, initialItemIndex: index)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func attachmentViewPresent(_ vc: UIViewController, animated: Bool) {
|
func attachmentViewPresent(_ vc: UIViewController, animated: Bool) {
|
||||||
|
|
|
@ -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 = 117
|
CURRENT_PROJECT_VERSION = 116
|
||||||
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
|
||||||
|
|
Loading…
Reference in New Issue