mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-28 15:22:39 +00:00
Merge pull request #7518 from vector-im/nimau/7497_timeline_closed_polls
Fix: allow to render a TimelinePoll even if the poll is loading
This commit is contained in:
commit
ef4dc46c97
12 changed files with 173 additions and 94 deletions
|
@ -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";
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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))))
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<AnyCancellable>()
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)" : ""
|
||||
}
|
||||
}
|
||||
|
||||
|
|
1
changelog.d/7497.bugfix
Normal file
1
changelog.d/7497.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Poll: The timeline sometimes displayed closed polls in the wrong order.
|
Loading…
Reference in a new issue