Compare commits
7 Commits
e522e30ce5
...
51db0066ac
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 51db0066ac | |
Shadowfacts | 9763edef47 | |
Shadowfacts | 442f57bfc4 | |
Shadowfacts | ae7101bb30 | |
Shadowfacts | 490d48c635 | |
Shadowfacts | 69ee3bb4f0 | |
Shadowfacts | 46b455c3d1 |
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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 */,
|
||||||
|
|
|
@ -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,9 +109,13 @@ class StatusAttachmentsGalleryDataSource: GalleryDataSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
func galleryContentTransitionSourceView(forItemAt index: Int) -> UIView? {
|
func galleryContentTransitionSourceView(forItemAt index: Int) -> UIView? {
|
||||||
|
if attachments.count > 4 && index >= 3 {
|
||||||
|
return moreView
|
||||||
|
} else {
|
||||||
let attachment = attachments[index]
|
let attachment = attachments[index]
|
||||||
return attachmentView(for: attachment)
|
return attachmentView(for: attachment)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func galleryApplicationActivities(forItemAt index: Int) -> [UIActivity]? {
|
func galleryApplicationActivities(forItemAt index: Int) -> [UIActivity]? {
|
||||||
[SaveToPhotosActivity()]
|
[SaveToPhotosActivity()]
|
||||||
|
|
|
@ -135,7 +135,9 @@ class ActionNotificationGroupCollectionViewCell: UICollectionViewListCell {
|
||||||
|
|
||||||
updateTimestamp()
|
updateTimestamp()
|
||||||
|
|
||||||
let people = group.notifications.compactMap {
|
let people = group.notifications
|
||||||
|
.uniques(by: \.account.id)
|
||||||
|
.compactMap {
|
||||||
mastodonController.persistentContainer.account(for: $0.account.id)
|
mastodonController.persistentContainer.account(for: $0.account.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue