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/Generated/Strings.swift # RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift # RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift # RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift
This commit is contained in:
@@ -26,8 +26,11 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable {
|
||||
// mock that screen.
|
||||
case active
|
||||
case past
|
||||
case activeEmpty
|
||||
case pastEmpty
|
||||
case activeNoMoreContent
|
||||
case contentLoading
|
||||
case empty
|
||||
case emptyLoading
|
||||
case emptyNoMoreContent
|
||||
case loading
|
||||
|
||||
/// The associated screen
|
||||
@@ -37,34 +40,40 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable {
|
||||
|
||||
/// Generate the view struct for the screen state.
|
||||
var screenView: ([Any], AnyView) {
|
||||
let pollHistoryMode: PollHistoryMode
|
||||
var pollHistoryMode: PollHistoryMode = .active
|
||||
let pollService = MockPollHistoryService()
|
||||
|
||||
switch self {
|
||||
case .active:
|
||||
pollHistoryMode = .active
|
||||
case .activeNoMoreContent:
|
||||
pollHistoryMode = .active
|
||||
pollService.hasNextBatch = false
|
||||
case .past:
|
||||
pollHistoryMode = .past
|
||||
case .activeEmpty:
|
||||
case .contentLoading:
|
||||
pollService.nextBatchPublishers.append(MockPollPublisher.loadingPolls)
|
||||
case .empty:
|
||||
pollHistoryMode = .active
|
||||
pollService.nextBatchPublisher = Empty(completeImmediately: true,
|
||||
outputType: TimelinePollDetails.self,
|
||||
failureType: Error.self).eraseToAnyPublisher()
|
||||
case .pastEmpty:
|
||||
pollHistoryMode = .past
|
||||
pollService.nextBatchPublisher = Empty(completeImmediately: true,
|
||||
outputType: TimelinePollDetails.self,
|
||||
failureType: Error.self).eraseToAnyPublisher()
|
||||
pollService.nextBatchPublishers = [MockPollPublisher.emptyPolls]
|
||||
case .emptyLoading:
|
||||
pollService.nextBatchPublishers = [MockPollPublisher.emptyPolls, MockPollPublisher.loadingPolls]
|
||||
case .emptyNoMoreContent:
|
||||
pollService.hasNextBatch = false
|
||||
pollService.nextBatchPublishers = [MockPollPublisher.emptyPolls]
|
||||
case .loading:
|
||||
pollHistoryMode = .active
|
||||
pollService.nextBatchPublisher = Empty(completeImmediately: false,
|
||||
outputType: TimelinePollDetails.self,
|
||||
failureType: Error.self).eraseToAnyPublisher()
|
||||
pollService.nextBatchPublishers = [MockPollPublisher.loadingPolls]
|
||||
}
|
||||
|
||||
let viewModel = PollHistoryViewModel(mode: pollHistoryMode, pollService: pollService)
|
||||
|
||||
// can simulate service and viewModel actions here if needs be.
|
||||
switch self {
|
||||
case .contentLoading, .emptyLoading:
|
||||
viewModel.process(viewAction: .loadMoreContent)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
return (
|
||||
[pollHistoryMode, viewModel],
|
||||
@@ -73,3 +82,17 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum MockPollPublisher {
|
||||
static var emptyPolls: AnyPublisher<TimelinePollDetails, Error> {
|
||||
Empty<TimelinePollDetails, Error>(completeImmediately: true).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
static var loadingPolls: AnyPublisher<TimelinePollDetails, Error> {
|
||||
Empty<TimelinePollDetails, Error>(completeImmediately: false).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
static var failure: AnyPublisher<TimelinePollDetails, Error> {
|
||||
Fail(error: NSError(domain: "fake", code: 1)).eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ enum PollHistoryMode: CaseIterable {
|
||||
|
||||
struct PollHistoryViewBindings {
|
||||
var mode: PollHistoryMode
|
||||
var alertInfo: AlertInfo<Bool>?
|
||||
}
|
||||
|
||||
struct PollHistoryViewState: BindableState {
|
||||
@@ -44,10 +45,13 @@ struct PollHistoryViewState: BindableState {
|
||||
var isLoading = false
|
||||
var canLoadMoreContent = true
|
||||
var polls: [TimelinePollDetails]?
|
||||
var syncStartDate: Date = .init()
|
||||
var syncedUpTo: Date = .distantFuture
|
||||
}
|
||||
|
||||
enum PollHistoryViewAction {
|
||||
case viewAppeared
|
||||
case segmentDidChange
|
||||
case showPollDetail(poll: TimelinePollDetails)
|
||||
case loadMoreContent
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ final class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModel
|
||||
init(mode: PollHistoryMode, pollService: PollHistoryServiceProtocol) {
|
||||
self.pollService = pollService
|
||||
super.init(initialViewState: PollHistoryViewState(mode: mode))
|
||||
state.canLoadMoreContent = pollService.hasNextBatch
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
@@ -37,32 +38,47 @@ final class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModel
|
||||
switch viewAction {
|
||||
case .viewAppeared:
|
||||
setupUpdateSubscriptions()
|
||||
fetchFirstBatch()
|
||||
fetchContent()
|
||||
case .segmentDidChange:
|
||||
updateViewState()
|
||||
case .showPollDetail(let poll):
|
||||
completion?(.showPollDetail(poll: poll))
|
||||
case .loadMoreContent:
|
||||
fetchContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension PollHistoryViewModel {
|
||||
func fetchFirstBatch() {
|
||||
func fetchContent() {
|
||||
state.isLoading = true
|
||||
|
||||
pollService
|
||||
.nextBatch()
|
||||
.collect()
|
||||
.sink { [weak self] _ in
|
||||
#warning("Handle errors")
|
||||
self?.state.isLoading = false
|
||||
.sink { [weak self] completion in
|
||||
self?.handleBatchEnded(completion: completion)
|
||||
} receiveValue: { [weak self] polls in
|
||||
self?.polls = polls
|
||||
self?.updateViewState()
|
||||
self?.add(polls: polls)
|
||||
}
|
||||
.store(in: &subcriptions)
|
||||
}
|
||||
|
||||
func handleBatchEnded(completion: Subscribers.Completion<Error>) {
|
||||
state.isLoading = false
|
||||
state.canLoadMoreContent = pollService.hasNextBatch
|
||||
|
||||
switch completion {
|
||||
case .finished:
|
||||
break
|
||||
case .failure:
|
||||
polls = polls ?? []
|
||||
state.bindings.alertInfo = .init(id: true, title: VectorL10n.pollHistoryFetchingError)
|
||||
}
|
||||
|
||||
updateViewState()
|
||||
}
|
||||
|
||||
func setupUpdateSubscriptions() {
|
||||
subcriptions.removeAll()
|
||||
|
||||
@@ -75,9 +91,15 @@ private extension PollHistoryViewModel {
|
||||
.store(in: &subcriptions)
|
||||
|
||||
pollService
|
||||
.pollErrors
|
||||
.sink { detail in
|
||||
#warning("Handle errors")
|
||||
.fetchedUpTo
|
||||
.weakAssign(to: \.state.syncedUpTo, on: self)
|
||||
.store(in: &subcriptions)
|
||||
|
||||
pollService
|
||||
.livePolls
|
||||
.sink { [weak self] livePoll in
|
||||
self?.add(polls: [livePoll])
|
||||
self?.updateViewState()
|
||||
}
|
||||
.store(in: &subcriptions)
|
||||
}
|
||||
@@ -90,6 +112,10 @@ private extension PollHistoryViewModel {
|
||||
polls?[pollIndex] = poll
|
||||
}
|
||||
|
||||
func add(polls: [TimelinePollDetails]) {
|
||||
self.polls = (self.polls ?? []) + polls
|
||||
}
|
||||
|
||||
func updateViewState() {
|
||||
let renderedPolls: [TimelinePollDetails]?
|
||||
|
||||
@@ -106,17 +132,22 @@ private extension PollHistoryViewModel {
|
||||
|
||||
extension PollHistoryViewModel.Context {
|
||||
var emptyPollsText: String {
|
||||
let days = PollHistoryConstants.chunkSizeInDays
|
||||
|
||||
switch (viewState.bindings.mode, viewState.canLoadMoreContent) {
|
||||
case (.active, true):
|
||||
return VectorL10n.pollHistoryNoActivePollPeriodText("\(days)")
|
||||
return VectorL10n.pollHistoryNoActivePollPeriodText("\(syncedPastDays)")
|
||||
case (.active, false):
|
||||
return VectorL10n.pollHistoryNoActivePollText
|
||||
case (.past, true):
|
||||
return VectorL10n.pollHistoryNoPastPollPeriodText("\(days)")
|
||||
return VectorL10n.pollHistoryNoPastPollPeriodText("\(syncedPastDays)")
|
||||
case (.past, false):
|
||||
return VectorL10n.pollHistoryNoPastPollText
|
||||
}
|
||||
}
|
||||
|
||||
var syncedPastDays: Int {
|
||||
guard let days = Calendar.current.dateComponents([.day], from: viewState.syncedUpTo, to: viewState.syncStartDate).day else {
|
||||
return 0
|
||||
}
|
||||
return max(0, days)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,58 +22,105 @@ final class PollHistoryService: PollHistoryServiceProtocol {
|
||||
private let room: MXRoom
|
||||
private let timeline: MXEventTimeline
|
||||
private let chunkSizeInDays: UInt
|
||||
private var timelineListener: Any?
|
||||
|
||||
private let updatesSubject: PassthroughSubject<TimelinePollDetails, Never> = .init()
|
||||
private let pollErrorsSubject: PassthroughSubject<Error, Never> = .init()
|
||||
private var timelineListener: Any?
|
||||
private var roomListener: Any?
|
||||
|
||||
private var pollAggregators: [String: PollAggregator] = [:]
|
||||
private var targetTimestamp: Date
|
||||
private var oldestEventDate: Date = .distantFuture
|
||||
// polls aggregation
|
||||
private var pollAggregationContexts: [String: PollAggregationContext] = [:]
|
||||
|
||||
// polls
|
||||
private var currentBatchSubject: PassthroughSubject<TimelinePollDetails, Error>?
|
||||
private var livePollsSubject: PassthroughSubject<TimelinePollDetails, Never> = .init()
|
||||
|
||||
// polls updates
|
||||
private let updatesSubject: PassthroughSubject<TimelinePollDetails, Never> = .init()
|
||||
|
||||
// timestamps
|
||||
private var targetTimestamp: Date = .init()
|
||||
private var oldestEventDateSubject: CurrentValueSubject<Date, Never> = .init(.init())
|
||||
|
||||
var updates: AnyPublisher<TimelinePollDetails, Never> {
|
||||
updatesSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
var pollErrors: AnyPublisher<Error, Never> {
|
||||
pollErrorsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(room: MXRoom, chunkSizeInDays: UInt) {
|
||||
self.room = room
|
||||
self.chunkSizeInDays = chunkSizeInDays
|
||||
timeline = MXRoomEventTimeline(room: room, andInitialEventId: nil)
|
||||
targetTimestamp = Date().addingTimeInterval(-TimeInterval(chunkSizeInDays) * Constants.oneDayInSeconds)
|
||||
setup(timeline: timeline)
|
||||
setupTimeline()
|
||||
setupLiveUpdates()
|
||||
}
|
||||
|
||||
func nextBatch() -> AnyPublisher<TimelinePollDetails, Error> {
|
||||
currentBatchSubject?.eraseToAnyPublisher() ?? startPagination()
|
||||
}
|
||||
|
||||
var hasNextBatch: Bool {
|
||||
timeline.canPaginate(.backwards)
|
||||
}
|
||||
|
||||
var fetchedUpTo: AnyPublisher<Date, Never> {
|
||||
oldestEventDateSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
var livePolls: AnyPublisher<TimelinePollDetails, Never> {
|
||||
livePollsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
deinit {
|
||||
guard let roomListener = roomListener else {
|
||||
return
|
||||
}
|
||||
room.removeListener(roomListener)
|
||||
}
|
||||
|
||||
class PollAggregationContext {
|
||||
var pollAggregator: PollAggregator?
|
||||
let isLivePoll: Bool
|
||||
var published: Bool
|
||||
|
||||
init(pollAggregator: PollAggregator? = nil, isLivePoll: Bool, published: Bool = false) {
|
||||
self.pollAggregator = pollAggregator
|
||||
self.isLivePoll = isLivePoll
|
||||
self.published = published
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension PollHistoryService {
|
||||
enum Constants {
|
||||
static let pageSize: UInt = 500
|
||||
static let oneDayInSeconds: TimeInterval = 8.6 * 10e3
|
||||
static let pageSize: UInt = 250
|
||||
}
|
||||
|
||||
func setup(timeline: MXEventTimeline) {
|
||||
func setupTimeline() {
|
||||
timeline.resetPagination()
|
||||
|
||||
timelineListener = timeline.listenToEvents { [weak self] event, _, _ in
|
||||
if event.eventType == .pollStart {
|
||||
self?.aggregatePoll(pollStartEvent: event)
|
||||
self?.aggregatePoll(pollStartEvent: event, isLivePoll: false)
|
||||
}
|
||||
|
||||
self?.updateTimestamp(event: event)
|
||||
}
|
||||
}
|
||||
|
||||
func setupLiveUpdates() {
|
||||
roomListener = room.listen(toEventsOfTypes: [kMXEventTypeStringPollStart, kMXEventTypeStringPollStartMSC3381]) { [weak self] event, _, _ in
|
||||
if event.eventType == .pollStart {
|
||||
self?.aggregatePoll(pollStartEvent: event, isLivePoll: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateTimestamp(event: MXEvent) {
|
||||
oldestEventDate = min(event.originServerDate, oldestEventDate)
|
||||
}
|
||||
|
||||
func startPagination() -> AnyPublisher<TimelinePollDetails, Error> {
|
||||
let startingTimestamp = oldestEventDate
|
||||
targetTimestamp = startingTimestamp.subtractingDays(chunkSizeInDays) ?? startingTimestamp
|
||||
|
||||
let batchSubject = PassthroughSubject<TimelinePollDetails, Error>()
|
||||
currentBatchSubject = batchSubject
|
||||
|
||||
@@ -81,14 +128,13 @@ private extension PollHistoryService {
|
||||
guard let self = self else {
|
||||
return
|
||||
}
|
||||
self.timeline.resetPagination()
|
||||
self.paginate(timeline: self.timeline)
|
||||
self.paginate()
|
||||
}
|
||||
|
||||
return batchSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func paginate(timeline: MXEventTimeline) {
|
||||
func paginate() {
|
||||
timeline.paginate(Constants.pageSize, direction: .backwards, onlyFromStore: false) { [weak self] response in
|
||||
guard let self = self else {
|
||||
return
|
||||
@@ -96,8 +142,8 @@ private extension PollHistoryService {
|
||||
|
||||
switch response {
|
||||
case .success:
|
||||
if timeline.canPaginate(.backwards), self.timestampTargetReached == false {
|
||||
self.paginate(timeline: timeline)
|
||||
if self.timeline.canPaginate(.backwards), self.timestampTargetReached == false {
|
||||
self.paginate()
|
||||
} else {
|
||||
self.completeBatch(completion: .finished)
|
||||
}
|
||||
@@ -112,21 +158,41 @@ private extension PollHistoryService {
|
||||
currentBatchSubject = nil
|
||||
}
|
||||
|
||||
func aggregatePoll(pollStartEvent: MXEvent) {
|
||||
guard pollAggregators[pollStartEvent.eventId] == nil else {
|
||||
func aggregatePoll(pollStartEvent: MXEvent, isLivePoll: Bool) {
|
||||
let eventId: String = pollStartEvent.eventId
|
||||
|
||||
guard pollAggregationContexts[eventId] == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let aggregator = try? PollAggregator(session: room.mxSession, room: room, pollEvent: pollStartEvent, delegate: self) else {
|
||||
return
|
||||
}
|
||||
let newContext: PollAggregationContext = .init(isLivePoll: isLivePoll)
|
||||
pollAggregationContexts[eventId] = newContext
|
||||
|
||||
pollAggregators[pollStartEvent.eventId] = aggregator
|
||||
do {
|
||||
newContext.pollAggregator = try PollAggregator(session: room.mxSession, room: room, pollEvent: pollStartEvent, delegate: self)
|
||||
} catch {
|
||||
pollAggregationContexts.removeValue(forKey: eventId)
|
||||
}
|
||||
}
|
||||
|
||||
var timestampTargetReached: Bool {
|
||||
oldestEventDate <= targetTimestamp
|
||||
}
|
||||
|
||||
var oldestEventDate: Date {
|
||||
get {
|
||||
oldestEventDateSubject.value
|
||||
}
|
||||
set {
|
||||
oldestEventDateSubject.send(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension Date {
|
||||
func subtractingDays(_ days: UInt) -> Date? {
|
||||
Calendar.current.date(byAdding: DateComponents(day: -Int(days)), to: self)
|
||||
}
|
||||
}
|
||||
|
||||
private extension MXEvent {
|
||||
@@ -138,17 +204,30 @@ private extension MXEvent {
|
||||
// MARK: - PollAggregatorDelegate
|
||||
|
||||
extension PollHistoryService: PollAggregatorDelegate {
|
||||
func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) {}
|
||||
func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) { }
|
||||
|
||||
func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) { }
|
||||
|
||||
func pollAggregatorDidEndLoading(_ aggregator: PollAggregator) {
|
||||
currentBatchSubject?.send(.init(poll: aggregator.poll, represent: .started))
|
||||
}
|
||||
|
||||
func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) {
|
||||
pollErrorsSubject.send(didFailWithError)
|
||||
guard let context = pollAggregationContexts[aggregator.poll.id], context.published == false else {
|
||||
return
|
||||
}
|
||||
|
||||
context.published = true
|
||||
|
||||
let newPoll: TimelinePollDetails = .init(poll: aggregator.poll, represent: .started)
|
||||
|
||||
if context.isLivePoll {
|
||||
livePollsSubject.send(newPoll)
|
||||
} else {
|
||||
currentBatchSubject?.send(newPoll)
|
||||
}
|
||||
}
|
||||
|
||||
func pollAggregatorDidUpdateData(_ aggregator: PollAggregator) {
|
||||
guard let context = pollAggregationContexts[aggregator.poll.id], context.published else {
|
||||
return
|
||||
}
|
||||
updatesSubject.send(.init(poll: aggregator.poll, represent: .started))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,29 +17,38 @@
|
||||
import Combine
|
||||
|
||||
final class MockPollHistoryService: PollHistoryServiceProtocol {
|
||||
lazy var nextBatchPublishers: [AnyPublisher<TimelinePollDetails, Error>] = [
|
||||
(activePollsData + pastPollsData)
|
||||
.publisher
|
||||
.setFailureType(to: Error.self)
|
||||
.eraseToAnyPublisher()
|
||||
]
|
||||
|
||||
func nextBatch() -> AnyPublisher<TimelinePollDetails, Error> {
|
||||
nextBatchPublishers.isEmpty ? Empty().eraseToAnyPublisher() : nextBatchPublishers.removeFirst()
|
||||
}
|
||||
|
||||
var updatesPublisher: AnyPublisher<TimelinePollDetails, Never> = Empty().eraseToAnyPublisher()
|
||||
var updates: AnyPublisher<TimelinePollDetails, Never> {
|
||||
updatesPublisher
|
||||
}
|
||||
|
||||
var pollErrorPublisher: AnyPublisher<Error, Never> = Empty().eraseToAnyPublisher()
|
||||
var pollErrors: AnyPublisher<Error, Never> {
|
||||
pollErrorPublisher
|
||||
var hasNextBatch = true
|
||||
|
||||
var fetchedUpToPublisher: AnyPublisher<Date, Never> = Just(.init()).eraseToAnyPublisher()
|
||||
var fetchedUpTo: AnyPublisher<Date, Never> {
|
||||
fetchedUpToPublisher
|
||||
}
|
||||
|
||||
lazy var nextBatchPublisher: AnyPublisher<TimelinePollDetails, Error> = (activePollsData + pastPollsData)
|
||||
.publisher
|
||||
.setFailureType(to: Error.self)
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
func nextBatch() -> AnyPublisher<TimelinePollDetails, Error> {
|
||||
nextBatchPublisher
|
||||
var livePollsPublisher: AnyPublisher<TimelinePollDetails, Never> = Empty().eraseToAnyPublisher()
|
||||
var livePolls: AnyPublisher<TimelinePollDetails, Never> {
|
||||
livePollsPublisher
|
||||
}
|
||||
}
|
||||
|
||||
private extension MockPollHistoryService {
|
||||
var activePollsData: [TimelinePollDetails] {
|
||||
(1...10)
|
||||
(1...3)
|
||||
.map { index in
|
||||
TimelinePollDetails(id: "a\(index)",
|
||||
question: "Do you like the active poll number \(index)?",
|
||||
@@ -56,7 +65,7 @@ private extension MockPollHistoryService {
|
||||
}
|
||||
|
||||
var pastPollsData: [TimelinePollDetails] {
|
||||
(1...10)
|
||||
(1...3)
|
||||
.map { index in
|
||||
TimelinePollDetails(id: "p\(index)",
|
||||
question: "Do you like the active poll number \(index)?",
|
||||
|
||||
@@ -21,10 +21,17 @@ protocol PollHistoryServiceProtocol {
|
||||
/// 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 `nextBatch()` publishers.
|
||||
/// Publishes updates for the polls previously pusblished by the `nextBatch()` or `livePolls` publishers.
|
||||
var updates: AnyPublisher<TimelinePollDetails, Never> { get }
|
||||
|
||||
/// Publishes errors regarding poll aggregations.
|
||||
/// Note: `nextBatch()` will continue to publish new polls even if some poll isn't being aggregated correctly.
|
||||
var pollErrors: AnyPublisher<Error, Never> { get }
|
||||
/// Publishes live polls not related with the current batch.
|
||||
var livePolls: AnyPublisher<TimelinePollDetails, Never> { get }
|
||||
|
||||
/// Returns true every time the service can fetch another batch.
|
||||
/// There is no guarantee the `nextBatch()` returned publisher will publish something anyway.
|
||||
var hasNextBatch: Bool { get }
|
||||
|
||||
/// Publishes the date up to the service is synced (in the past).
|
||||
/// This date doesn't need to be related with any poll event.
|
||||
var fetchedUpTo: AnyPublisher<Date, Never> { get }
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ final class PollHistoryUITests: MockScreenTestCase {
|
||||
let emptyText = app.staticTexts["PollHistory.emptyText"]
|
||||
let items = app.staticTexts["PollListItem.title"]
|
||||
let selectedSegment = app.buttons[VectorL10n.pollHistoryActiveSegmentTitle]
|
||||
let loadMoreButton = app.buttons["PollHistory.loadMore"]
|
||||
let winningOption = app.staticTexts["PollListData.winningOption"]
|
||||
|
||||
XCTAssertEqual(title, VectorL10n.pollHistoryTitle)
|
||||
@@ -31,6 +32,7 @@ final class PollHistoryUITests: MockScreenTestCase {
|
||||
XCTAssertFalse(emptyText.exists)
|
||||
XCTAssertTrue(selectedSegment.exists)
|
||||
XCTAssertEqual(selectedSegment.value as? String, VectorL10n.accessibilitySelected)
|
||||
XCTAssertTrue(loadMoreButton.exists)
|
||||
XCTAssertFalse(winningOption.exists)
|
||||
}
|
||||
|
||||
@@ -40,6 +42,7 @@ final class PollHistoryUITests: MockScreenTestCase {
|
||||
let emptyText = app.staticTexts["PollHistory.emptyText"]
|
||||
let items = app.staticTexts["PollListItem.title"]
|
||||
let selectedSegment = app.buttons[VectorL10n.pollHistoryPastSegmentTitle]
|
||||
let loadMoreButton = app.buttons["PollHistory.loadMore"]
|
||||
let winningOption = app.buttons["PollAnswerOption0"]
|
||||
|
||||
XCTAssertEqual(title, VectorL10n.pollHistoryTitle)
|
||||
@@ -47,33 +50,66 @@ final class PollHistoryUITests: MockScreenTestCase {
|
||||
XCTAssertFalse(emptyText.exists)
|
||||
XCTAssertTrue(selectedSegment.exists)
|
||||
XCTAssertEqual(selectedSegment.value as? String, VectorL10n.accessibilitySelected)
|
||||
XCTAssertTrue(loadMoreButton.exists)
|
||||
XCTAssertTrue(winningOption.exists)
|
||||
}
|
||||
|
||||
func testPastPollHistoryIsEmpty() {
|
||||
app.goToScreenWithIdentifier(MockPollHistoryScreenState.pastEmpty.title)
|
||||
func testActivePollHistoryHasContentAndCantLoadMore() {
|
||||
app.goToScreenWithIdentifier(MockPollHistoryScreenState.activeNoMoreContent.title)
|
||||
let emptyText = app.staticTexts["PollHistory.emptyText"]
|
||||
let items = app.staticTexts["PollListItem.title"]
|
||||
let loadMoreButton = app.buttons["PollHistory.loadMore"]
|
||||
|
||||
XCTAssertTrue(items.exists)
|
||||
XCTAssertFalse(emptyText.exists)
|
||||
XCTAssertFalse(loadMoreButton.exists)
|
||||
}
|
||||
|
||||
func testActivePollHistoryHasContentAndCanLoadMore() {
|
||||
app.goToScreenWithIdentifier(MockPollHistoryScreenState.contentLoading.title)
|
||||
let title = app.navigationBars.firstMatch.identifier
|
||||
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 loadMoreButton = app.buttons["PollHistory.loadMore"]
|
||||
|
||||
XCTAssertEqual(title, VectorL10n.pollHistoryTitle)
|
||||
XCTAssertFalse(items.exists)
|
||||
XCTAssertTrue(emptyText.exists)
|
||||
XCTAssertTrue(selectedSegment.exists)
|
||||
XCTAssertEqual(selectedSegment.value as? String, VectorL10n.accessibilitySelected)
|
||||
XCTAssertFalse(winningOption.exists)
|
||||
XCTAssertTrue(items.exists)
|
||||
XCTAssertFalse(emptyText.exists)
|
||||
XCTAssertTrue(loadMoreButton.exists)
|
||||
XCTAssertFalse(loadMoreButton.isEnabled)
|
||||
}
|
||||
|
||||
func testLoaderIsShowing() {
|
||||
app.goToScreenWithIdentifier(MockPollHistoryScreenState.loading.title)
|
||||
let title = app.navigationBars.firstMatch.identifier
|
||||
let loadingText = app.staticTexts["PollHistory.loadingText"]
|
||||
func testActivePollHistoryEmptyAndCanLoadMore() {
|
||||
app.goToScreenWithIdentifier(MockPollHistoryScreenState.empty.title)
|
||||
let emptyText = app.staticTexts["PollHistory.emptyText"]
|
||||
let items = app.staticTexts["PollListItem.title"]
|
||||
let loadMoreButton = app.buttons["PollHistory.loadMore"]
|
||||
|
||||
XCTAssertEqual(title, VectorL10n.pollHistoryTitle)
|
||||
XCTAssertFalse(items.exists)
|
||||
XCTAssertTrue(loadingText.exists)
|
||||
XCTAssertTrue(emptyText.exists)
|
||||
XCTAssertTrue(loadMoreButton.exists)
|
||||
XCTAssertTrue(loadMoreButton.isEnabled)
|
||||
}
|
||||
|
||||
func testActivePollHistoryEmptyAndLoading() {
|
||||
app.goToScreenWithIdentifier(MockPollHistoryScreenState.emptyLoading.title)
|
||||
let emptyText = app.staticTexts["PollHistory.emptyText"]
|
||||
let items = app.staticTexts["PollListItem.title"]
|
||||
let loadMoreButton = app.buttons["PollHistory.loadMore"]
|
||||
|
||||
XCTAssertFalse(items.exists)
|
||||
XCTAssertTrue(emptyText.exists)
|
||||
XCTAssertTrue(loadMoreButton.exists)
|
||||
XCTAssertFalse(loadMoreButton.isEnabled)
|
||||
}
|
||||
|
||||
func testActivePollHistoryEmptyAndCantLoadMore() {
|
||||
app.goToScreenWithIdentifier(MockPollHistoryScreenState.emptyNoMoreContent.title)
|
||||
let emptyText = app.staticTexts["PollHistory.emptyText"]
|
||||
let items = app.staticTexts["PollListItem.title"]
|
||||
let loadMoreButton = app.buttons["PollHistory.loadMore"]
|
||||
|
||||
XCTAssertFalse(items.exists)
|
||||
XCTAssertTrue(emptyText.exists)
|
||||
XCTAssertFalse(loadMoreButton.exists)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -42,9 +42,11 @@ final class PollHistoryViewModelTests: XCTestCase {
|
||||
|
||||
func testLoadingStateIsTrueWhileLoading() {
|
||||
XCTAssertFalse(viewModel.state.isLoading)
|
||||
pollHistoryService.nextBatchPublisher = Empty(completeImmediately: false, outputType: TimelinePollDetails.self, failureType: Error.self).eraseToAnyPublisher()
|
||||
pollHistoryService.nextBatchPublishers = [MockPollPublisher.loadingPolls, MockPollPublisher.emptyPolls]
|
||||
viewModel.process(viewAction: .viewAppeared)
|
||||
XCTAssertTrue(viewModel.state.isLoading)
|
||||
viewModel.process(viewAction: .viewAppeared)
|
||||
XCTAssertFalse(viewModel.state.isLoading)
|
||||
}
|
||||
|
||||
func testUpdatesAreHandled() throws {
|
||||
@@ -79,6 +81,35 @@ final class PollHistoryViewModelTests: XCTestCase {
|
||||
let pollDates = try polls.map(\.startDate)
|
||||
XCTAssertEqual(pollDates, pollDates.sorted(by: { $0 > $1 }))
|
||||
}
|
||||
|
||||
func testLivePollsAreHandled() throws {
|
||||
pollHistoryService.nextBatchPublishers = [MockPollPublisher.emptyPolls]
|
||||
pollHistoryService.livePollsPublisher = Just(mockPoll).eraseToAnyPublisher()
|
||||
viewModel.process(viewAction: .viewAppeared)
|
||||
XCTAssertEqual(viewModel.state.polls?.count, 1)
|
||||
XCTAssertEqual(viewModel.state.polls?.first?.id, "id")
|
||||
}
|
||||
|
||||
func testLivePollsDontChangeLoadingState() throws {
|
||||
let livePolls = PassthroughSubject<TimelinePollDetails, Never>()
|
||||
pollHistoryService.nextBatchPublishers = [MockPollPublisher.loadingPolls]
|
||||
pollHistoryService.livePollsPublisher = livePolls.eraseToAnyPublisher()
|
||||
viewModel.process(viewAction: .viewAppeared)
|
||||
XCTAssertTrue(viewModel.state.isLoading)
|
||||
XCTAssertNil(viewModel.state.polls)
|
||||
livePolls.send(mockPoll)
|
||||
XCTAssertTrue(viewModel.state.isLoading)
|
||||
XCTAssertNotNil(viewModel.state.polls)
|
||||
XCTAssertEqual(viewModel.state.polls?.count, 1)
|
||||
}
|
||||
|
||||
func testAfterFailureCompletionIsCalled() throws {
|
||||
pollHistoryService.nextBatchPublishers = [MockPollPublisher.failure]
|
||||
viewModel.process(viewAction: .viewAppeared)
|
||||
XCTAssertFalse(viewModel.state.isLoading)
|
||||
XCTAssertNotNil(viewModel.state.polls)
|
||||
XCTAssertNotNil(viewModel.state.bindings.alertInfo)
|
||||
}
|
||||
}
|
||||
|
||||
private extension PollHistoryViewModelTests {
|
||||
@@ -87,4 +118,18 @@ private extension PollHistoryViewModelTests {
|
||||
try XCTUnwrap(viewModel.state.polls)
|
||||
}
|
||||
}
|
||||
|
||||
var mockPoll: TimelinePollDetails {
|
||||
.init(id: "id",
|
||||
question: "Do you like polls?",
|
||||
answerOptions: [],
|
||||
closed: false,
|
||||
startDate: .init(),
|
||||
totalAnswerCount: 3,
|
||||
type: .undisclosed,
|
||||
eventType: .started,
|
||||
maxAllowedSelections: 1,
|
||||
hasBeenEdited: false,
|
||||
hasDecryptionError: false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ struct PollHistory: View {
|
||||
.onChange(of: viewModel.mode) { _ in
|
||||
viewModel.send(viewAction: .segmentDidChange)
|
||||
}
|
||||
.alert(item: $viewModel.alertInfo) {
|
||||
$0.alert
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@@ -76,19 +79,23 @@ struct PollHistory: View {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var loadMoreButton: some View {
|
||||
HStack(spacing: 8) {
|
||||
if viewModel.viewState.isLoading {
|
||||
spinner
|
||||
if viewModel.viewState.canLoadMoreContent {
|
||||
HStack(spacing: 8) {
|
||||
if viewModel.viewState.isLoading {
|
||||
spinner
|
||||
}
|
||||
|
||||
Button {
|
||||
viewModel.send(viewAction: .loadMoreContent)
|
||||
} label: {
|
||||
Text(VectorL10n.pollHistoryLoadMore)
|
||||
.font(theme.fonts.body)
|
||||
}
|
||||
.accessibilityIdentifier("PollHistory.loadMore")
|
||||
.disabled(viewModel.viewState.isLoading)
|
||||
}
|
||||
|
||||
Button {
|
||||
#warning("handle action in next ticket")
|
||||
} label: {
|
||||
Text(VectorL10n.pollHistoryLoadMore)
|
||||
.font(theme.fonts.body)
|
||||
}
|
||||
.disabled(viewModel.viewState.isLoading)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user