TimelineKind refactor (#3193)

This commit is contained in:
Mauro
2024-08-22 20:33:21 +02:00
committed by GitHub
parent 8c07397548
commit 993fe8cd36
25 changed files with 69 additions and 71 deletions
@@ -11910,11 +11910,11 @@ class RoomTimelineProviderMock: RoomTimelineProviderProtocol {
set(value) { underlyingPaginationState = value } set(value) { underlyingPaginationState = value }
} }
var underlyingPaginationState: PaginationState! var underlyingPaginationState: PaginationState!
var isLive: Bool { var kind: TimelineKind {
get { return underlyingIsLive } get { return underlyingKind }
set(value) { underlyingIsLive = value } set(value) { underlyingKind = value }
} }
var underlyingIsLive: Bool! var underlyingKind: TimelineKind!
var membershipChangePublisher: AnyPublisher<Void, Never> { var membershipChangePublisher: AnyPublisher<Void, Never> {
get { return underlyingMembershipChangePublisher } get { return underlyingMembershipChangePublisher }
set(value) { underlyingMembershipChangePublisher = value } set(value) { underlyingMembershipChangePublisher = value }
@@ -35,7 +35,7 @@ class AutoUpdatingRoomTimelineProviderMock: RoomTimelineProvider {
} }
super.init(timeline: timelineMock, super.init(timeline: timelineMock,
isLive: true, kind: .live,
paginationStatePublisher: innerPaginationStatePublisher.eraseToAnyPublisher()) paginationStatePublisher: innerPaginationStatePublisher.eraseToAnyPublisher())
Task.detached { Task.detached {
@@ -52,7 +52,6 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
viewModel = PinnedEventsTimelineScreenViewModel() viewModel = PinnedEventsTimelineScreenViewModel()
timelineViewModel = TimelineViewModel(roomProxy: parameters.roomProxy, timelineViewModel = TimelineViewModel(roomProxy: parameters.roomProxy,
timelineController: parameters.timelineController, timelineController: parameters.timelineController,
isPinnedEventsTimeline: true,
mediaProvider: parameters.mediaProvider, mediaProvider: parameters.mediaProvider,
mediaPlayerProvider: parameters.mediaPlayerProvider, mediaPlayerProvider: parameters.mediaPlayerProvider,
voiceMessageMediaManager: parameters.voiceMessageMediaManager, voiceMessageMediaManager: parameters.voiceMessageMediaManager,
@@ -95,11 +95,10 @@ struct PinnedEventsTimelineScreen: View {
struct PinnedEventsTimelineScreen_Previews: PreviewProvider, TestablePreview { struct PinnedEventsTimelineScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = PinnedEventsTimelineScreenViewModel() static let viewModel = PinnedEventsTimelineScreenViewModel()
static let emptyTimelineViewModel: TimelineViewModel = { static let emptyTimelineViewModel: TimelineViewModel = {
let timelineController = MockRoomTimelineController() let timelineController = MockRoomTimelineController(timelineKind: .pinned)
timelineController.timelineItems = [] timelineController.timelineItems = []
return TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Preview room")), return TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Preview room")),
timelineController: timelineController, timelineController: timelineController,
isPinnedEventsTimeline: true,
mediaProvider: MockMediaProvider(), mediaProvider: MockMediaProvider(),
mediaPlayerProvider: MediaPlayerProviderMock(), mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
@@ -79,7 +79,6 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
timelineViewModel = TimelineViewModel(roomProxy: parameters.roomProxy, timelineViewModel = TimelineViewModel(roomProxy: parameters.roomProxy,
focussedEventID: parameters.focussedEvent?.eventID, focussedEventID: parameters.focussedEvent?.eventID,
timelineController: parameters.timelineController, timelineController: parameters.timelineController,
isPinnedEventsTimeline: false,
mediaProvider: parameters.mediaProvider, mediaProvider: parameters.mediaProvider,
mediaPlayerProvider: parameters.mediaPlayerProvider, mediaPlayerProvider: parameters.mediaPlayerProvider,
voiceMessageMediaManager: parameters.voiceMessageMediaManager, voiceMessageMediaManager: parameters.voiceMessageMediaManager,
@@ -54,19 +54,20 @@ struct PinnedItemsBannerView: View {
@ViewBuilder @ViewBuilder
private var viewAllButton: some View { private var viewAllButton: some View {
switch state {
case .loaded:
Button { onViewAllButtonTap() } label: { Button { onViewAllButtonTap() } label: {
Text(L10n.screenRoomPinnedBannerViewAllButtonTitle) Text(state.isLoading ? "" : L10n.screenRoomPinnedBannerViewAllButtonTitle)
.font(.compound.bodyMDSemibold) .font(.compound.bodyMDSemibold)
.foregroundStyle(Color.compound.textPrimary) .foregroundStyle(Color.compound.textPrimary)
.opacity(state.isLoading ? 0 : 1)
// Use overlay instead otherwise the sliding animation would not work
.overlay(alignment: .trailing) {
ProgressView()
.opacity(state.isLoading ? 1 : 0)
}
.padding(.horizontal, 16) .padding(.horizontal, 16)
.padding(.vertical, 5) .padding(.vertical, 5)
} }
case .loading: .disabled(state.isLoading)
ProgressView()
.padding(.horizontal, 16)
}
} }
private var content: some View { private var content: some View {
@@ -220,7 +220,6 @@ struct RoomScreen_Previews: PreviewProvider, TestablePreview {
static let roomViewModel = RoomScreenViewModel.mock(roomProxyMock: roomProxyMock) static let roomViewModel = RoomScreenViewModel.mock(roomProxyMock: roomProxyMock)
static let timelineViewModel = TimelineViewModel(roomProxy: roomProxyMock, static let timelineViewModel = TimelineViewModel(roomProxy: roomProxyMock,
timelineController: MockRoomTimelineController(), timelineController: MockRoomTimelineController(),
isPinnedEventsTimeline: false,
mediaProvider: MockMediaProvider(), mediaProvider: MockMediaProvider(),
mediaPlayerProvider: MediaPlayerProviderMock(), mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
@@ -52,7 +52,6 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
init(roomProxy: JoinedRoomProxyProtocol, init(roomProxy: JoinedRoomProxyProtocol,
focussedEventID: String? = nil, focussedEventID: String? = nil,
timelineController: RoomTimelineControllerProtocol, timelineController: RoomTimelineControllerProtocol,
isPinnedEventsTimeline: Bool,
mediaProvider: MediaProviderProtocol, mediaProvider: MediaProviderProtocol,
mediaPlayerProvider: MediaPlayerProviderProtocol, mediaPlayerProvider: MediaPlayerProviderProtocol,
voiceMessageMediaManager: VoiceMessageMediaManagerProtocol, voiceMessageMediaManager: VoiceMessageMediaManagerProtocol,
@@ -81,7 +80,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
appSettings: appSettings, appSettings: appSettings,
analyticsService: analyticsService) analyticsService: analyticsService)
super.init(initialViewState: TimelineViewState(isPinnedEventsTimeline: isPinnedEventsTimeline, super.init(initialViewState: TimelineViewState(isPinnedEventsTimeline: timelineController.timelineKind == .pinned,
roomID: roomProxy.id, roomID: roomProxy.id,
isEncryptedOneToOneRoom: roomProxy.isEncryptedOneToOneRoom, isEncryptedOneToOneRoom: roomProxy.isEncryptedOneToOneRoom,
timelineViewState: TimelineState(focussedEvent: focussedEventID.map { .init(eventID: $0, appearance: .immediate) }), timelineViewState: TimelineState(focussedEvent: focussedEventID.map { .init(eventID: $0, appearance: .immediate) }),
@@ -827,7 +826,6 @@ extension TimelineViewModel {
static let mock = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Preview room")), static let mock = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Preview room")),
focussedEventID: nil, focussedEventID: nil,
timelineController: MockRoomTimelineController(), timelineController: MockRoomTimelineController(),
isPinnedEventsTimeline: false,
mediaProvider: MockMediaProvider(), mediaProvider: MockMediaProvider(),
mediaPlayerProvider: MediaPlayerProviderMock(), mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
@@ -838,8 +836,7 @@ extension TimelineViewModel {
static let pinnedEventsTimelineMock = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Preview room")), static let pinnedEventsTimelineMock = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Preview room")),
focussedEventID: nil, focussedEventID: nil,
timelineController: MockRoomTimelineController(), timelineController: MockRoomTimelineController(timelineKind: .pinned),
isPinnedEventsTimeline: true,
mediaProvider: MockMediaProvider(), mediaProvider: MockMediaProvider(),
mediaPlayerProvider: MediaPlayerProviderMock(), mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
@@ -54,7 +54,6 @@ struct ReadReceiptsSummaryView_Previews: PreviewProvider, TestablePreview {
let roomProxyMock = JoinedRoomProxyMock(.init(name: "Room", members: members)) let roomProxyMock = JoinedRoomProxyMock(.init(name: "Room", members: members))
let mock = TimelineViewModel(roomProxy: roomProxyMock, let mock = TimelineViewModel(roomProxy: roomProxyMock,
timelineController: MockRoomTimelineController(), timelineController: MockRoomTimelineController(),
isPinnedEventsTimeline: false,
mediaProvider: MockMediaProvider(), mediaProvider: MockMediaProvider(),
mediaPlayerProvider: MediaPlayerProviderMock(), mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
@@ -92,7 +92,6 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview {
static let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Test", members: members)), static let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Test", members: members)),
timelineController: MockRoomTimelineController(), timelineController: MockRoomTimelineController(),
isPinnedEventsTimeline: false,
mediaProvider: MockMediaProvider(), mediaProvider: MockMediaProvider(),
mediaPlayerProvider: MediaPlayerProviderMock(), mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
@@ -99,7 +99,6 @@ struct HighlightedTimelineItemTimeline_Previews: PreviewProvider {
static let timelineViewModel = TimelineViewModel(roomProxy: roomProxyMock, static let timelineViewModel = TimelineViewModel(roomProxy: roomProxyMock,
focussedEventID: focussedEventID, focussedEventID: focussedEventID,
timelineController: MockRoomTimelineController(), timelineController: MockRoomTimelineController(),
isPinnedEventsTimeline: false,
mediaProvider: MockMediaProvider(), mediaProvider: MockMediaProvider(),
mediaPlayerProvider: MediaPlayerProviderMock(), mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
@@ -84,7 +84,6 @@ struct TimelineView_Previews: PreviewProvider, TestablePreview {
static let roomViewModel = RoomScreenViewModel.mock(roomProxyMock: roomProxyMock) static let roomViewModel = RoomScreenViewModel.mock(roomProxyMock: roomProxyMock)
static let timelineViewModel = TimelineViewModel(roomProxy: roomProxyMock, static let timelineViewModel = TimelineViewModel(roomProxy: roomProxyMock,
timelineController: MockRoomTimelineController(), timelineController: MockRoomTimelineController(),
isPinnedEventsTimeline: false,
mediaProvider: MockMediaProvider(), mediaProvider: MockMediaProvider(),
mediaPlayerProvider: MediaPlayerProviderMock(), mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
@@ -44,7 +44,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
} }
do { do {
let timeline = try await TimelineProxy(timeline: room.pinnedEventsTimeline(internalIdPrefix: nil, maxEventsToLoad: 100), isLive: true) let timeline = try await TimelineProxy(timeline: room.pinnedEventsTimeline(internalIdPrefix: nil, maxEventsToLoad: 100), kind: .pinned)
await timeline.subscribeForUpdates() await timeline.subscribeForUpdates()
innerPinnedEventsTimeline = timeline innerPinnedEventsTimeline = timeline
return timeline return timeline
@@ -172,7 +172,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
self.roomListItem = roomListItem self.roomListItem = roomListItem
self.room = room self.room = room
timeline = try await TimelineProxy(timeline: room.timeline(), isLive: true) timeline = try await TimelineProxy(timeline: room.timeline(), kind: .live)
Task { Task {
await updateMembers() await updateMembers()
@@ -216,7 +216,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
func timelineFocusedOnEvent(eventID: String, numberOfEvents: UInt16) async -> Result<TimelineProxyProtocol, RoomProxyError> { func timelineFocusedOnEvent(eventID: String, numberOfEvents: UInt16) async -> Result<TimelineProxyProtocol, RoomProxyError> {
do { do {
let timeline = try await room.timelineFocusedOnEvent(eventId: eventID, numContextEvents: numberOfEvents, internalIdPrefix: UUID().uuidString) let timeline = try await room.timelineFocusedOnEvent(eventId: eventID, numContextEvents: numberOfEvents, internalIdPrefix: UUID().uuidString)
return .success(TimelineProxy(timeline: timeline, isLive: false)) return .success(TimelineProxy(timeline: timeline, kind: .detached))
} catch let error as FocusEventError { } catch let error as FocusEventError {
switch error { switch error {
case .InvalidEventId(_, let error): case .InvalidEventId(_, let error):
@@ -42,7 +42,7 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
private(set) var isLive: Bool let kind: TimelineKind
private let membershipChangeSubject = PassthroughSubject<Void, Never>() private let membershipChangeSubject = PassthroughSubject<Void, Never>()
var membershipChangePublisher: AnyPublisher<Void, Never> { var membershipChangePublisher: AnyPublisher<Void, Never> {
@@ -54,10 +54,10 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
roomTimelineObservationToken?.cancel() roomTimelineObservationToken?.cancel()
} }
init(timeline: Timeline, isLive: Bool, paginationStatePublisher: AnyPublisher<PaginationState, Never>) { init(timeline: Timeline, kind: TimelineKind, paginationStatePublisher: AnyPublisher<PaginationState, Never>) {
serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomtimelineprovider", qos: .utility) serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomtimelineprovider", qos: .utility)
itemProxiesSubject = CurrentValueSubject<[TimelineItemProxy], Never>([]) itemProxiesSubject = CurrentValueSubject<[TimelineItemProxy], Never>([])
self.isLive = isLive self.kind = kind
paginationStatePublisher paginationStatePublisher
.sink { [weak self] in .sink { [weak self] in
@@ -45,8 +45,8 @@ protocol RoomTimelineProviderProtocol {
var itemProxies: [TimelineItemProxy] { get } var itemProxies: [TimelineItemProxy] { get }
/// Whether the timeline is back/forward paginating or not (or has reached the start/end of the room). /// Whether the timeline is back/forward paginating or not (or has reached the start/end of the room).
var paginationState: PaginationState { get } var paginationState: PaginationState { get }
/// Whether or not the provider is for a live timeline. /// The kind of the timeline
var isLive: Bool { get } var kind: TimelineKind { get }
/// A publisher that signals when changes to the room's membership have occurred through `/sync`. /// A publisher that signals when changes to the room's membership have occurred through `/sync`.
/// ///
/// This is temporary and will be replace by a subscription on the room itself. /// This is temporary and will be replace by a subscription on the room itself.
@@ -28,6 +28,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
var roomProxy: JoinedRoomProxyProtocol? var roomProxy: JoinedRoomProxyProtocol?
var roomID: String { roomProxy?.id ?? "MockRoomIdentifier" } var roomID: String { roomProxy?.id ?? "MockRoomIdentifier" }
var timelineKind: TimelineKind
let callbacks = PassthroughSubject<RoomTimelineControllerCallback, Never>() let callbacks = PassthroughSubject<RoomTimelineControllerCallback, Never>()
@@ -36,7 +37,8 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
private var client: UITestsSignalling.Client? private var client: UITestsSignalling.Client?
init(listenForSignals: Bool = false) { init(timelineKind: TimelineKind = .live, listenForSignals: Bool = false) {
self.timelineKind = timelineKind
callbacks.send(.paginationState(PaginationState(backward: .idle, forward: .timelineEndReached))) callbacks.send(.paginationState(PaginationState(backward: .idle, forward: .timelineEndReached)))
callbacks.send(.isLive(true)) callbacks.send(.isLive(true))
@@ -25,7 +25,6 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
private let timelineItemFactory: RoomTimelineItemFactoryProtocol private let timelineItemFactory: RoomTimelineItemFactoryProtocol
private let appSettings: AppSettings private let appSettings: AppSettings
private let serialDispatchQueue: DispatchQueue private let serialDispatchQueue: DispatchQueue
private let shouldHideStart: Bool
let callbacks = PassthroughSubject<RoomTimelineControllerCallback, Never>() let callbacks = PassthroughSubject<RoomTimelineControllerCallback, Never>()
@@ -42,17 +41,19 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
roomProxy.id roomProxy.id
} }
var timelineKind: TimelineKind {
liveTimelineProvider.kind
}
init(roomProxy: JoinedRoomProxyProtocol, init(roomProxy: JoinedRoomProxyProtocol,
timelineProxy: TimelineProxyProtocol, timelineProxy: TimelineProxyProtocol,
initialFocussedEventID: String?, initialFocussedEventID: String?,
shouldHideStart: Bool,
timelineItemFactory: RoomTimelineItemFactoryProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol,
appSettings: AppSettings) { appSettings: AppSettings) {
self.roomProxy = roomProxy self.roomProxy = roomProxy
liveTimelineProvider = timelineProxy.timelineProvider liveTimelineProvider = timelineProxy.timelineProvider
self.timelineItemFactory = timelineItemFactory self.timelineItemFactory = timelineItemFactory
self.appSettings = appSettings self.appSettings = appSettings
self.shouldHideStart = shouldHideStart
serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomtimelineprovider", qos: .utility) serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomtimelineprovider", qos: .utility)
activeTimeline = timelineProxy activeTimeline = timelineProxy
@@ -312,7 +313,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
// Inform the world that the initial items are loading from the store // Inform the world that the initial items are loading from the store
callbacks.send(.paginationState(.init(backward: .paginating, forward: .paginating))) callbacks.send(.paginationState(.init(backward: .paginating, forward: .paginating)))
callbacks.send(.isLive(activeTimelineProvider.isLive)) callbacks.send(.isLive(activeTimelineProvider.kind == .live))
updateTimelineItemsCancellable = activeTimelineProvider updateTimelineItemsCancellable = activeTimelineProvider
.updatePublisher .updatePublisher
@@ -367,7 +368,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
// Check if we need to add anything to the top of the timeline. // Check if we need to add anything to the top of the timeline.
switch paginationState.backward { switch paginationState.backward {
case .timelineEndReached: case .timelineEndReached:
if !shouldHideStart, !roomProxy.isEncryptedOneToOneRoom { if timelineKind != .pinned, !roomProxy.isEncryptedOneToOneRoom {
let timelineStart = TimelineStartRoomTimelineItem(name: roomProxy.name) let timelineStart = TimelineStartRoomTimelineItem(name: roomProxy.name)
newTimelineItems.insert(timelineStart, at: 0) newTimelineItems.insert(timelineStart, at: 0)
} }
@@ -23,7 +23,6 @@ struct RoomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol {
RoomTimelineController(roomProxy: roomProxy, RoomTimelineController(roomProxy: roomProxy,
timelineProxy: roomProxy.timeline, timelineProxy: roomProxy.timeline,
initialFocussedEventID: initialFocussedEventID, initialFocussedEventID: initialFocussedEventID,
shouldHideStart: false,
timelineItemFactory: timelineItemFactory, timelineItemFactory: timelineItemFactory,
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
} }
@@ -36,7 +35,6 @@ struct RoomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol {
return RoomTimelineController(roomProxy: roomProxy, return RoomTimelineController(roomProxy: roomProxy,
timelineProxy: pinnedEventsTimeline, timelineProxy: pinnedEventsTimeline,
initialFocussedEventID: nil, initialFocussedEventID: nil,
shouldHideStart: true,
timelineItemFactory: timelineItemFactory, timelineItemFactory: timelineItemFactory,
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
} }
@@ -38,6 +38,7 @@ enum RoomTimelineControllerError: Error {
@MainActor @MainActor
protocol RoomTimelineControllerProtocol { protocol RoomTimelineControllerProtocol {
var roomID: String { get } var roomID: String { get }
var timelineKind: TimelineKind { get }
var timelineItems: [RoomTimelineItemProtocol] { get } var timelineItems: [RoomTimelineItemProtocol] { get }
var callbacks: PassthroughSubject<RoomTimelineControllerCallback, Never> { get } var callbacks: PassthroughSubject<RoomTimelineControllerCallback, Never> { get }
@@ -27,7 +27,7 @@ final class TimelineProxy: TimelineProxyProtocol {
private let backPaginationStatusSubject = CurrentValueSubject<PaginationStatus, Never>(.timelineEndReached) private let backPaginationStatusSubject = CurrentValueSubject<PaginationStatus, Never>(.timelineEndReached)
private let forwardPaginationStatusSubject = CurrentValueSubject<PaginationStatus, Never>(.timelineEndReached) private let forwardPaginationStatusSubject = CurrentValueSubject<PaginationStatus, Never>(.timelineEndReached)
let isLive: Bool private let kind: TimelineKind
private var innerTimelineProvider: RoomTimelineProviderProtocol! private var innerTimelineProvider: RoomTimelineProviderProtocol!
var timelineProvider: RoomTimelineProviderProtocol { var timelineProvider: RoomTimelineProviderProtocol {
@@ -38,9 +38,9 @@ final class TimelineProxy: TimelineProxyProtocol {
backPaginationStatusObservationToken?.cancel() backPaginationStatusObservationToken?.cancel()
} }
init(timeline: Timeline, isLive: Bool) { init(timeline: Timeline, kind: TimelineKind) {
self.timeline = timeline self.timeline = timeline
self.isLive = isLive self.kind = kind
} }
func subscribeForUpdates() async { func subscribeForUpdates() async {
@@ -56,7 +56,7 @@ final class TimelineProxy: TimelineProxyProtocol {
await subscribeToPagination() await subscribeToPagination()
let provider = await RoomTimelineProvider(timeline: timeline, isLive: isLive, paginationStatePublisher: paginationStatePublisher) let provider = await RoomTimelineProvider(timeline: timeline, kind: kind, paginationStatePublisher: paginationStatePublisher)
// Make sure the existing items are built so that we have content in the timeline before // Make sure the existing items are built so that we have content in the timeline before
// determining whether or not the timeline should paginate to load more items. // determining whether or not the timeline should paginate to load more items.
await provider.waitForInitialItems() await provider.waitForInitialItems()
@@ -84,15 +84,21 @@ final class TimelineProxy: TimelineProxyProtocol {
// We can't subscribe to back pagination on detached timelines and as live timelines // We can't subscribe to back pagination on detached timelines and as live timelines
// can be shared between multiple instances of the same room on the stack, it is // can be shared between multiple instances of the same room on the stack, it is
// safer to still use the subscription logic for back pagination when live. // safer to still use the subscription logic for back pagination when live.
await if isLive { switch kind {
paginateBackwardsOnLive(requestSize: requestSize) case .live:
} else { return await paginateBackwardsOnLive(requestSize: requestSize)
focussedPaginate(.backwards, requestSize: requestSize) case .detached:
return await focussedPaginate(.backwards, requestSize: requestSize)
case .pinned:
return .success(())
} }
} }
func paginateForwards(requestSize: UInt16) async -> Result<Void, TimelineProxyError> { func paginateForwards(requestSize: UInt16) async -> Result<Void, TimelineProxyError> {
await focussedPaginate(.forwards, requestSize: requestSize) guard kind != .pinned else {
return .success(())
}
return await focussedPaginate(.forwards, requestSize: requestSize)
} }
/// Paginate backwards using the subscription from Rust to drive the pagination state. /// Paginate backwards using the subscription from Rust to drive the pagination state.
@@ -538,7 +544,8 @@ final class TimelineProxy: TimelineProxyProtocol {
} }
private func subscribeToPagination() async { private func subscribeToPagination() async {
if isLive { switch kind {
case .live:
let backPaginationListener = RoomPaginationStatusListener { [weak self] status in let backPaginationListener = RoomPaginationStatusListener { [weak self] status in
guard let self else { guard let self else {
return return
@@ -557,13 +564,15 @@ final class TimelineProxy: TimelineProxyProtocol {
} catch { } catch {
MXLog.error("Failed to subscribe to back pagination status with error: \(error)") MXLog.error("Failed to subscribe to back pagination status with error: \(error)")
} }
} else { forwardPaginationStatusSubject.send(.timelineEndReached)
case .detached:
// Detached timelines don't support observation, set the initial state ourself. // Detached timelines don't support observation, set the initial state ourself.
backPaginationStatusSubject.send(.idle) backPaginationStatusSubject.send(.idle)
forwardPaginationStatusSubject.send(.idle)
case .pinned:
backPaginationStatusSubject.send(.timelineEndReached)
forwardPaginationStatusSubject.send(.timelineEndReached)
} }
// Detached timelines don't support observation, set the initial state ourself.
forwardPaginationStatusSubject.send(isLive ? .timelineEndReached : .idle)
} }
} }
@@ -18,6 +18,12 @@ import Combine
import Foundation import Foundation
import MatrixRustSDK import MatrixRustSDK
enum TimelineKind {
case live
case detached
case pinned
}
enum TimelineProxyError: Error { enum TimelineProxyError: Error {
case sdkError(Error) case sdkError(Error)
@@ -649,7 +649,6 @@ class MockScreen: Identifiable {
let timelineController = RoomTimelineController(roomProxy: roomProxy, let timelineController = RoomTimelineController(roomProxy: roomProxy,
timelineProxy: roomProxy.timeline, timelineProxy: roomProxy.timeline,
initialFocussedEventID: nil, initialFocussedEventID: nil,
shouldHideStart: false,
timelineItemFactory: RoomTimelineItemFactory(userID: "@alice:matrix.org", timelineItemFactory: RoomTimelineItemFactory(userID: "@alice:matrix.org",
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
stateEventStringBuilder: RoomStateEventStringBuilder(userID: "@alice:matrix.org")), stateEventStringBuilder: RoomStateEventStringBuilder(userID: "@alice:matrix.org")),
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:919e3df86ce263d06f479c701b6981a66b0d6217e13cf3cff12445b9c6a24d77 oid sha256:6ada9407d2f6c9da3cedb750353ffcf6e4cc6493177b3e2796d66b8245982042
size 106938 size 107677
-3
View File
@@ -28,7 +28,6 @@ class PillContextTests: XCTestCase {
proxyMock.membersPublisher = subject.asCurrentValuePublisher() proxyMock.membersPublisher = subject.asCurrentValuePublisher()
let mock = TimelineViewModel(roomProxy: proxyMock, let mock = TimelineViewModel(roomProxy: proxyMock,
timelineController: MockRoomTimelineController(), timelineController: MockRoomTimelineController(),
isPinnedEventsTimeline: false,
mediaProvider: MockMediaProvider(), mediaProvider: MockMediaProvider(),
mediaPlayerProvider: MediaPlayerProviderMock(), mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
@@ -57,7 +56,6 @@ class PillContextTests: XCTestCase {
proxyMock.membersPublisher = subject.asCurrentValuePublisher() proxyMock.membersPublisher = subject.asCurrentValuePublisher()
let mock = TimelineViewModel(roomProxy: proxyMock, let mock = TimelineViewModel(roomProxy: proxyMock,
timelineController: MockRoomTimelineController(), timelineController: MockRoomTimelineController(),
isPinnedEventsTimeline: false,
mediaProvider: MockMediaProvider(), mediaProvider: MockMediaProvider(),
mediaPlayerProvider: MediaPlayerProviderMock(), mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
@@ -79,7 +77,6 @@ class PillContextTests: XCTestCase {
mockController.roomProxy = proxyMock mockController.roomProxy = proxyMock
let mock = TimelineViewModel(roomProxy: proxyMock, let mock = TimelineViewModel(roomProxy: proxyMock,
timelineController: mockController, timelineController: mockController,
isPinnedEventsTimeline: false,
mediaProvider: MockMediaProvider(), mediaProvider: MockMediaProvider(),
mediaPlayerProvider: MediaPlayerProviderMock(), mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
@@ -345,7 +345,6 @@ class TimelineViewModelTests: XCTestCase {
let viewModel = TimelineViewModel(roomProxy: roomProxy, let viewModel = TimelineViewModel(roomProxy: roomProxy,
timelineController: timelineController, timelineController: timelineController,
isPinnedEventsTimeline: false,
mediaProvider: MockMediaProvider(), mediaProvider: MockMediaProvider(),
mediaPlayerProvider: MediaPlayerProviderMock(), mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
@@ -370,7 +369,6 @@ class TimelineViewModelTests: XCTestCase {
timelineController.timelineItems = [message] timelineController.timelineItems = [message]
let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "", members: [RoomMemberProxyMock.mockAlice, RoomMemberProxyMock.mockCharlie])), let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "", members: [RoomMemberProxyMock.mockAlice, RoomMemberProxyMock.mockCharlie])),
timelineController: timelineController, timelineController: timelineController,
isPinnedEventsTimeline: false,
mediaProvider: MockMediaProvider(), mediaProvider: MockMediaProvider(),
mediaPlayerProvider: MediaPlayerProviderMock(), mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
@@ -396,7 +394,6 @@ class TimelineViewModelTests: XCTestCase {
roomProxyMock.underlyingActionsPublisher = actionsSubject.eraseToAnyPublisher() roomProxyMock.underlyingActionsPublisher = actionsSubject.eraseToAnyPublisher()
let viewModel = TimelineViewModel(roomProxy: roomProxyMock, let viewModel = TimelineViewModel(roomProxy: roomProxyMock,
timelineController: MockRoomTimelineController(), timelineController: MockRoomTimelineController(),
isPinnedEventsTimeline: false,
mediaProvider: MockMediaProvider(), mediaProvider: MockMediaProvider(),
mediaPlayerProvider: MediaPlayerProviderMock(), mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
@@ -425,7 +422,6 @@ class TimelineViewModelTests: XCTestCase {
roomProxyMock.underlyingActionsPublisher = actionsSubject.eraseToAnyPublisher() roomProxyMock.underlyingActionsPublisher = actionsSubject.eraseToAnyPublisher()
let viewModel = TimelineViewModel(roomProxy: roomProxyMock, let viewModel = TimelineViewModel(roomProxy: roomProxyMock,
timelineController: MockRoomTimelineController(), timelineController: MockRoomTimelineController(),
isPinnedEventsTimeline: false,
mediaProvider: MockMediaProvider(), mediaProvider: MockMediaProvider(),
mediaPlayerProvider: MediaPlayerProviderMock(), mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
@@ -455,7 +451,6 @@ class TimelineViewModelTests: XCTestCase {
TimelineViewModel(roomProxy: roomProxy ?? JoinedRoomProxyMock(.init(name: "")), TimelineViewModel(roomProxy: roomProxy ?? JoinedRoomProxyMock(.init(name: "")),
focussedEventID: focussedEventID, focussedEventID: focussedEventID,
timelineController: timelineController, timelineController: timelineController,
isPinnedEventsTimeline: false,
mediaProvider: MockMediaProvider(), mediaProvider: MockMediaProvider(),
mediaPlayerProvider: MediaPlayerProviderMock(), mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(),