diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 679041963..563dc13b9 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2405,6 +2405,8 @@ Tap the + to start adding people."; "poll_timeline_reply_ended_poll" = "Ended poll"; +"poll_timeline_loading" = "Loading..."; + // MARK: - Location sharing "location_sharing_title" = "Location"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 90aff5d8f..2e23e0457 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4927,6 +4927,10 @@ public class VectorL10n: NSObject { public static var pollTimelineEndedText: String { return VectorL10n.tr("Vector", "poll_timeline_ended_text") } + /// Loading... + public static var pollTimelineLoading: String { + return VectorL10n.tr("Vector", "poll_timeline_loading") + } /// Please try again public static var pollTimelineNotClosedSubtitle: String { return VectorL10n.tr("Vector", "poll_timeline_not_closed_subtitle") diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift index 09a8fb3c7..9c57bdabb 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift @@ -48,7 +48,7 @@ enum MockPollHistoryDetailScreenState: MockScreenState, CaseIterable { } var screenView: ([Any], AnyView) { - let timelineViewModel = TimelinePollViewModel(timelinePollDetails: poll) + let timelineViewModel = TimelinePollViewModel(timelinePollDetailsState: .loaded(poll)) let viewModel = PollHistoryDetailViewModel(poll: poll) return ([viewModel], AnyView(PollHistoryDetail(viewModel: viewModel.context, contentPoll: TimelinePollView(viewModel: timelineViewModel.context)))) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index 7f6d8c5f6..c4471844e 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -209,13 +209,13 @@ extension PollHistoryService: PollAggregatorDelegate { func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) { } func pollAggregatorDidEndLoading(_ aggregator: PollAggregator) { - guard let context = pollAggregationContexts[aggregator.poll.id], context.published == false else { + guard let poll = aggregator.poll, let context = pollAggregationContexts[poll.id], context.published == false else { return } context.published = true - let newPoll: TimelinePollDetails = .init(poll: aggregator.poll, represent: .started) + let newPoll: TimelinePollDetails = .init(poll: poll, represent: .started) if context.isLivePoll { livePollsSubject.send(newPoll) @@ -225,9 +225,9 @@ extension PollHistoryService: PollAggregatorDelegate { } func pollAggregatorDidUpdateData(_ aggregator: PollAggregator) { - guard let context = pollAggregationContexts[aggregator.poll.id], context.published else { + guard let poll = aggregator.poll, let context = pollAggregationContexts[poll.id], context.published else { return } - updatesSubject.send(.init(poll: aggregator.poll, represent: .started)) + updatesSubject.send(.init(poll: poll, represent: .started)) } } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift index 3214fae65..e2202524b 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift @@ -32,7 +32,7 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel private let parameters: TimelinePollCoordinatorParameters private let selectedAnswerIdentifiersSubject = PassthroughSubject<[String], Never>() - private var pollAggregator: PollAggregator + private var pollAggregator: PollAggregator! private(set) var viewModel: TimelinePollViewModelProtocol! private var cancellables = Set() @@ -46,10 +46,9 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel init(parameters: TimelinePollCoordinatorParameters) throws { self.parameters = parameters - try pollAggregator = PollAggregator(session: parameters.session, room: parameters.room, pollEvent: parameters.pollEvent) - pollAggregator.delegate = self + viewModel = TimelinePollViewModel(timelinePollDetailsState: .loading) + try pollAggregator = PollAggregator(session: parameters.session, room: parameters.room, pollEvent: parameters.pollEvent, delegate: self) - viewModel = TimelinePollViewModel(timelinePollDetails: buildTimelinePollFrom(pollAggregator.poll)) viewModel.completion = { [weak self] result in guard let self = self else { return } @@ -92,11 +91,11 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel } func canEndPoll() -> Bool { - pollAggregator.poll.isClosed == false + pollAggregator.poll?.isClosed == false } func canEditPoll() -> Bool { - pollAggregator.poll.isClosed == false && pollAggregator.poll.totalAnswerCount == 0 + pollAggregator.poll?.isClosed == false && pollAggregator.poll?.totalAnswerCount == 0 } func endPoll() { @@ -108,14 +107,23 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel // MARK: - PollAggregatorDelegate func pollAggregatorDidUpdateData(_ aggregator: PollAggregator) { - viewModel.updateWithPollDetails(buildTimelinePollFrom(aggregator.poll)) + if let poll = aggregator.poll { + viewModel.updateWithPollDetailsState(.loaded(buildTimelinePollFrom(poll))) + } } func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) { } - func pollAggregatorDidEndLoading(_ aggregator: PollAggregator) { } + func pollAggregatorDidEndLoading(_ aggregator: PollAggregator) { + guard let poll = aggregator.poll else { + return + } + viewModel.updateWithPollDetailsState(.loaded(buildTimelinePollFrom(poll))) + } - func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) { } + func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) { + viewModel.updateWithPollDetailsState(.errored) + } // MARK: - Private diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift index a36a7d092..2fd2b032f 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift @@ -41,111 +41,134 @@ class TimelinePollViewModelTests: XCTestCase { hasBeenEdited: false, hasDecryptionError: false) - viewModel = TimelinePollViewModel(timelinePollDetails: timelinePoll) + viewModel = TimelinePollViewModel(timelinePollDetailsState: .loaded(timelinePoll)) context = viewModel.context } func testInitialState() { - XCTAssertEqual(context.viewState.poll.answerOptions.count, 3) - XCTAssertFalse(context.viewState.poll.closed) - XCTAssertEqual(context.viewState.poll.type, .disclosed) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions.count, 3) + XCTAssertEqual(context.viewState.pollState.poll?.closed, false) + XCTAssertEqual(context.viewState.pollState.poll?.type, .disclosed) } func testSingleSelectionOnMax1Allowed() { context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) - - XCTAssertTrue(context.viewState.poll.answerOptions[0].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[1].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, true) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, false) } func testSingleReselectionOnMax1Allowed() { context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) - - XCTAssertTrue(context.viewState.poll.answerOptions[0].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[1].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, true) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, false) } func testMultipleSelectionOnMax1Allowed() { context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) context.send(viewAction: .selectAnswerOptionWithIdentifier("3")) - - XCTAssertFalse(context.viewState.poll.answerOptions[0].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[1].selected) - XCTAssertTrue(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, true) } func testMultipleReselectionOnMax1Allowed() { context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) context.send(viewAction: .selectAnswerOptionWithIdentifier("3")) context.send(viewAction: .selectAnswerOptionWithIdentifier("3")) - - XCTAssertFalse(context.viewState.poll.answerOptions[0].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[1].selected) - XCTAssertTrue(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, true) } func testClosedSelection() { - viewModel.state.poll.closed = true + guard case var .loaded(poll) = context.viewState.pollState else { + return XCTFail() + } + poll.closed = true + viewModel.updateWithPollDetailsState(.loaded(poll)) context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) context.send(viewAction: .selectAnswerOptionWithIdentifier("3")) - XCTAssertFalse(context.viewState.poll.answerOptions[0].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[1].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, false) } func testSingleSelectionOnMax2Allowed() { - viewModel.state.poll.maxAllowedSelections = 2 + guard case var .loaded(poll) = context.viewState.pollState else { + return XCTFail() + } + poll.maxAllowedSelections = 2 + viewModel.updateWithPollDetailsState(.loaded(poll)) context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) - XCTAssertTrue(context.viewState.poll.answerOptions[0].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[1].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, true) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, false) } func testSingleReselectionOnMax2Allowed() { - viewModel.state.poll.maxAllowedSelections = 2 + guard case var .loaded(poll) = context.viewState.pollState else { + return XCTFail() + } + poll.maxAllowedSelections = 2 + viewModel.updateWithPollDetailsState(.loaded(poll)) context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) - XCTAssertFalse(context.viewState.poll.answerOptions[0].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[1].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, false) } func testMultipleSelectionOnMax2Allowed() { - viewModel.state.poll.maxAllowedSelections = 2 - + guard case var .loaded(poll) = context.viewState.pollState else { + return XCTFail() + } + poll.maxAllowedSelections = 2 + viewModel.updateWithPollDetailsState(.loaded(poll)) + context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) context.send(viewAction: .selectAnswerOptionWithIdentifier("3")) context.send(viewAction: .selectAnswerOptionWithIdentifier("2")) - XCTAssertTrue(context.viewState.poll.answerOptions[0].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[1].selected) - XCTAssertTrue(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, true) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, true) context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) - XCTAssertFalse(context.viewState.poll.answerOptions[0].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[1].selected) - XCTAssertTrue(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, true) context.send(viewAction: .selectAnswerOptionWithIdentifier("2")) - XCTAssertFalse(context.viewState.poll.answerOptions[0].selected) - XCTAssertTrue(context.viewState.poll.answerOptions[1].selected) - XCTAssertTrue(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, true) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, true) context.send(viewAction: .selectAnswerOptionWithIdentifier("3")) - XCTAssertFalse(context.viewState.poll.answerOptions[0].selected) - XCTAssertTrue(context.viewState.poll.answerOptions[1].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, true) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, false) + } +} + +private extension TimelinePollDetailsState { + var poll: TimelinePollDetails? { + switch self { + case .loaded(let poll): + return poll + default: + return nil + } } } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift index 0ee87c55f..6b2d52c78 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift @@ -37,6 +37,12 @@ enum TimelinePollEventType { case ended } +enum TimelinePollDetailsState { + case loading + case loaded(TimelinePollDetails) + case errored +} + struct TimelinePollAnswerOption: Identifiable { var id: String var text: String @@ -94,7 +100,7 @@ struct TimelinePollDetails { extension TimelinePollDetails: Identifiable { } struct TimelinePollViewState: BindableState { - var poll: TimelinePollDetails + var pollState: TimelinePollDetailsState var bindings: TimelinePollViewStateBindings } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift index 8c70b21e3..c81d78683 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift @@ -23,6 +23,9 @@ enum MockTimelinePollScreenState: MockScreenState, CaseIterable { case openUndisclosed case closedUndisclosed case closedPollEnded + case loading + case invalidStartEvent + case withAlert var screenType: Any.Type { TimelinePollDetails.self @@ -45,7 +48,20 @@ enum MockTimelinePollScreenState: MockScreenState, CaseIterable { hasBeenEdited: false, hasDecryptionError: false) - let viewModel = TimelinePollViewModel(timelinePollDetails: poll) + let viewModel: TimelinePollViewModel + + switch self { + case .loading: + viewModel = TimelinePollViewModel(timelinePollDetailsState: .loading) + case .invalidStartEvent: + viewModel = TimelinePollViewModel(timelinePollDetailsState: .errored) + default: + viewModel = TimelinePollViewModel(timelinePollDetailsState: .loaded(poll)) + } + + if self == .withAlert { + viewModel.showAnsweringFailure() + } return ([viewModel], AnyView(TimelinePollView(viewModel: viewModel.context))) } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift index a86862cf4..26ac65a68 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift @@ -30,8 +30,8 @@ class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelPro // MARK: - Setup - init(timelinePollDetails: TimelinePollDetails) { - super.init(initialViewState: TimelinePollViewState(poll: timelinePollDetails, bindings: TimelinePollViewStateBindings())) + init(timelinePollDetailsState: TimelinePollDetailsState) { + super.init(initialViewState: TimelinePollViewState(pollState: timelinePollDetailsState, bindings: TimelinePollViewStateBindings())) } // MARK: - Public @@ -40,11 +40,11 @@ class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelPro switch viewAction { // Update local state. An update will be pushed from the coordinator once sent. case .selectAnswerOptionWithIdentifier(let identifier): - guard !state.poll.closed else { + // only if the poll is ready and not closed + guard case let .loaded(poll) = state.pollState, !poll.closed else { return } - - if state.poll.maxAllowedSelections == 1 { + if poll.maxAllowedSelections == 1 { updateSingleSelectPollLocalState(selectedAnswerIdentifier: identifier, callback: completion) } else { updateMultiSelectPollLocalState(&state, selectedAnswerIdentifier: identifier, callback: completion) @@ -54,8 +54,8 @@ class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelPro // MARK: - TimelinePollViewModelProtocol - func updateWithPollDetails(_ pollDetails: TimelinePollDetails) { - state.poll = pollDetails + func updateWithPollDetailsState(_ pollDetailsState: TimelinePollDetailsState) { + state.pollState = pollDetailsState } func showAnsweringFailure() { @@ -73,33 +73,40 @@ class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelPro // MARK: - Private func updateSingleSelectPollLocalState(selectedAnswerIdentifier: String, callback: TimelinePollViewModelCallback?) { - state.poll.answerOptions.updateEach { answerOption in + guard case var .loaded(poll) = state.pollState else { return } + + var pollAnswerOptions = poll.answerOptions + pollAnswerOptions.updateEach { answerOption in if answerOption.selected { answerOption.selected = false answerOption.count = UInt(max(0, Int(answerOption.count) - 1)) - state.poll.totalAnswerCount = UInt(max(0, Int(state.poll.totalAnswerCount) - 1)) + poll.totalAnswerCount = UInt(max(0, Int(poll.totalAnswerCount) - 1)) } if answerOption.id == selectedAnswerIdentifier { answerOption.selected = true answerOption.count += 1 - state.poll.totalAnswerCount += 1 + poll.totalAnswerCount += 1 } } - + poll.answerOptions = pollAnswerOptions + state.pollState = .loaded(poll) informCoordinatorOfSelectionUpdate(state: state, callback: callback) } func updateMultiSelectPollLocalState(_ state: inout TimelinePollViewState, selectedAnswerIdentifier: String, callback: TimelinePollViewModelCallback?) { - let selectedAnswerOptions = state.poll.answerOptions.filter { $0.selected == true } + guard case .loaded(var poll) = state.pollState else { return } + + let selectedAnswerOptions = poll.answerOptions.filter { $0.selected == true } let isDeselecting = selectedAnswerOptions.filter { $0.id == selectedAnswerIdentifier }.count > 0 - if !isDeselecting, selectedAnswerOptions.count >= state.poll.maxAllowedSelections { + if !isDeselecting, selectedAnswerOptions.count >= poll.maxAllowedSelections { return } - state.poll.answerOptions.updateEach { answerOption in + var pollAnswerOptions = poll.answerOptions + pollAnswerOptions.updateEach { answerOption in if answerOption.id != selectedAnswerIdentifier { return } @@ -107,22 +114,24 @@ class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelPro if answerOption.selected { answerOption.selected = false answerOption.count = UInt(max(0, Int(answerOption.count) - 1)) - state.poll.totalAnswerCount = UInt(max(0, Int(state.poll.totalAnswerCount) - 1)) + poll.totalAnswerCount = UInt(max(0, Int(poll.totalAnswerCount) - 1)) } else { answerOption.selected = true answerOption.count += 1 - state.poll.totalAnswerCount += 1 + poll.totalAnswerCount += 1 } } - + poll.answerOptions = pollAnswerOptions + state.pollState = .loaded(poll) informCoordinatorOfSelectionUpdate(state: state, callback: callback) } func informCoordinatorOfSelectionUpdate(state: TimelinePollViewState, callback: TimelinePollViewModelCallback?) { - let selectedIdentifiers = state.poll.answerOptions.compactMap { answerOption in + guard case .loaded(let poll) = state.pollState else { return } + + let selectedIdentifiers = poll.answerOptions.compactMap { answerOption in answerOption.selected ? answerOption.id : nil } - callback?(.selectedAnswerOptionsWithIdentifiers(selectedIdentifiers)) } } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModelProtocol.swift index 492f7f7a3..ade681438 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModelProtocol.swift @@ -20,7 +20,7 @@ protocol TimelinePollViewModelProtocol { var context: TimelinePollViewModelType.Context { get } var completion: ((TimelinePollViewModelResult) -> Void)? { get set } - func updateWithPollDetails(_ pollDetails: TimelinePollDetails) + func updateWithPollDetailsState(_ pollDetailsState: TimelinePollDetailsState) func showAnsweringFailure() func showClosingFailure() } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift index 2109a0e8a..52533288c 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift @@ -28,8 +28,23 @@ struct TimelinePollView: View { @ObservedObject var viewModel: TimelinePollViewModel.Context var body: some View { - let poll = viewModel.viewState.poll - + Group { + switch viewModel.viewState.pollState { + case .loading: + TimelinePollMessageView(message: VectorL10n.pollTimelineLoading) + case .loaded(let poll): + pollContent(poll) + case .errored: + TimelinePollMessageView(message: VectorL10n.pollTimelineReplyEndedPoll) + } + } + .alert(item: $viewModel.alertInfo) { info in + info.alert + } + } + + @ViewBuilder + private func pollContent(_ poll: TimelinePollDetails) -> some View { VStack(alignment: .leading, spacing: 16.0) { if poll.representsPollEndedEvent { Text(VectorL10n.pollTimelineEndedText) @@ -40,7 +55,7 @@ struct TimelinePollView: View { Text(poll.question) .font(theme.fonts.bodySB) .foregroundColor(theme.colors.primaryContent) + - Text(editedText) + Text(editedText(poll)) .font(theme.fonts.footnote) .foregroundColor(theme.colors.secondaryContent) @@ -54,21 +69,16 @@ struct TimelinePollView: View { .disabled(poll.closed) .fixedSize(horizontal: false, vertical: true) - Text(totalVotesString) + Text(totalVotesString(poll)) .lineLimit(2) .font(theme.fonts.footnote) .foregroundColor(theme.colors.tertiaryContent) } .padding([.horizontal, .top], 2.0) .padding([.bottom]) - .alert(item: $viewModel.alertInfo) { info in - info.alert - } } - private var totalVotesString: String { - let poll = viewModel.viewState.poll - + private func totalVotesString(_ poll: TimelinePollDetails) -> String { if poll.hasDecryptionError, poll.totalAnswerCount > 0 { return VectorL10n.pollTimelineDecryptionError } @@ -95,8 +105,8 @@ struct TimelinePollView: View { } } - private var editedText: String { - viewModel.viewState.poll.hasBeenEdited ? " \(VectorL10n.eventFormatterMessageEditedMention)" : "" + private func editedText(_ poll: TimelinePollDetails) -> String { + poll.hasBeenEdited ? " \(VectorL10n.eventFormatterMessageEditedMention)" : "" } } diff --git a/changelog.d/7497.bugfix b/changelog.d/7497.bugfix new file mode 100644 index 000000000..a8558b843 --- /dev/null +++ b/changelog.d/7497.bugfix @@ -0,0 +1 @@ +Poll: The timeline sometimes displayed closed polls in the wrong order.