Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fetch poll history (PSG-1043) #7293

Merged
merged 36 commits into from
Jan 24, 2023
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
3b29c56
Begin PollHistoryService
alfogrillo Jan 19, 2023
c690fc7
Expose TimelinePollDetails init init TimelinePollCoordinator
alfogrillo Jan 19, 2023
c8d13c6
Inject TimelinePollDetails in PollListItem
alfogrillo Jan 19, 2023
729acb7
Reset pagination on landing
alfogrillo Jan 19, 2023
29749f3
Add support to start date
alfogrillo Jan 19, 2023
aec5089
Add id in TimelinePollDetails
alfogrillo Jan 20, 2023
7ca3cbb
Add target timestamp
alfogrillo Jan 20, 2023
d670bad
Add loading view
alfogrillo Jan 20, 2023
9625440
Refine loading logic
alfogrillo Jan 20, 2023
2b56b5f
Add pagination loop
alfogrillo Jan 20, 2023
ed3f020
Handle poll updates
alfogrillo Jan 20, 2023
0bef2aa
Refactor PollHistoryViewState
alfogrillo Jan 20, 2023
8aab5fe
Add empty screen with number of days
alfogrillo Jan 20, 2023
0b108cd
Improve tests
alfogrillo Jan 20, 2023
847390f
Improve UX
alfogrillo Jan 20, 2023
4132b2f
Improve error handling
alfogrillo Jan 20, 2023
1e0d731
Cleanup code
alfogrillo Jan 20, 2023
a5d35fe
Optimize page size
alfogrillo Jan 20, 2023
08920f4
Cleanup code
alfogrillo Jan 20, 2023
8ef48cc
Refactor PollHistoryService
alfogrillo Jan 23, 2023
af6bd9e
Add PollHistory view model UTs
alfogrillo Jan 23, 2023
2fb2f38
Add docs
alfogrillo Jan 23, 2023
b87fc4a
Cleanup
alfogrillo Jan 23, 2023
4df5502
Cleanup
alfogrillo Jan 23, 2023
a095605
Rename private var
alfogrillo Jan 23, 2023
1027940
Add changelog.d file
alfogrillo Jan 23, 2023
6f6edae
Fix UT
alfogrillo Jan 23, 2023
bd64090
Rename update poll method
alfogrillo Jan 24, 2023
6374ca1
Localize load more button
alfogrillo Jan 24, 2023
4270988
Update RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift
Jan 24, 2023
df2454a
Removing redundant init
alfogrillo Jan 24, 2023
4102e2c
Refactor PollKind conversion
alfogrillo Jan 24, 2023
3e92514
Add emptyPollsText in the view model
alfogrillo Jan 24, 2023
ff229ff
Refactor TimelinePollAnswerOptionButton
alfogrillo Jan 24, 2023
11f605b
Refactor next() -> nextBatch()
alfogrillo Jan 24, 2023
22b397e
Update tests
alfogrillo Jan 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Riot/Assets/en.lproj/Vector.strings
Original file line number Diff line number Diff line change
Expand Up @@ -2304,10 +2304,13 @@ Tap the + to start adding people.";
// MARK: - Polls history

"poll_history_title" = "Poll history";
"poll_history_loading_text" = "Displaying polls";
"poll_history_active_segment_title" = "Active polls";
"poll_history_past_segment_title" = "Past polls";
"poll_history_no_active_poll_text" = "There are no active polls in this room";
"poll_history_no_past_poll_text" = "There are no past polls in this room";
"poll_history_no_active_poll_period_text" = "There are no active polls for the past %@ days. Load more polls to view polls for previous months";
"poll_history_no_past_poll_period_text" = "There are no past polls for the past %@ days. Load more polls to view polls for previous months";

// MARK: - Polls

Expand Down
12 changes: 12 additions & 0 deletions Riot/Generated/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4851,10 +4851,22 @@ public class VectorL10n: NSObject {
public static var pollHistoryActiveSegmentTitle: String {
return VectorL10n.tr("Vector", "poll_history_active_segment_title")
}
/// Displaying polls
public static var pollHistoryLoadingText: String {
return VectorL10n.tr("Vector", "poll_history_loading_text")
}
/// There are no active polls for the past %@ days. Load more polls to view polls for previous months
public static func pollHistoryNoActivePollPeriodText(_ p1: String) -> String {
return VectorL10n.tr("Vector", "poll_history_no_active_poll_period_text", p1)
}
/// There are no active polls in this room
public static var pollHistoryNoActivePollText: String {
return VectorL10n.tr("Vector", "poll_history_no_active_poll_text")
}
/// There are no past polls for the past %@ days. Load more polls to view polls for previous months
public static func pollHistoryNoPastPollPeriodText(_ p1: String) -> String {
return VectorL10n.tr("Vector", "poll_history_no_past_poll_period_text", p1)
}
/// There are no past polls in this room
public static var pollHistoryNoPastPollText: String {
return VectorL10n.tr("Vector", "poll_history_no_past_poll_text")
Expand Down
2 changes: 1 addition & 1 deletion Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType {
coordinator.start()
push(coordinator: coordinator)
case .pollHistory:
let coordinator: PollHistoryCoordinator = .init(parameters: .init(mode: .active))
let coordinator: PollHistoryCoordinator = .init(parameters: .init(mode: .active, room: self.room))
coordinator.start()
push(coordinator: coordinator)
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import SwiftUI

struct PollHistoryCoordinatorParameters {
let mode: PollHistoryMode
let room: MXRoom
}

final class PollHistoryCoordinator: Coordinator, Presentable {
Expand All @@ -32,9 +33,7 @@ final class PollHistoryCoordinator: Coordinator, Presentable {

init(parameters: PollHistoryCoordinatorParameters) {
self.parameters = parameters

#warning("replace with the real service after that it's done")
let viewModel = PollHistoryViewModel(mode: parameters.mode, pollService: MockPollHistoryService())
let viewModel = PollHistoryViewModel(mode: parameters.mode, pollService: PollHistoryService(room: parameters.room, chunkSizeInDays: PollHistoryConstants.chunkSizeInDays))
let view = PollHistory(viewModel: viewModel.context)
pollHistoryViewModel = viewModel
pollHistoryHostingController = VectorHostingController(rootView: view)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
// limitations under the License.
//

import Combine
import Foundation
import SwiftUI

Expand All @@ -27,6 +28,7 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable {
case past
case activeEmpty
case pastEmpty
case loading

/// The associated screen
var screenType: Any.Type {
Expand All @@ -45,10 +47,19 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable {
pollHistoryMode = .past
case .activeEmpty:
pollHistoryMode = .active
pollService.activePollsData = []
pollService.nextPublisher = Empty(completeImmediately: true,
outputType: TimelinePollDetails.self,
failureType: Error.self).eraseToAnyPublisher()
case .pastEmpty:
pollHistoryMode = .past
pollService.pastPollsData = []
pollService.nextPublisher = Empty(completeImmediately: true,
outputType: TimelinePollDetails.self,
failureType: Error.self).eraseToAnyPublisher()
case .loading:
pollHistoryMode = .active
pollService.nextPublisher = Empty(completeImmediately: false,
outputType: TimelinePollDetails.self,
failureType: Error.self).eraseToAnyPublisher()
}

let viewModel = PollHistoryViewModel(mode: pollHistoryMode, pollService: pollService)
Expand Down
8 changes: 7 additions & 1 deletion RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@

// MARK: View model

enum PollHistoryConstants {
static let chunkSizeInDays: UInt = 30
}

enum PollHistoryViewModelResult: Equatable {
#warning("e.g. show poll detail")
}
Expand All @@ -37,7 +41,9 @@ struct PollHistoryViewState: BindableState {
}

var bindings: PollHistoryViewBindings
var polls: [PollListData] = []
var isLoading = false
var canLoadMoreContent = true
var polls: [TimelinePollDetails]?
}

enum PollHistoryViewAction {
Expand Down
72 changes: 49 additions & 23 deletions RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,15 @@
// limitations under the License.
//

import Combine
import SwiftUI

typealias PollHistoryViewModelType = StateStoreViewModel<PollHistoryViewState, PollHistoryViewAction>

final class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModelProtocol {
private let pollService: PollHistoryServiceProtocol
private var polls: [PollListData] = []
private var fetchingTask: Task<Void, Error>? {
didSet {
oldValue?.cancel()
}
}
private var polls: [TimelinePollDetails]?
private var subcriptions: Set<AnyCancellable> = .init()
alfogrillo marked this conversation as resolved.
Show resolved Hide resolved

var completion: ((PollHistoryViewModelResult) -> Void)?

Expand All @@ -39,39 +36,68 @@ final class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModel
override func process(viewAction: PollHistoryViewAction) {
switch viewAction {
case .viewAppeared:
fetchingTask = fetchPolls()
setupUpdateSubscriptions()
fetchFirstBatch()
case .segmentDidChange:
updatePolls()
updateViewState()
}
}
}

private extension PollHistoryViewModel {
func fetchPolls() -> Task<Void, Error> {
Task {
let polls = try await pollService.fetchHistory()

guard Task.isCancelled == false else {
return
func fetchFirstBatch() {
state.isLoading = true

pollService
.next()
.collect()
.sink { [weak self] _ in
#warning("Handle errors")
self?.state.isLoading = false
} receiveValue: { [weak self] polls in
self?.polls = polls
self?.updateViewState()
}

await MainActor.run {
self.polls = polls
updatePolls()
.store(in: &subcriptions)
}

func setupUpdateSubscriptions() {
subcriptions.removeAll()

pollService
.updates
.sink { [weak self] detail in
self?.updatePolls(with: detail)
self?.updateViewState()
}
.store(in: &subcriptions)

pollService
.pollErrors
.sink { detail in
#warning("Handle errors")
}
.store(in: &subcriptions)
}

func updatePolls(with poll: TimelinePollDetails) {
alfogrillo marked this conversation as resolved.
Show resolved Hide resolved
guard let pollIndex = polls?.firstIndex(where: { $0.id == poll.id }) else {
return
}

polls?[pollIndex] = poll
}

func updatePolls() {
let renderedPolls: [PollListData]
func updateViewState() {
let renderedPolls: [TimelinePollDetails]?

switch context.mode {
case .active:
renderedPolls = polls.filter { $0.winningOption == nil }
renderedPolls = polls?.filter { $0.closed == false }
case .past:
renderedPolls = polls.filter { $0.winningOption != nil }
renderedPolls = polls?.filter { $0.closed == true }
}

state.polls = renderedPolls
state.polls = renderedPolls?.sorted(by: { $0.startDate > $1.startDate })
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,141 @@
// limitations under the License.
//

import MatrixSDK
import Combine
import Foundation
import MatrixSDK

final class PollHistoryService: PollHistoryServiceProtocol {
func fetchHistory() async throws -> [PollListData] {
[]
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 pollAggregators: [String: PollAggregator] = [:]
private var targetTimestamp: Date
private var oldestEventDate: Date = .distantFuture
private var currentBatchSubject: PassthroughSubject<TimelinePollDetails, Error>?

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)
}

func next() -> AnyPublisher<TimelinePollDetails, Error> {
currentBatchSubject?.eraseToAnyPublisher() ?? startPagination()
}
}

private extension PollHistoryService {
enum Constants {
static let pageSize: UInt = 500
static let oneDayInSeconds: TimeInterval = 8.6 * 10e3
}

func setup(timeline: MXEventTimeline) {
timelineListener = timeline.listenToEvents { [weak self] event, _, _ in
if event.eventType == .pollStart {
self?.aggregatePoll(pollStartEvent: event)
}

self?.updateTimestamp(event: event)
}
}

func updateTimestamp(event: MXEvent) {
oldestEventDate = min(event.originServerDate, oldestEventDate)
}

func startPagination() -> AnyPublisher<TimelinePollDetails, Error> {
let batchSubject = PassthroughSubject<TimelinePollDetails, Error>()
currentBatchSubject = batchSubject

DispatchQueue.main.async { [weak self] in
guard let self = self else {
return
}
self.timeline.resetPagination()
self.paginate(timeline: self.timeline)
}

return batchSubject.eraseToAnyPublisher()
}

func paginate(timeline: MXEventTimeline) {
timeline.paginate(Constants.pageSize, direction: .backwards, onlyFromStore: false) { [weak self] response in
guard let self = self else {
return
}

switch response {
case .success:
if timeline.canPaginate(.backwards), self.timestampTargetReached == false {
self.paginate(timeline: timeline)
} else {
self.completeBatch(completion: .finished)
}
case .failure(let error):
self.completeBatch(completion: .failure(error))
}
}
}

func completeBatch(completion: Subscribers.Completion<Error>) {
currentBatchSubject?.send(completion: completion)
currentBatchSubject = nil
}

func aggregatePoll(pollStartEvent: MXEvent) {
guard pollAggregators[pollStartEvent.eventId] == nil else {
return
}

guard let aggregator = try? PollAggregator(session: room.mxSession, room: room, pollEvent: pollStartEvent, delegate: self) else {
return
}

pollAggregators[pollStartEvent.eventId] = aggregator
}

var timestampTargetReached: Bool {
oldestEventDate <= targetTimestamp
}
}

private extension MXEvent {
var originServerDate: Date {
.init(timeIntervalSince1970: Double(originServerTs) / 1000)
}
}

// MARK: - PollAggregatorDelegate

extension PollHistoryService: PollAggregatorDelegate {
func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) {}

func pollAggregatorDidEndLoading(_ aggregator: PollAggregator) {
currentBatchSubject?.send(.init(poll: aggregator.poll, represent: .started))
}

func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) {
pollErrorsSubject.send(didFailWithError)
}

func pollAggregatorDidUpdateData(_ aggregator: PollAggregator) {
updatesSubject.send(.init(poll: aggregator.poll, represent: .started))
}
}
Loading