mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-28 23:32:41 +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_reply_ended_poll" = "Ended poll";
|
||||||
|
|
||||||
|
"poll_timeline_loading" = "Loading...";
|
||||||
|
|
||||||
// MARK: - Location sharing
|
// MARK: - Location sharing
|
||||||
|
|
||||||
"location_sharing_title" = "Location";
|
"location_sharing_title" = "Location";
|
||||||
|
|
|
@ -4927,6 +4927,10 @@ public class VectorL10n: NSObject {
|
||||||
public static var pollTimelineEndedText: String {
|
public static var pollTimelineEndedText: String {
|
||||||
return VectorL10n.tr("Vector", "poll_timeline_ended_text")
|
return VectorL10n.tr("Vector", "poll_timeline_ended_text")
|
||||||
}
|
}
|
||||||
|
/// Loading...
|
||||||
|
public static var pollTimelineLoading: String {
|
||||||
|
return VectorL10n.tr("Vector", "poll_timeline_loading")
|
||||||
|
}
|
||||||
/// Please try again
|
/// Please try again
|
||||||
public static var pollTimelineNotClosedSubtitle: String {
|
public static var pollTimelineNotClosedSubtitle: String {
|
||||||
return VectorL10n.tr("Vector", "poll_timeline_not_closed_subtitle")
|
return VectorL10n.tr("Vector", "poll_timeline_not_closed_subtitle")
|
||||||
|
|
|
@ -48,7 +48,7 @@ enum MockPollHistoryDetailScreenState: MockScreenState, CaseIterable {
|
||||||
}
|
}
|
||||||
|
|
||||||
var screenView: ([Any], AnyView) {
|
var screenView: ([Any], AnyView) {
|
||||||
let timelineViewModel = TimelinePollViewModel(timelinePollDetails: poll)
|
let timelineViewModel = TimelinePollViewModel(timelinePollDetailsState: .loaded(poll))
|
||||||
let viewModel = PollHistoryDetailViewModel(poll: poll)
|
let viewModel = PollHistoryDetailViewModel(poll: poll)
|
||||||
|
|
||||||
return ([viewModel], AnyView(PollHistoryDetail(viewModel: viewModel.context, contentPoll: TimelinePollView(viewModel: timelineViewModel.context))))
|
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 pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) { }
|
||||||
|
|
||||||
func pollAggregatorDidEndLoading(_ aggregator: PollAggregator) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
context.published = true
|
context.published = true
|
||||||
|
|
||||||
let newPoll: TimelinePollDetails = .init(poll: aggregator.poll, represent: .started)
|
let newPoll: TimelinePollDetails = .init(poll: poll, represent: .started)
|
||||||
|
|
||||||
if context.isLivePoll {
|
if context.isLivePoll {
|
||||||
livePollsSubject.send(newPoll)
|
livePollsSubject.send(newPoll)
|
||||||
|
@ -225,9 +225,9 @@ extension PollHistoryService: PollAggregatorDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func pollAggregatorDidUpdateData(_ aggregator: PollAggregator) {
|
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
|
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 parameters: TimelinePollCoordinatorParameters
|
||||||
private let selectedAnswerIdentifiersSubject = PassthroughSubject<[String], Never>()
|
private let selectedAnswerIdentifiersSubject = PassthroughSubject<[String], Never>()
|
||||||
|
|
||||||
private var pollAggregator: PollAggregator
|
private var pollAggregator: PollAggregator!
|
||||||
private(set) var viewModel: TimelinePollViewModelProtocol!
|
private(set) var viewModel: TimelinePollViewModelProtocol!
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
@ -46,10 +46,9 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel
|
||||||
init(parameters: TimelinePollCoordinatorParameters) throws {
|
init(parameters: TimelinePollCoordinatorParameters) throws {
|
||||||
self.parameters = parameters
|
self.parameters = parameters
|
||||||
|
|
||||||
try pollAggregator = PollAggregator(session: parameters.session, room: parameters.room, pollEvent: parameters.pollEvent)
|
viewModel = TimelinePollViewModel(timelinePollDetailsState: .loading)
|
||||||
pollAggregator.delegate = self
|
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
|
viewModel.completion = { [weak self] result in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
@ -92,11 +91,11 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel
|
||||||
}
|
}
|
||||||
|
|
||||||
func canEndPoll() -> Bool {
|
func canEndPoll() -> Bool {
|
||||||
pollAggregator.poll.isClosed == false
|
pollAggregator.poll?.isClosed == false
|
||||||
}
|
}
|
||||||
|
|
||||||
func canEditPoll() -> Bool {
|
func canEditPoll() -> Bool {
|
||||||
pollAggregator.poll.isClosed == false && pollAggregator.poll.totalAnswerCount == 0
|
pollAggregator.poll?.isClosed == false && pollAggregator.poll?.totalAnswerCount == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func endPoll() {
|
func endPoll() {
|
||||||
|
@ -108,14 +107,23 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel
|
||||||
// MARK: - PollAggregatorDelegate
|
// MARK: - PollAggregatorDelegate
|
||||||
|
|
||||||
func pollAggregatorDidUpdateData(_ aggregator: PollAggregator) {
|
func pollAggregatorDidUpdateData(_ aggregator: PollAggregator) {
|
||||||
viewModel.updateWithPollDetails(buildTimelinePollFrom(aggregator.poll))
|
if let poll = aggregator.poll {
|
||||||
|
viewModel.updateWithPollDetailsState(.loaded(buildTimelinePollFrom(poll)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) { }
|
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
|
// MARK: - Private
|
||||||
|
|
||||||
|
|
|
@ -41,111 +41,134 @@ class TimelinePollViewModelTests: XCTestCase {
|
||||||
hasBeenEdited: false,
|
hasBeenEdited: false,
|
||||||
hasDecryptionError: false)
|
hasDecryptionError: false)
|
||||||
|
|
||||||
viewModel = TimelinePollViewModel(timelinePollDetails: timelinePoll)
|
viewModel = TimelinePollViewModel(timelinePollDetailsState: .loaded(timelinePoll))
|
||||||
context = viewModel.context
|
context = viewModel.context
|
||||||
}
|
}
|
||||||
|
|
||||||
func testInitialState() {
|
func testInitialState() {
|
||||||
XCTAssertEqual(context.viewState.poll.answerOptions.count, 3)
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions.count, 3)
|
||||||
XCTAssertFalse(context.viewState.poll.closed)
|
XCTAssertEqual(context.viewState.pollState.poll?.closed, false)
|
||||||
XCTAssertEqual(context.viewState.poll.type, .disclosed)
|
XCTAssertEqual(context.viewState.pollState.poll?.type, .disclosed)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testSingleSelectionOnMax1Allowed() {
|
func testSingleSelectionOnMax1Allowed() {
|
||||||
context.send(viewAction: .selectAnswerOptionWithIdentifier("1"))
|
context.send(viewAction: .selectAnswerOptionWithIdentifier("1"))
|
||||||
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, true)
|
||||||
XCTAssertTrue(context.viewState.poll.answerOptions[0].selected)
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false)
|
||||||
XCTAssertFalse(context.viewState.poll.answerOptions[1].selected)
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, false)
|
||||||
XCTAssertFalse(context.viewState.poll.answerOptions[2].selected)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testSingleReselectionOnMax1Allowed() {
|
func testSingleReselectionOnMax1Allowed() {
|
||||||
context.send(viewAction: .selectAnswerOptionWithIdentifier("1"))
|
context.send(viewAction: .selectAnswerOptionWithIdentifier("1"))
|
||||||
context.send(viewAction: .selectAnswerOptionWithIdentifier("1"))
|
context.send(viewAction: .selectAnswerOptionWithIdentifier("1"))
|
||||||
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, true)
|
||||||
XCTAssertTrue(context.viewState.poll.answerOptions[0].selected)
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false)
|
||||||
XCTAssertFalse(context.viewState.poll.answerOptions[1].selected)
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, false)
|
||||||
XCTAssertFalse(context.viewState.poll.answerOptions[2].selected)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testMultipleSelectionOnMax1Allowed() {
|
func testMultipleSelectionOnMax1Allowed() {
|
||||||
context.send(viewAction: .selectAnswerOptionWithIdentifier("1"))
|
context.send(viewAction: .selectAnswerOptionWithIdentifier("1"))
|
||||||
context.send(viewAction: .selectAnswerOptionWithIdentifier("3"))
|
context.send(viewAction: .selectAnswerOptionWithIdentifier("3"))
|
||||||
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, false)
|
||||||
XCTAssertFalse(context.viewState.poll.answerOptions[0].selected)
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false)
|
||||||
XCTAssertFalse(context.viewState.poll.answerOptions[1].selected)
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, true)
|
||||||
XCTAssertTrue(context.viewState.poll.answerOptions[2].selected)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testMultipleReselectionOnMax1Allowed() {
|
func testMultipleReselectionOnMax1Allowed() {
|
||||||
context.send(viewAction: .selectAnswerOptionWithIdentifier("1"))
|
context.send(viewAction: .selectAnswerOptionWithIdentifier("1"))
|
||||||
context.send(viewAction: .selectAnswerOptionWithIdentifier("3"))
|
context.send(viewAction: .selectAnswerOptionWithIdentifier("3"))
|
||||||
context.send(viewAction: .selectAnswerOptionWithIdentifier("3"))
|
context.send(viewAction: .selectAnswerOptionWithIdentifier("3"))
|
||||||
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, false)
|
||||||
XCTAssertFalse(context.viewState.poll.answerOptions[0].selected)
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false)
|
||||||
XCTAssertFalse(context.viewState.poll.answerOptions[1].selected)
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, true)
|
||||||
XCTAssertTrue(context.viewState.poll.answerOptions[2].selected)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testClosedSelection() {
|
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("1"))
|
||||||
context.send(viewAction: .selectAnswerOptionWithIdentifier("3"))
|
context.send(viewAction: .selectAnswerOptionWithIdentifier("3"))
|
||||||
|
|
||||||
XCTAssertFalse(context.viewState.poll.answerOptions[0].selected)
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, false)
|
||||||
XCTAssertFalse(context.viewState.poll.answerOptions[1].selected)
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false)
|
||||||
XCTAssertFalse(context.viewState.poll.answerOptions[2].selected)
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testSingleSelectionOnMax2Allowed() {
|
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"))
|
context.send(viewAction: .selectAnswerOptionWithIdentifier("1"))
|
||||||
|
|
||||||
XCTAssertTrue(context.viewState.poll.answerOptions[0].selected)
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, true)
|
||||||
XCTAssertFalse(context.viewState.poll.answerOptions[1].selected)
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false)
|
||||||
XCTAssertFalse(context.viewState.poll.answerOptions[2].selected)
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testSingleReselectionOnMax2Allowed() {
|
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"))
|
||||||
context.send(viewAction: .selectAnswerOptionWithIdentifier("1"))
|
context.send(viewAction: .selectAnswerOptionWithIdentifier("1"))
|
||||||
|
|
||||||
XCTAssertFalse(context.viewState.poll.answerOptions[0].selected)
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, false)
|
||||||
XCTAssertFalse(context.viewState.poll.answerOptions[1].selected)
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false)
|
||||||
XCTAssertFalse(context.viewState.poll.answerOptions[2].selected)
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testMultipleSelectionOnMax2Allowed() {
|
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("1"))
|
||||||
context.send(viewAction: .selectAnswerOptionWithIdentifier("3"))
|
context.send(viewAction: .selectAnswerOptionWithIdentifier("3"))
|
||||||
context.send(viewAction: .selectAnswerOptionWithIdentifier("2"))
|
context.send(viewAction: .selectAnswerOptionWithIdentifier("2"))
|
||||||
|
|
||||||
XCTAssertTrue(context.viewState.poll.answerOptions[0].selected)
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, true)
|
||||||
XCTAssertFalse(context.viewState.poll.answerOptions[1].selected)
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false)
|
||||||
XCTAssertTrue(context.viewState.poll.answerOptions[2].selected)
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, true)
|
||||||
|
|
||||||
context.send(viewAction: .selectAnswerOptionWithIdentifier("1"))
|
context.send(viewAction: .selectAnswerOptionWithIdentifier("1"))
|
||||||
|
|
||||||
XCTAssertFalse(context.viewState.poll.answerOptions[0].selected)
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, false)
|
||||||
XCTAssertFalse(context.viewState.poll.answerOptions[1].selected)
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false)
|
||||||
XCTAssertTrue(context.viewState.poll.answerOptions[2].selected)
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, true)
|
||||||
|
|
||||||
context.send(viewAction: .selectAnswerOptionWithIdentifier("2"))
|
context.send(viewAction: .selectAnswerOptionWithIdentifier("2"))
|
||||||
|
|
||||||
XCTAssertFalse(context.viewState.poll.answerOptions[0].selected)
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, false)
|
||||||
XCTAssertTrue(context.viewState.poll.answerOptions[1].selected)
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, true)
|
||||||
XCTAssertTrue(context.viewState.poll.answerOptions[2].selected)
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, true)
|
||||||
|
|
||||||
context.send(viewAction: .selectAnswerOptionWithIdentifier("3"))
|
context.send(viewAction: .selectAnswerOptionWithIdentifier("3"))
|
||||||
|
|
||||||
XCTAssertFalse(context.viewState.poll.answerOptions[0].selected)
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, false)
|
||||||
XCTAssertTrue(context.viewState.poll.answerOptions[1].selected)
|
XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, true)
|
||||||
XCTAssertFalse(context.viewState.poll.answerOptions[2].selected)
|
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
|
case ended
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum TimelinePollDetailsState {
|
||||||
|
case loading
|
||||||
|
case loaded(TimelinePollDetails)
|
||||||
|
case errored
|
||||||
|
}
|
||||||
|
|
||||||
struct TimelinePollAnswerOption: Identifiable {
|
struct TimelinePollAnswerOption: Identifiable {
|
||||||
var id: String
|
var id: String
|
||||||
var text: String
|
var text: String
|
||||||
|
@ -94,7 +100,7 @@ struct TimelinePollDetails {
|
||||||
extension TimelinePollDetails: Identifiable { }
|
extension TimelinePollDetails: Identifiable { }
|
||||||
|
|
||||||
struct TimelinePollViewState: BindableState {
|
struct TimelinePollViewState: BindableState {
|
||||||
var poll: TimelinePollDetails
|
var pollState: TimelinePollDetailsState
|
||||||
var bindings: TimelinePollViewStateBindings
|
var bindings: TimelinePollViewStateBindings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,9 @@ enum MockTimelinePollScreenState: MockScreenState, CaseIterable {
|
||||||
case openUndisclosed
|
case openUndisclosed
|
||||||
case closedUndisclosed
|
case closedUndisclosed
|
||||||
case closedPollEnded
|
case closedPollEnded
|
||||||
|
case loading
|
||||||
|
case invalidStartEvent
|
||||||
|
case withAlert
|
||||||
|
|
||||||
var screenType: Any.Type {
|
var screenType: Any.Type {
|
||||||
TimelinePollDetails.self
|
TimelinePollDetails.self
|
||||||
|
@ -45,7 +48,20 @@ enum MockTimelinePollScreenState: MockScreenState, CaseIterable {
|
||||||
hasBeenEdited: false,
|
hasBeenEdited: false,
|
||||||
hasDecryptionError: 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)))
|
return ([viewModel], AnyView(TimelinePollView(viewModel: viewModel.context)))
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,8 +30,8 @@ class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelPro
|
||||||
|
|
||||||
// MARK: - Setup
|
// MARK: - Setup
|
||||||
|
|
||||||
init(timelinePollDetails: TimelinePollDetails) {
|
init(timelinePollDetailsState: TimelinePollDetailsState) {
|
||||||
super.init(initialViewState: TimelinePollViewState(poll: timelinePollDetails, bindings: TimelinePollViewStateBindings()))
|
super.init(initialViewState: TimelinePollViewState(pollState: timelinePollDetailsState, bindings: TimelinePollViewStateBindings()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public
|
// MARK: - Public
|
||||||
|
@ -40,11 +40,11 @@ class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelPro
|
||||||
switch viewAction {
|
switch viewAction {
|
||||||
// Update local state. An update will be pushed from the coordinator once sent.
|
// Update local state. An update will be pushed from the coordinator once sent.
|
||||||
case .selectAnswerOptionWithIdentifier(let identifier):
|
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
|
return
|
||||||
}
|
}
|
||||||
|
if poll.maxAllowedSelections == 1 {
|
||||||
if state.poll.maxAllowedSelections == 1 {
|
|
||||||
updateSingleSelectPollLocalState(selectedAnswerIdentifier: identifier, callback: completion)
|
updateSingleSelectPollLocalState(selectedAnswerIdentifier: identifier, callback: completion)
|
||||||
} else {
|
} else {
|
||||||
updateMultiSelectPollLocalState(&state, selectedAnswerIdentifier: identifier, callback: completion)
|
updateMultiSelectPollLocalState(&state, selectedAnswerIdentifier: identifier, callback: completion)
|
||||||
|
@ -54,8 +54,8 @@ class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelPro
|
||||||
|
|
||||||
// MARK: - TimelinePollViewModelProtocol
|
// MARK: - TimelinePollViewModelProtocol
|
||||||
|
|
||||||
func updateWithPollDetails(_ pollDetails: TimelinePollDetails) {
|
func updateWithPollDetailsState(_ pollDetailsState: TimelinePollDetailsState) {
|
||||||
state.poll = pollDetails
|
state.pollState = pollDetailsState
|
||||||
}
|
}
|
||||||
|
|
||||||
func showAnsweringFailure() {
|
func showAnsweringFailure() {
|
||||||
|
@ -73,33 +73,40 @@ class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelPro
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
func updateSingleSelectPollLocalState(selectedAnswerIdentifier: String, callback: TimelinePollViewModelCallback?) {
|
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 {
|
if answerOption.selected {
|
||||||
answerOption.selected = false
|
answerOption.selected = false
|
||||||
answerOption.count = UInt(max(0, Int(answerOption.count) - 1))
|
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 {
|
if answerOption.id == selectedAnswerIdentifier {
|
||||||
answerOption.selected = true
|
answerOption.selected = true
|
||||||
answerOption.count += 1
|
answerOption.count += 1
|
||||||
state.poll.totalAnswerCount += 1
|
poll.totalAnswerCount += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
poll.answerOptions = pollAnswerOptions
|
||||||
|
state.pollState = .loaded(poll)
|
||||||
informCoordinatorOfSelectionUpdate(state: state, callback: callback)
|
informCoordinatorOfSelectionUpdate(state: state, callback: callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateMultiSelectPollLocalState(_ state: inout TimelinePollViewState, selectedAnswerIdentifier: String, callback: TimelinePollViewModelCallback?) {
|
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
|
let isDeselecting = selectedAnswerOptions.filter { $0.id == selectedAnswerIdentifier }.count > 0
|
||||||
|
|
||||||
if !isDeselecting, selectedAnswerOptions.count >= state.poll.maxAllowedSelections {
|
if !isDeselecting, selectedAnswerOptions.count >= poll.maxAllowedSelections {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
state.poll.answerOptions.updateEach { answerOption in
|
var pollAnswerOptions = poll.answerOptions
|
||||||
|
pollAnswerOptions.updateEach { answerOption in
|
||||||
if answerOption.id != selectedAnswerIdentifier {
|
if answerOption.id != selectedAnswerIdentifier {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -107,22 +114,24 @@ class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelPro
|
||||||
if answerOption.selected {
|
if answerOption.selected {
|
||||||
answerOption.selected = false
|
answerOption.selected = false
|
||||||
answerOption.count = UInt(max(0, Int(answerOption.count) - 1))
|
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 {
|
} else {
|
||||||
answerOption.selected = true
|
answerOption.selected = true
|
||||||
answerOption.count += 1
|
answerOption.count += 1
|
||||||
state.poll.totalAnswerCount += 1
|
poll.totalAnswerCount += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
poll.answerOptions = pollAnswerOptions
|
||||||
|
state.pollState = .loaded(poll)
|
||||||
informCoordinatorOfSelectionUpdate(state: state, callback: callback)
|
informCoordinatorOfSelectionUpdate(state: state, callback: callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
func informCoordinatorOfSelectionUpdate(state: TimelinePollViewState, callback: TimelinePollViewModelCallback?) {
|
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
|
answerOption.selected ? answerOption.id : nil
|
||||||
}
|
}
|
||||||
|
|
||||||
callback?(.selectedAnswerOptionsWithIdentifiers(selectedIdentifiers))
|
callback?(.selectedAnswerOptionsWithIdentifiers(selectedIdentifiers))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ protocol TimelinePollViewModelProtocol {
|
||||||
var context: TimelinePollViewModelType.Context { get }
|
var context: TimelinePollViewModelType.Context { get }
|
||||||
var completion: ((TimelinePollViewModelResult) -> Void)? { get set }
|
var completion: ((TimelinePollViewModelResult) -> Void)? { get set }
|
||||||
|
|
||||||
func updateWithPollDetails(_ pollDetails: TimelinePollDetails)
|
func updateWithPollDetailsState(_ pollDetailsState: TimelinePollDetailsState)
|
||||||
func showAnsweringFailure()
|
func showAnsweringFailure()
|
||||||
func showClosingFailure()
|
func showClosingFailure()
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,8 +28,23 @@ struct TimelinePollView: View {
|
||||||
@ObservedObject var viewModel: TimelinePollViewModel.Context
|
@ObservedObject var viewModel: TimelinePollViewModel.Context
|
||||||
|
|
||||||
var body: some View {
|
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) {
|
VStack(alignment: .leading, spacing: 16.0) {
|
||||||
if poll.representsPollEndedEvent {
|
if poll.representsPollEndedEvent {
|
||||||
Text(VectorL10n.pollTimelineEndedText)
|
Text(VectorL10n.pollTimelineEndedText)
|
||||||
|
@ -40,7 +55,7 @@ struct TimelinePollView: View {
|
||||||
Text(poll.question)
|
Text(poll.question)
|
||||||
.font(theme.fonts.bodySB)
|
.font(theme.fonts.bodySB)
|
||||||
.foregroundColor(theme.colors.primaryContent) +
|
.foregroundColor(theme.colors.primaryContent) +
|
||||||
Text(editedText)
|
Text(editedText(poll))
|
||||||
.font(theme.fonts.footnote)
|
.font(theme.fonts.footnote)
|
||||||
.foregroundColor(theme.colors.secondaryContent)
|
.foregroundColor(theme.colors.secondaryContent)
|
||||||
|
|
||||||
|
@ -54,21 +69,16 @@ struct TimelinePollView: View {
|
||||||
.disabled(poll.closed)
|
.disabled(poll.closed)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
|
||||||
Text(totalVotesString)
|
Text(totalVotesString(poll))
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
.font(theme.fonts.footnote)
|
.font(theme.fonts.footnote)
|
||||||
.foregroundColor(theme.colors.tertiaryContent)
|
.foregroundColor(theme.colors.tertiaryContent)
|
||||||
}
|
}
|
||||||
.padding([.horizontal, .top], 2.0)
|
.padding([.horizontal, .top], 2.0)
|
||||||
.padding([.bottom])
|
.padding([.bottom])
|
||||||
.alert(item: $viewModel.alertInfo) { info in
|
|
||||||
info.alert
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var totalVotesString: String {
|
private func totalVotesString(_ poll: TimelinePollDetails) -> String {
|
||||||
let poll = viewModel.viewState.poll
|
|
||||||
|
|
||||||
if poll.hasDecryptionError, poll.totalAnswerCount > 0 {
|
if poll.hasDecryptionError, poll.totalAnswerCount > 0 {
|
||||||
return VectorL10n.pollTimelineDecryptionError
|
return VectorL10n.pollTimelineDecryptionError
|
||||||
}
|
}
|
||||||
|
@ -95,8 +105,8 @@ struct TimelinePollView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var editedText: String {
|
private func editedText(_ poll: TimelinePollDetails) -> String {
|
||||||
viewModel.viewState.poll.hasBeenEdited ? " \(VectorL10n.eventFormatterMessageEditedMention)" : ""
|
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