RoomScreenViewModel is now TimelineViewModel (#3157)

Co-authored-by: Stefan Ceriu <stefanc@matrix.org>
This commit is contained in:
Mauro
2024-08-13 13:36:40 +02:00
committed by GitHub
parent 4519275559
commit 1ad361a6e8
88 changed files with 2266 additions and 1973 deletions
@@ -0,0 +1,134 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Compound
import SwiftUI
struct PollOptionView: View {
@Environment(\.isEnabled) private var isEnabled
let pollOption: Poll.Option
let showVotes: Bool
let isFinalResult: Bool
var body: some View {
HStack(alignment: .firstTextBaseline, spacing: 12) {
Image(systemSymbol: pollOption.isSelected ? .checkmarkCircleFill : .circle)
.font(.compound.bodyLG)
.foregroundColor(pollOption.isSelected && isEnabled ? .compound.iconPrimary : .compound.iconTertiary)
.accessibilityAddTraits(pollOption.isSelected ? .isSelected : [])
VStack(spacing: 10) {
HStack(alignment: .lastTextBaseline, spacing: 8) {
Text(pollOption.text)
.font(isFinalWinningOption ? .compound.bodyLGSemibold : .compound.bodyLG)
.multilineTextAlignment(.leading)
.foregroundColor(.compound.textPrimary)
.frame(maxWidth: .infinity, alignment: .leading)
if showVotes {
if isFinalWinningOption {
HStack(spacing: 4) {
CompoundIcon(asset: Asset.Images.pollWinner)
.foregroundColor(.compound.iconAccentTertiary)
Text(L10n.commonPollVotesCount(pollOption.votes))
.font(.compound.bodySMSemibold)
.foregroundColor(.compound.textPrimary)
}
} else {
Text(L10n.commonPollVotesCount(pollOption.votes))
.font(.compound.bodySM)
.foregroundColor(.compound.textSecondary)
}
}
}
PollProgressView(progress: progress)
}
}
}
// MARK: - Private
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._bgEmptyItemAlpha)
Capsule()
.frame(maxWidth: progress * geometry.size.width)
}
}
.frame(height: 6)
}
}
struct PollOptionView_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
VStack(spacing: 8) {
Group {
PollOptionView(pollOption: .init(id: "1",
text: "Italian 🇮🇹",
votes: 1,
allVotes: 10,
isSelected: true,
isWinning: false),
showVotes: false,
isFinalResult: false)
PollOptionView(pollOption: .init(id: "2",
text: "Chinese 🇨🇳",
votes: 9,
allVotes: 10,
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()
}
}
}
@@ -0,0 +1,173 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Compound
import SwiftUI
enum PollViewAction {
case selectOption(optionID: String)
case edit
case end
}
struct PollView: View {
private let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy)
let poll: Poll
let editable: Bool
let actionHandler: (PollViewAction) -> Void
var body: some View {
VStack(alignment: .leading, spacing: 16) {
questionView
optionsView
summaryView
toolbarView
}
.frame(maxWidth: 450)
}
// MARK: - Private
private var questionView: some View {
HStack(alignment: .top, spacing: 12) {
CompoundIcon(poll.hasEnded ? \.pollsEnd : \.polls,
size: .custom(22),
relativeTo: .compound.bodyLGSemibold)
.accessibilityHidden(true)
Text(poll.question)
.multilineTextAlignment(.leading)
.font(.compound.bodyLGSemibold)
}
}
private var optionsView: some View {
ForEach(poll.options, id: \.id) { option in
Button {
guard !option.isSelected else { return }
actionHandler(.selectOption(optionID: option.id))
feedbackGenerator.impactOccurred()
} label: {
PollOptionView(pollOption: option,
showVotes: showVotes,
isFinalResult: poll.hasEnded)
.foregroundColor(progressBarColor(for: option))
}
.disabled(poll.hasEnded)
}
}
@ViewBuilder
private var summaryView: some View {
if let summaryText = poll.summaryText {
Text(summaryText)
.font(.compound.bodySM)
.scaledPadding(.leading, showVotes ? 0 : 32)
.foregroundColor(.compound.textSecondary)
.frame(maxWidth: .infinity, alignment: showVotes ? .trailing : .leading)
}
}
@ViewBuilder
private var toolbarView: some View {
if !poll.hasEnded, poll.createdByAccountOwner {
Button {
toolbarAction()
} label: {
Text(editable ? L10n.actionEditPoll : L10n.actionEndPoll)
.lineLimit(2, reservesSpace: false)
.font(.compound.bodyLGSemibold)
.foregroundColor(.compound.textOnSolidPrimary)
.padding(.horizontal, 24)
.padding(.vertical, 10)
.frame(maxWidth: .infinity)
.background {
Capsule()
.foregroundColor(.compound.bgActionPrimaryRest)
}
}
.padding(.top, 8)
}
}
private func toolbarAction() {
if editable {
actionHandler(.edit)
} else {
actionHandler(.end)
}
}
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.commonPollTotalVotes($0.allVotes)
}
}
switch kind {
case .disclosed:
return options.first.map {
L10n.commonPollTotalVotes($0.allVotes)
}
case .undisclosed:
return L10n.commonPollUndisclosedText
}
}
}
struct PollView_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
PollView(poll: .disclosed(), editable: false) { _ in }
.padding()
.previewDisplayName("Disclosed")
PollView(poll: .undisclosed(), editable: false) { _ in }
.padding()
.previewDisplayName("Undisclosed")
PollView(poll: .endedDisclosed, editable: false) { _ in }
.padding()
.previewDisplayName("Ended, Disclosed")
PollView(poll: .endedUndisclosed, editable: false) { _ in }
.padding()
.previewDisplayName("Ended, Undisclosed")
PollView(poll: .disclosed(createdByAccountOwner: true), editable: true) { _ in }
.padding()
.previewDisplayName("Creator, disclosed")
PollView(poll: .emptyDisclosed, editable: true) { _ in }
.padding()
.previewDisplayName("Creator, no votes")
}
}