Merge branch 'develop' into flescio/1040-poll_detail

# Conflicts:
#	Riot/Assets/en.lproj/Vector.strings
#	Riot/Generated/Strings.swift
This commit is contained in:
Flavio Alescio
2023-01-25 15:49:40 +01:00
95 changed files with 714 additions and 957 deletions
@@ -47,17 +47,17 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable {
pollHistoryMode = .past
case .activeEmpty:
pollHistoryMode = .active
pollService.nextPublisher = Empty(completeImmediately: true,
pollService.nextBatchPublisher = Empty(completeImmediately: true,
outputType: TimelinePollDetails.self,
failureType: Error.self).eraseToAnyPublisher()
case .pastEmpty:
pollHistoryMode = .past
pollService.nextPublisher = Empty(completeImmediately: true,
pollService.nextBatchPublisher = Empty(completeImmediately: true,
outputType: TimelinePollDetails.self,
failureType: Error.self).eraseToAnyPublisher()
case .loading:
pollHistoryMode = .active
pollService.nextPublisher = Empty(completeImmediately: false,
pollService.nextBatchPublisher = Empty(completeImmediately: false,
outputType: TimelinePollDetails.self,
failureType: Error.self).eraseToAnyPublisher()
}
@@ -51,7 +51,7 @@ private extension PollHistoryViewModel {
state.isLoading = true
pollService
.next()
.nextBatch()
.collect()
.sink { [weak self] _ in
#warning("Handle errors")
@@ -69,7 +69,7 @@ private extension PollHistoryViewModel {
pollService
.updates
.sink { [weak self] detail in
self?.updatePolls(with: detail)
self?.update(poll: detail)
self?.updateViewState()
}
.store(in: &subcriptions)
@@ -82,7 +82,7 @@ private extension PollHistoryViewModel {
.store(in: &subcriptions)
}
func updatePolls(with poll: TimelinePollDetails) {
func update(poll: TimelinePollDetails) {
guard let pollIndex = polls?.firstIndex(where: { $0.id == poll.id }) else {
return
}
@@ -103,3 +103,20 @@ private extension PollHistoryViewModel {
state.polls = renderedPolls?.sorted(by: { $0.startDate > $1.startDate })
}
}
extension PollHistoryViewModel.Context {
var emptyPollsText: String {
let days = PollHistoryConstants.chunkSizeInDays
switch (viewState.bindings.mode, viewState.canLoadMoreContent) {
case (.active, true):
return VectorL10n.pollHistoryNoActivePollPeriodText("\(days)")
case (.active, false):
return VectorL10n.pollHistoryNoActivePollText
case (.past, true):
return VectorL10n.pollHistoryNoPastPollPeriodText("\(days)")
case (.past, false):
return VectorL10n.pollHistoryNoPastPollText
}
}
}
@@ -48,7 +48,7 @@ final class PollHistoryService: PollHistoryServiceProtocol {
setup(timeline: timeline)
}
func next() -> AnyPublisher<TimelinePollDetails, Error> {
func nextBatch() -> AnyPublisher<TimelinePollDetails, Error> {
currentBatchSubject?.eraseToAnyPublisher() ?? startPagination()
}
}
@@ -27,13 +27,13 @@ final class MockPollHistoryService: PollHistoryServiceProtocol {
pollErrorPublisher
}
lazy var nextPublisher: AnyPublisher<TimelinePollDetails, Error> = (activePollsData + pastPollsData)
lazy var nextBatchPublisher: AnyPublisher<TimelinePollDetails, Error> = (activePollsData + pastPollsData)
.publisher
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
func next() -> AnyPublisher<TimelinePollDetails, Error> {
nextPublisher
func nextBatch() -> AnyPublisher<TimelinePollDetails, Error> {
nextBatchPublisher
}
}
@@ -1,4 +1,4 @@
//
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,13 +18,13 @@ import Combine
protocol PollHistoryServiceProtocol {
/// Returns a Publisher publishing the polls in the next batch.
/// Implementations should return the same publisher if `next()` is called again before the previous publisher completes.
func next() -> AnyPublisher<TimelinePollDetails, Error>
/// Implementations should return the same publisher if `nextBatch()` is called again before the previous publisher completes.
func nextBatch() -> AnyPublisher<TimelinePollDetails, Error>
/// Publishes updates for the polls previously pusblished by the `next()` publishers.
/// Publishes updates for the polls previously pusblished by the `nextBatch()` publishers.
var updates: AnyPublisher<TimelinePollDetails, Never> { get }
/// Publishes errors regarding poll aggregations.
/// Note: `next()` will continue to publish new polls even if some poll isn't being aggregated correctly.
/// Note: `nextBatch()` will continue to publish new polls even if some poll isn't being aggregated correctly.
var pollErrors: AnyPublisher<Error, Never> { get }
}
@@ -40,7 +40,7 @@ final class PollHistoryUITests: MockScreenTestCase {
let emptyText = app.staticTexts["PollHistory.emptyText"]
let items = app.staticTexts["PollListItem.title"]
let selectedSegment = app.buttons[VectorL10n.pollHistoryPastSegmentTitle]
let winningOption = app.staticTexts["PollListData.winningOption"]
let winningOption = app.buttons["PollAnswerOption0"]
XCTAssertEqual(title, VectorL10n.pollHistoryTitle)
XCTAssertTrue(items.exists)
@@ -53,7 +53,7 @@ final class PollHistoryUITests: MockScreenTestCase {
func testPastPollHistoryIsEmpty() {
app.goToScreenWithIdentifier(MockPollHistoryScreenState.pastEmpty.title)
let title = app.navigationBars.firstMatch.identifier
let emptyText = app.staticTexts["PollHistory.emptyLoadMoreText"]
let emptyText = app.staticTexts["PollHistory.emptyText"]
let items = app.staticTexts["PollListItem.title"]
let selectedSegment = app.buttons[VectorL10n.pollHistoryPastSegmentTitle]
let winningOption = app.staticTexts["PollListData.winningOption"]
@@ -42,7 +42,7 @@ final class PollHistoryViewModelTests: XCTestCase {
func testLoadingStateIsTrueWhileLoading() {
XCTAssertFalse(viewModel.state.isLoading)
pollHistoryService.nextPublisher = Empty(completeImmediately: false, outputType: TimelinePollDetails.self, failureType: Error.self).eraseToAnyPublisher()
pollHistoryService.nextBatchPublisher = Empty(completeImmediately: false, outputType: TimelinePollDetails.self, failureType: Error.self).eraseToAnyPublisher()
viewModel.process(viewAction: .viewAppeared)
XCTAssertTrue(viewModel.state.isLoading)
}
@@ -53,7 +53,7 @@ final class PollHistoryViewModelTests: XCTestCase {
viewModel.process(viewAction: .viewAppeared)
var firstPoll = try XCTUnwrap(try polls.first)
XCTAssertEqual(firstPoll.question, "Do you like the active poll number 9?")
XCTAssertEqual(firstPoll.question, "Do you like the active poll number 1?")
firstPoll.question = "foo"
mockUpdates.send(firstPoll)
@@ -85,42 +85,32 @@ struct PollHistory: View {
Button {
#warning("handle action in next ticket")
} label: {
Text("Load more polls")
Text(VectorL10n.pollHistoryLoadMore)
.font(theme.fonts.body)
}
.disabled(viewModel.viewState.isLoading)
}
}
@ViewBuilder
private var spinner: some View {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
}
@ViewBuilder
private var noPollsView: some View {
if viewModel.viewState.canLoadMoreContent {
let days = PollHistoryConstants.chunkSizeInDays
VStack(spacing: 32) {
Text(viewModel.mode == .active ? VectorL10n.pollHistoryNoActivePollPeriodText("\(days)") : VectorL10n.pollHistoryNoPastPollPeriodText("\(days)"))
.font(theme.fonts.body)
.foregroundColor(theme.colors.secondaryContent)
.multilineTextAlignment(.center)
.padding(.horizontal, 16)
.accessibilityIdentifier("PollHistory.emptyLoadMoreText")
loadMoreButton
}
.frame(maxHeight: .infinity)
} else {
Text(viewModel.mode == .active ? VectorL10n.pollHistoryNoActivePollText : VectorL10n.pollHistoryNoPastPollText)
VStack(spacing: 32) {
Text(viewModel.emptyPollsText)
.font(theme.fonts.body)
.multilineTextAlignment(.center)
.foregroundColor(theme.colors.secondaryContent)
.frame(maxHeight: .infinity)
.padding(.horizontal, 16)
.accessibilityIdentifier("PollHistory.emptyText")
if viewModel.viewState.canLoadMoreContent {
loadMoreButton
}
}
.frame(maxHeight: .infinity)
}
private var loadingView: some View {
@@ -48,56 +48,17 @@ struct PollListItem: View {
if pollData.closed {
VStack(alignment: .leading, spacing: 12) {
let winningOptions = pollData.answerOptions.filter(\.winner)
ForEach(winningOptions) {
optionView(winningOption: $0)
TimelinePollAnswerOptionButton(poll: pollData, answerOption: $0, action: nil)
}
resultView
}
}
}
}
private var clipShape: some Shape {
RoundedRectangle(cornerRadius: 4.0)
}
private func optionView(winningOption: TimelinePollAnswerOption) -> some View {
VStack(alignment: .leading, spacing: 12.0) {
HStack(alignment: .top, spacing: 8.0) {
Text(winningOption.text)
.font(theme.fonts.body)
.foregroundColor(theme.colors.primaryContent)
.accessibilityIdentifier("PollListData.winningOption")
Spacer()
votesText(winningOption: winningOption)
}
ProgressView(value: Double(winningOption.count),
total: Double(pollData.totalAnswerCount))
.progressViewStyle(LinearProgressViewStyle())
.scaleEffect(x: 1.0, y: 1.2, anchor: .center)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 8.0)
.padding(.top, 12.0)
.padding(.bottom, 12.0)
.clipShape(clipShape)
.overlay(clipShape.stroke(theme.colors.accent, lineWidth: 1.0))
.accentColor(theme.colors.accent)
}
private func votesText(winningOption: TimelinePollAnswerOption) -> some View {
Label {
Text(winningOption.count == 1 ? VectorL10n.pollTimelineOneVote : VectorL10n.pollTimelineVotesCount(Int(winningOption.count)))
.font(theme.fonts.footnote)
.foregroundColor(theme.colors.accent)
} icon: {
Image(uiImage: Asset.Images.pollWinnerIcon.image)
}
}
private var resultView: some View {
let text = pollData.totalAnswerCount == 1 ? VectorL10n.pollTimelineTotalFinalResultsOneVote : VectorL10n.pollTimelineTotalFinalResults(Int(pollData.totalAnswerCount))
@@ -133,8 +94,7 @@ struct PollListItem_Previews: PreviewProvider {
maxAllowedSelections: 1,
hasBeenEdited: false,
hasDecryptionError: false)
let pollData2 = TimelinePollDetails(id: UUID().uuidString,
question: "Do you like polls?",
answerOptions: [.init(id: "id", text: "Yes, of course!", count: 18, winner: true, selected: true)],