Add See Results button to polls

Closes #445
This commit is contained in:
Shadowfacts 2024-04-02 22:01:20 -04:00
parent 442f57bfc4
commit 9763edef47
5 changed files with 71 additions and 117 deletions

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

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