Polls UI Polish

This commit is contained in:
kate-signal 2025-10-14 16:00:48 -04:00 committed by GitHub
parent aac8b1f636
commit 150e670b33
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 86 additions and 17 deletions

View File

@ -143,11 +143,11 @@ public class CVPollView: ManualStackView {
let circleSize = CGSize(square: 2)
var optionRowInnerStackConfig: CVStackViewConfig {
func buildOptionRowInnerStackConfig(voteLabelWidth: Double) -> CVStackViewConfig {
CVStackViewConfig(axis: .horizontal,
alignment: .leading,
spacing: 8,
layoutMargins: UIEdgeInsets(top: 2, leading: 0, bottom: 2, trailing: 30))
layoutMargins: UIEdgeInsets(top: 2, leading: 0, bottom: 2, trailing: voteLabelWidth))
}
}
@ -164,6 +164,22 @@ public class CVPollView: ManualStackView {
)
}
private static func localizedNumber(from votes: Int) -> String {
let formatter: NumberFormatter = {
let f = NumberFormatter()
f.numberStyle = .decimal
return f
}()
return formatter.string(from: NSNumber(value: votes))!
}
private static func voteLabelWidthWithPadding(localizedVotes: String) -> Double {
let attributes = [NSAttributedString.Key.font: UIFont.dynamicTypeBody]
let textSize = localizedVotes.size(withAttributes: attributes)
return textSize.width + 4
}
static func measure(
maxWidth: CGFloat,
measurementBuilder: CVCellMeasurement.Builder,
@ -226,9 +242,22 @@ public class CVPollView: ManualStackView {
lineBreakMode: .byWordWrapping
)
let maxOptionLabelWidth = (maxLabelWidth - (configurator.optionRowInnerStackConfig.layoutMargins.right +
configurator.checkBoxSize.width +
configurator.optionRowInnerStackConfig.spacing))
let hasLocalUserVoted = option.localUserHasVoted(localAci: state.localAci)
// When the poll is ended, the checkbox should be removed except for options
// the local user voted for. Those checkboxes should be shifted right.
// In order to make sure they don't overlap with vote count, we need to measure
// the vote count width and update the option row stack config trailing
// spacing accordingly.
let checkboxSize = poll.isEnded && !hasLocalUserVoted ? 0 : configurator.checkBoxSize.width
let localizedVotesString = localizedNumber(from: option.acis.count)
let voteLabelWidth = voteLabelWidthWithPadding(localizedVotes: localizedVotesString)
let innerStackConfig = configurator.buildOptionRowInnerStackConfig(voteLabelWidth: voteLabelWidth)
let maxOptionLabelWidth = (maxLabelWidth - (innerStackConfig.layoutMargins.right +
checkboxSize +
innerStackConfig.spacing))
let optionLabelTextSize = CVText.measureLabel(
config: optionTextConfig,
@ -241,11 +270,22 @@ public class CVPollView: ManualStackView {
width: maxOptionLabelWidth,
height: optionLabelTextSize.height
)
var subViewInfos: [ManualStackSubviewInfo] = []
if poll.isEnded {
subViewInfos = [optionRowSize.asManualSubviewInfo]
if hasLocalUserVoted {
subViewInfos.append(configurator.checkBoxSize.asManualSubviewInfo(hasFixedSize: true))
}
} else {
subViewInfos = [configurator.checkBoxSize.asManualSubviewInfo(hasFixedSize: true), optionRowSize.asManualSubviewInfo]
}
let optionRowInnerMeasurement = ManualStackView.measure(
config: configurator.optionRowInnerStackConfig,
config: innerStackConfig,
measurementBuilder: measurementBuilder,
measurementKey: measurementKey_optionRowInnerStack + String(option.optionIndex),
subviewInfos: [configurator.checkBoxSize.asManualSubviewInfo(hasFixedSize: true), optionRowSize.asManualSubviewInfo]
subviewInfos: subViewInfos
)
let progressBarSize = CGSize(width: maxLabelWidth, height: 8)
@ -343,7 +383,8 @@ public class CVPollView: ManualStackView {
voteType: voteType,
delegate: componentDelegate
)
}
},
pollIsEnded: poll.isEnded
)
optionSubviews.append(row)
}
@ -417,6 +458,7 @@ public class CVPollView: ManualStackView {
totalVotes: Int,
hasLocalUserAlreadyVoted: Bool,
pollVoteHandler: @escaping (VoteType) -> Void,
pollIsEnded: Bool
) {
self.pollVoteHandler = pollVoteHandler
@ -429,7 +471,8 @@ public class CVPollView: ManualStackView {
votes: pollOption.acis.count,
totalVotes: totalVotes,
hasLocalUserAlreadyVoted: hasLocalUserAlreadyVoted,
isPending: pollOption.isPending
isPending: pollOption.isPending,
pollIsEnded: pollIsEnded
)
}
@ -497,7 +540,8 @@ public class CVPollView: ManualStackView {
votes: Int,
totalVotes: Int,
hasLocalUserAlreadyVoted: Bool,
isPending: Bool
isPending: Bool,
pollIsEnded: Bool
) {
checkbox.setImage(UIImage(named: Theme.iconName(.checkCircleFill)), for: .selected)
checkbox.setImage(UIImage(named: Theme.iconName(.circle)), for: .normal)
@ -517,17 +561,32 @@ public class CVPollView: ManualStackView {
)
optionTextConfig.applyForRendering(label: optionText)
var subviews: [UIView] = []
if pollIsEnded {
checkbox.isUserInteractionEnabled = false
subviews = [optionText]
if hasLocalUserAlreadyVoted {
subviews.append(checkbox)
}
} else {
subviews = [checkbox, optionText]
}
let localizedVotesString = localizedNumber(from: votes)
let voteLabelWidth = voteLabelWidthWithPadding(localizedVotes: localizedVotesString)
let innerStackConfig = configurator.buildOptionRowInnerStackConfig(voteLabelWidth: voteLabelWidth)
innerStack.configure(
config: configurator.optionRowInnerStackConfig,
config: innerStackConfig,
cellMeasurement: cellMeasurement,
measurementKey: measurementKey_optionRowInnerStack + String(index),
subviews: [checkbox, optionText]
subviews: subviews
)
innerStackContainer.addSubviewToFillSuperviewEdges(innerStack)
let numVotesConfig = CVLabelConfig.unstyledText(
String(votes), // TODO: Localize number
localizedVotesString,
font: UIFont.dynamicTypeBody,
textColor: configurator.textColor,
numberOfLines: 0,

View File

@ -40,7 +40,7 @@ public class CVComponentPoll: CVComponentBase, CVComponent {
public func measure(maxWidth: CGFloat, measurementBuilder: SignalUI.CVCellMeasurement.Builder) -> CGSize {
owsAssertDebug(maxWidth > 0)
let maxWidth = min(maxWidth, conversationStyle.maxMediaMessageWidth)
let maxWidth = min(maxWidth, conversationStyle.maxMessageWidth)
return CVPollView.measure(maxWidth: maxWidth, measurementBuilder: measurementBuilder, state: poll.state)
}

View File

@ -57,6 +57,13 @@ struct PollDetailsView: View {
fileprivate let viewModel: PollDetailsViewModel
private var poll: OWSPoll
var titleString: String {
if poll.isEnded {
return OWSLocalizedString("POLL_RESULTS_TITLE", comment: "Title of poll details pane when poll is ended")
}
return OWSLocalizedString("POLL_DETAILS_TITLE", comment: "Title of poll details pane")
}
fileprivate init(poll: OWSPoll, viewModel: PollDetailsViewModel) {
self.poll = poll
self.viewModel = viewModel
@ -65,7 +72,7 @@ struct PollDetailsView: View {
var body: some View {
VStack(spacing: 0) {
ZStack {
Text(OWSLocalizedString("POLL_DETAILS_TITLE", comment: "Title of poll details pane"))
Text(titleString)
.font(.headline)
.fontWeight(.semibold)
.frame(maxWidth: .infinity, alignment: .center)
@ -147,7 +154,7 @@ struct PollDetailsView: View {
private func addressCell(address: SignalServiceAddress) -> ManualStackView? {
let cell = ContactCellView()
let config = ContactCellConfiguration(address: address, localUserDisplayMode: .noteToSelf)
let config = ContactCellConfiguration(address: address, localUserDisplayMode: .asLocalUser)
config.avatarSizeClass = .twentyEight
SSKEnvironment.shared.databaseStorageRef.read { transaction in

View File

@ -6521,7 +6521,7 @@
"POLL_DETAILS_TITLE" = "Poll Details";
/* Shown when another user ends a poll. Embeds {{ another user }} and {{ poll question }}. */
"POLL_ENDED_BY_OTHER_CHAT_LIST_UPDATE" = "%@ ended the poll: %@";
"POLL_ENDED_BY_OTHER_CHAT_LIST_UPDATE" = "%@ ended the poll: %@";
/* Notification that {{contact}} ended a poll with question {{poll question}} */
"POLL_ENDED_NOTIFICATION" = "%@ ended the poll “%@”";
@ -6550,6 +6550,9 @@
/* Placeholder text for poll question */
"POLL_QUESTION_PLACEHOLDER_TEXT" = "Ask a Question";
/* Title of poll details pane when poll is ended */
"POLL_RESULTS_TITLE" = "Poll Results";
/* Label specifying the user can select more than one option */
"POLL_SELECT_LABEL_MULTIPLE" = "Select one or more";