From 9763edef479f455b0079796a316a3732a53bfca6 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 2 Apr 2024 22:01:20 -0400 Subject: [PATCH] Add See Results button to polls Closes #445 --- Tusker.xcodeproj/project.pbxproj | 4 -- Tusker/Views/Poll/PollOptionView.swift | 4 +- Tusker/Views/Poll/PollOptionsView.swift | 4 +- Tusker/Views/Poll/PollVoteButton.swift | 81 --------------------- Tusker/Views/Poll/StatusPollView.swift | 95 +++++++++++++++++-------- 5 files changed, 71 insertions(+), 117 deletions(-) delete mode 100644 Tusker/Views/Poll/PollVoteButton.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index cbdddb4b45..eb0694542d 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -255,7 +255,6 @@ D6B81F442560390300F6E31D /* MenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B81F432560390300F6E31D /* MenuController.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 */; }; - D6B9366B281EE77E00237D0E /* PollVoteButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B9366A281EE77E00237D0E /* PollVoteButton.swift */; }; D6B9366D2828445000237D0E /* SavedInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B9366C2828444F00237D0E /* SavedInstance.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 */; }; @@ -658,7 +657,6 @@ D6B81F432560390300F6E31D /* MenuController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuController.swift; sourceTree = ""; }; D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Visibility.swift"; sourceTree = ""; }; D6B93666281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSidebarMyProfileCollectionViewCell.swift; sourceTree = ""; }; - D6B9366A281EE77E00237D0E /* PollVoteButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollVoteButton.swift; sourceTree = ""; }; D6B9366C2828444F00237D0E /* SavedInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedInstance.swift; sourceTree = ""; }; D6B9366E2828452F00237D0E /* SavedHashtag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedHashtag.swift; sourceTree = ""; }; D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Helpers.swift"; sourceTree = ""; }; @@ -898,7 +896,6 @@ D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */, D623A5402635FB3C0095BD04 /* PollOptionView.swift */, D623A542263634100095BD04 /* PollOptionCheckboxView.swift */, - D6B9366A281EE77E00237D0E /* PollVoteButton.swift */, ); path = Poll; sourceTree = ""; @@ -2201,7 +2198,6 @@ D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */, 04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */, D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */, - D6B9366B281EE77E00237D0E /* PollVoteButton.swift in Sources */, D68A76DA29511CA6001DA1B3 /* AccountPreferences.swift in Sources */, D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */, D6895DC428D65342006341DA /* ConfirmReblogStatusPreviewView.swift in Sources */, diff --git a/Tusker/Views/Poll/PollOptionView.swift b/Tusker/Views/Poll/PollOptionView.swift index 38ec18c2a7..52831d6011 100644 --- a/Tusker/Views/Poll/PollOptionView.swift +++ b/Tusker/Views/Poll/PollOptionView.swift @@ -75,7 +75,7 @@ class PollOptionView: UIView { 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 if showCheckbox { checkbox.isChecked = ownVoted @@ -100,7 +100,7 @@ class PollOptionView: UIView { accessibilityLabel = option.title - if (poll.voted ?? false) || poll.effectiveExpired, + if showResults, let optionVotes = option.votesCount { let frac: CGFloat if poll.multiple, diff --git a/Tusker/Views/Poll/PollOptionsView.swift b/Tusker/Views/Poll/PollOptionsView.swift index b6d86f8c2d..431dfdedc2 100644 --- a/Tusker/Views/Poll/PollOptionsView.swift +++ b/Tusker/Views/Poll/PollOptionsView.swift @@ -65,7 +65,7 @@ class PollOptionsView: UIControl { fatalError("init(coder:) has not been implemented") } - func updateUI(poll: Poll) { + func updateUI(poll: Poll, showResults: Bool) { self.poll = poll if poll.options.count > options.count { @@ -81,7 +81,7 @@ class PollOptionsView: UIControl { } 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 } } diff --git a/Tusker/Views/Poll/PollVoteButton.swift b/Tusker/Views/Poll/PollVoteButton.swift deleted file mode 100644 index 60d289c8ac..0000000000 --- a/Tusker/Views/Poll/PollVoteButton.swift +++ /dev/null @@ -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 - } - } - -} diff --git a/Tusker/Views/Poll/StatusPollView.swift b/Tusker/Views/Poll/StatusPollView.swift index dfe566e22a..56485ff024 100644 --- a/Tusker/Views/Poll/StatusPollView.swift +++ b/Tusker/Views/Poll/StatusPollView.swift @@ -15,7 +15,7 @@ class StatusPollView: UIView, StatusContentView { let f = DateComponentsFormatter() f.includesTimeRemainingPhrase = true f.maximumUnitCount = 1 - f.unitsStyle = .full + f.unitsStyle = .abbreviated f.allowedUnits = [.weekOfMonth, .day, .hour, .minute] return f }() @@ -25,9 +25,10 @@ class StatusPollView: UIView, StatusContentView { private var statusID: String! private(set) var poll: Poll? + private var showingResults = false private var optionsView: PollOptionsView! - private var voteButton: PollVoteButton! + private var voteButton: UIButton! private var infoLabel: UILabel! private var canVote = true @@ -63,11 +64,11 @@ class StatusPollView: UIView, StatusContentView { infoLabel.adjustsFontSizeToFitWidth = true addSubview(infoLabel) - voteButton = PollVoteButton() + voteButton = UIButton(configuration: .plain()) voteButton.translatesAutoresizingMaskIntoConstraints = false - voteButton.addTarget(self, action: #selector(votePressed)) - voteButton.setFont(infoLabel.font) + voteButton.addTarget(self, action: #selector(votePressed), for: .touchUpInside) voteButton.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + voteButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) addSubview(voteButton) NSLayoutConstraint.activate([ @@ -76,20 +77,24 @@ class StatusPollView: UIView, StatusContentView { optionsView.topAnchor.constraint(equalTo: topAnchor), 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.trailingAnchor.constraint(equalTo: voteButton.leadingAnchor, constant: -8), voteButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 44), voteButton.trailingAnchor.constraint(equalTo: trailingAnchor), - voteButton.topAnchor.constraint(equalTo: optionsView.bottomAnchor), - voteButton.bottomAnchor.constraint(equalTo: bottomAnchor), + voteButton.topAnchor.constraint(equalTo: infoLabel.topAnchor), + voteButton.bottomAnchor.constraint(equalTo: infoLabel.bottomAnchor), ]) accessibilityElements = [optionsView!, infoLabel!, voteButton!] } func updateUI(status: StatusMO, poll: Poll?) { + if statusID != status.id { + showingResults = false + } + self.statusID = status.id self.poll = poll @@ -104,19 +109,17 @@ class StatusPollView: UIView, StatusContentView { optionsView.mastodonController = mastodonController optionsView.isEnabled = canVote - optionsView.updateUI(poll: poll) + optionsView.updateUI(poll: poll, showResults: showingResults || !canVote) - var expired = false let expiryText: String? if let expiresAt = poll.expiresAt { if expiresAt > Date() { expiryText = StatusPollView.formatter.string(from: Date(), to: expiresAt) } else { - expired = true expiryText = nil } } else { - expiryText = "Does not expire" + expiryText = "No expiry" } let format = NSLocalizedString("poll votes count", comment: "poll total votes count") @@ -125,20 +128,7 @@ class StatusPollView: UIView, StatusContentView { infoLabel.text! += ", \(expiryText)" } - 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 + updateVoteButton(status: status, poll: poll) } func estimateHeight(effectiveWidth: CGFloat) -> CGFloat { @@ -146,11 +136,54 @@ class StatusPollView: UIView, StatusContentView { 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() { - 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() { + if !optionsView.checkedOptionIndices.isEmpty { + doVote() + } else { + showResults() + } + } + + private func doVote() { guard let statusID, let poll else { return @@ -158,7 +191,6 @@ class StatusPollView: UIView, StatusContentView { optionsView.isEnabled = false voteButton.isEnabled = false - voteButton.disabledTitle = "Voted" #if !os(visionOS) 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) + } + } + }