mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-21 17:12:45 +02:00
Merge branch 'develop' into flescio/1040-poll_detail
# Conflicts: # Riot/Assets/en.lproj/Vector.strings # Riot/Generated/Strings.swift
This commit is contained in:
@@ -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)],
|
||||
|
||||
Reference in New Issue
Block a user