Refine polls UI in the timeline (#1474)

* Add summary view in PollRoomTimelineView

* Add selectedPollOption action

* Handle undisclosed polls

* Add more logics in PollRoomTimelineView

* Refine ended poll UI

* Refine poll layout

* More UI refinements

* Fix layout issue

* Refine progress bar

* Add poll mocks and UI tests

* Cleanup
This commit is contained in:
Alfonso Grillo
2023-08-11 12:25:16 +02:00
committed by GitHub
parent f64159670b
commit 25f66d2557
27 changed files with 465 additions and 144 deletions
@@ -18,19 +18,26 @@ import SwiftUI
struct PollOptionView: View {
let pollOption: Poll.Option
let showVotes: Bool
let isFinalResult: Bool
var body: some View {
HStack(alignment: .top, spacing: 8) {
HStack(alignment: .firstTextBaseline, spacing: 12) {
FormRowAccessory(kind: .multipleSelection(isSelected: pollOption.isSelected))
VStack(spacing: 10) {
HStack(alignment: .lastTextBaseline) {
Text(pollOption.text)
.font(isFinalWinningOption ? .compound.bodyLGSemibold : .compound.bodyLG)
.multilineTextAlignment(.leading)
.foregroundColor(.compound.textPrimary)
.frame(maxWidth: .infinity, alignment: .leading)
Text(L10n.commonPollVotesCount(pollOption.votes))
.font(.compound.bodySM)
.foregroundColor(.compound.textSecondary)
if showVotes {
Text(L10n.commonPollVotesCount(pollOption.votes))
.font(isFinalWinningOption ? .compound.bodySMSemibold : .compound.bodySM)
.foregroundColor(isFinalWinningOption ? .compound.textPrimary : .compound.textSecondary)
}
}
progressView
@@ -41,8 +48,39 @@ struct PollOptionView: View {
// MARK: - Private
private var progressView: some View {
ProgressView(value: Double(pollOption.votes) / Double(pollOption.allVotes))
.progressViewStyle(LinearProgressViewStyle(tint: .compound.textPrimary))
PollProgressView(progress: progress)
}
private var progress: Double {
switch (showVotes, pollOption.allVotes, pollOption.isSelected) {
case (true, let allVotes, _) where allVotes > 0:
return Double(pollOption.votes) / Double(allVotes)
case (false, _, true):
return 1
default:
return 0
}
}
private var isFinalWinningOption: Bool {
pollOption.isWinning && isFinalResult
}
}
private struct PollProgressView: View {
let progress: Double
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
Capsule()
.foregroundColor(.compound.borderDisabled)
Capsule()
.frame(maxWidth: progress * geometry.size.width)
}
}
.frame(height: 6)
}
}
@@ -54,13 +92,28 @@ struct PollOptionView_Previews: PreviewProvider {
text: "Italian 🇮🇹",
votes: 1,
allVotes: 10,
isSelected: true))
isSelected: true,
isWinning: false),
showVotes: false,
isFinalResult: false)
PollOptionView(pollOption: .init(id: "2",
text: "Chinese 🇨🇳",
votes: 9,
allVotes: 10,
isSelected: false))
isSelected: false,
isWinning: true),
showVotes: true,
isFinalResult: false)
PollOptionView(pollOption: .init(id: "2",
text: "Chinese 🇨🇳",
votes: 9,
allVotes: 10,
isSelected: false,
isWinning: true),
showVotes: true,
isFinalResult: true)
}
.padding()
}
@@ -19,6 +19,8 @@ import SwiftUI
struct PollRoomTimelineView: View {
let timelineItem: PollRoomTimelineItem
@Environment(\.timelineStyle) var timelineStyle
@EnvironmentObject private var context: RoomScreenViewModel.Context
@ScaledMetric private var summaryPadding = 32
var body: some View {
TimelineStyler(timelineItem: timelineItem) {
@@ -26,10 +28,18 @@ struct PollRoomTimelineView: View {
questionView
ForEach(poll.options, id: \.id) { option in
Button { } label: {
PollOptionView(pollOption: option)
Button {
context.send(viewAction: .selectedPollOption(poll: poll, optionID: option.id))
} label: {
PollOptionView(pollOption: option,
showVotes: showVotes,
isFinalResult: poll.hasEnded)
.foregroundColor(progressBarColor(for: option))
}
.disabled(poll.hasEnded)
}
summaryView
}
.frame(maxWidth: 450)
}
@@ -42,50 +52,104 @@ struct PollRoomTimelineView: View {
}
private var questionView: some View {
HStack(spacing: 4) {
Image(Asset.Images.equalizer.name)
HStack(alignment: .top, spacing: 12) {
Image(Asset.Images.timelinePoll.name)
Text(poll.question)
.multilineTextAlignment(.leading)
.font(.compound.bodyLGSemibold)
}
}
@ViewBuilder
private var summaryView: some View {
if let summaryText = poll.summaryText {
Text(summaryText)
.font(.compound.bodySM)
.padding(.leading, showVotes ? 0 : summaryPadding)
.foregroundColor(.compound.textSecondary)
.frame(maxWidth: .infinity, alignment: showVotes ? .trailing : .leading)
}
}
private func progressBarColor(for option: Poll.Option) -> Color {
if poll.hasEnded {
return option.isWinning ? .compound.textActionAccent : .compound.textDisabled
} else {
return .compound.textPrimary
}
}
private var showVotes: Bool {
poll.hasEnded || poll.kind == .disclosed
}
}
private extension Poll {
var summaryText: String? {
guard !hasEnded else {
return options.first.map {
L10n.commonPollFinalVotes($0.allVotes)
}
}
switch kind {
case .disclosed:
return options.first.map {
L10n.commonPollTotalVotes($0.allVotes)
}
case .undisclosed:
return L10n.commonPollUndisclosedText
}
}
var hasEnded: Bool {
endDate != nil
}
}
struct PollRoomTimelineView_Previews: PreviewProvider {
static let viewModel = RoomScreenViewModel.mock
static var previews: some View {
PollRoomTimelineView(timelineItem: .init(id: .random,
poll: .mock,
body: "Foo",
timestamp: "Now",
isOutgoing: false,
isEditable: false,
sender: .init(id: "Bob"),
properties: .init()))
PollRoomTimelineView(timelineItem: .mock(poll: .disclosed))
.environment(\.timelineStyle, .bubbles)
.environmentObject(viewModel.context)
.previewDisplayName("Poll bubble style")
.previewDisplayName("Disclosed, Bubble")
PollRoomTimelineView(timelineItem: .init(id: .random,
poll: .mock,
body: "Foo",
timestamp: "Now",
isOutgoing: false,
isEditable: false,
sender: .init(id: "Bob"),
properties: .init()))
PollRoomTimelineView(timelineItem: .mock(poll: .undisclosed))
.environment(\.timelineStyle, .bubbles)
.environmentObject(viewModel.context)
.previewDisplayName("Undisclosed, Bubble")
PollRoomTimelineView(timelineItem: .mock(poll: .endedDisclosed))
.environment(\.timelineStyle, .bubbles)
.environmentObject(viewModel.context)
.previewDisplayName("Ended, Disclosed, Bubble")
PollRoomTimelineView(timelineItem: .mock(poll: .endedUndisclosed))
.environment(\.timelineStyle, .bubbles)
.environmentObject(viewModel.context)
.previewDisplayName("Ended, Undisclosed, Bubble")
PollRoomTimelineView(timelineItem: .mock(poll: .disclosed))
.environment(\.timelineStyle, .plain)
.environmentObject(viewModel.context)
.previewDisplayName("Poll plain style")
.previewDisplayName("Disclosed, Plain")
PollRoomTimelineView(timelineItem: .mock(poll: .undisclosed))
.environment(\.timelineStyle, .plain)
.environmentObject(viewModel.context)
.previewDisplayName("Undisclosed, Plain")
PollRoomTimelineView(timelineItem: .mock(poll: .endedDisclosed))
.environment(\.timelineStyle, .plain)
.environmentObject(viewModel.context)
.previewDisplayName("Ended, Disclosed, Plain")
PollRoomTimelineView(timelineItem: .mock(poll: .endedUndisclosed))
.environment(\.timelineStyle, .plain)
.environmentObject(viewModel.context)
.previewDisplayName("Ended, Undisclosed, Plain")
}
}
private extension Poll {
static let mock: Self = .init(question: "Do you like polls?",
pollKind: .disclosed,
maxSelections: 1,
options: [.init(id: "1", text: "Yes", votes: 1, allVotes: 3, isSelected: true), .init(id: "2", text: "No", votes: 2, allVotes: 3, isSelected: false)],
votes: [:],
endDate: nil)
}