From 993fe8cd366954f23589adda2cffdbe055e0487b Mon Sep 17 00:00:00 2001 From: Mauro <34335419+Velin92@users.noreply.github.com> Date: Thu, 22 Aug 2024 20:33:21 +0200 Subject: [PATCH] `TimelineKind` refactor (#3193) --- .../Mocks/Generated/GeneratedMocks.swift | 8 ++-- .../Mocks/RoomTimelineProviderMock.swift | 2 +- ...innedEventsTimelineScreenCoordinator.swift | 1 - .../View/PinnedEventsTimelineScreen.swift | 3 +- .../RoomScreen/RoomScreenCoordinator.swift | 1 - .../PinnedItemsBannerView.swift | 23 ++++++------ .../Screens/RoomScreen/View/RoomScreen.swift | 1 - .../Screens/Timeline/TimelineViewModel.swift | 7 +--- .../ReadReceiptsSummaryView.swift | 1 - .../TimelineReadReceiptsView.swift | 1 - .../HighlightedTimelineItemModifier.swift | 1 - .../Screens/Timeline/View/TimelineView.swift | 1 - .../Services/Room/JoinedRoomProxy.swift | 6 +-- .../Timeline/RoomTimelineProvider.swift | 6 +-- .../RoomTimelineProviderProtocol.swift | 4 +- .../MockRoomTimelineController.swift | 4 +- .../RoomTimelineController.swift | 11 +++--- .../RoomTimelineControllerFactory.swift | 2 - .../RoomTimelineControllerProtocol.swift | 1 + .../Services/Timeline/TimelineProxy.swift | 37 ++++++++++++------- .../Timeline/TimelineProxyProtocol.swift | 6 +++ .../UITests/UITestsAppCoordinator.swift | 1 - ...nnedItemsBannerView-iPhone-15-pseudo.1.png | 4 +- UnitTests/Sources/PillContextTests.swift | 3 -- .../Sources/TimelineViewModelTests.swift | 5 --- 25 files changed, 69 insertions(+), 71 deletions(-) diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index e9bf04f3d0..fac3902160 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -11910,11 +11910,11 @@ class RoomTimelineProviderMock: RoomTimelineProviderProtocol { set(value) { underlyingPaginationState = value } } var underlyingPaginationState: PaginationState! - var isLive: Bool { - get { return underlyingIsLive } - set(value) { underlyingIsLive = value } + var kind: TimelineKind { + get { return underlyingKind } + set(value) { underlyingKind = value } } - var underlyingIsLive: Bool! + var underlyingKind: TimelineKind! var membershipChangePublisher: AnyPublisher { get { return underlyingMembershipChangePublisher } set(value) { underlyingMembershipChangePublisher = value } diff --git a/ElementX/Sources/Mocks/RoomTimelineProviderMock.swift b/ElementX/Sources/Mocks/RoomTimelineProviderMock.swift index 1a13ce597f..7bb1725b9a 100644 --- a/ElementX/Sources/Mocks/RoomTimelineProviderMock.swift +++ b/ElementX/Sources/Mocks/RoomTimelineProviderMock.swift @@ -35,7 +35,7 @@ class AutoUpdatingRoomTimelineProviderMock: RoomTimelineProvider { } super.init(timeline: timelineMock, - isLive: true, + kind: .live, paginationStatePublisher: innerPaginationStatePublisher.eraseToAnyPublisher()) Task.detached { diff --git a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift index 3257d82974..52c0cb7159 100644 --- a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift +++ b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift @@ -52,7 +52,6 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol { viewModel = PinnedEventsTimelineScreenViewModel() timelineViewModel = TimelineViewModel(roomProxy: parameters.roomProxy, timelineController: parameters.timelineController, - isPinnedEventsTimeline: true, mediaProvider: parameters.mediaProvider, mediaPlayerProvider: parameters.mediaPlayerProvider, voiceMessageMediaManager: parameters.voiceMessageMediaManager, diff --git a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift index 87db39bd86..582c6eafd1 100644 --- a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift +++ b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift @@ -95,11 +95,10 @@ struct PinnedEventsTimelineScreen: View { struct PinnedEventsTimelineScreen_Previews: PreviewProvider, TestablePreview { static let viewModel = PinnedEventsTimelineScreenViewModel() static let emptyTimelineViewModel: TimelineViewModel = { - let timelineController = MockRoomTimelineController() + let timelineController = MockRoomTimelineController(timelineKind: .pinned) timelineController.timelineItems = [] return TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Preview room")), timelineController: timelineController, - isPinnedEventsTimeline: true, mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index c08229f62f..f9bc2f8cf9 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -79,7 +79,6 @@ final class RoomScreenCoordinator: CoordinatorProtocol { timelineViewModel = TimelineViewModel(roomProxy: parameters.roomProxy, focussedEventID: parameters.focussedEvent?.eventID, timelineController: parameters.timelineController, - isPinnedEventsTimeline: false, mediaProvider: parameters.mediaProvider, mediaPlayerProvider: parameters.mediaPlayerProvider, voiceMessageMediaManager: parameters.voiceMessageMediaManager, diff --git a/ElementX/Sources/Screens/RoomScreen/View/PinnedItemsBanner/PinnedItemsBannerView.swift b/ElementX/Sources/Screens/RoomScreen/View/PinnedItemsBanner/PinnedItemsBannerView.swift index 38c2bcd57b..07621e9d88 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/PinnedItemsBanner/PinnedItemsBannerView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/PinnedItemsBanner/PinnedItemsBannerView.swift @@ -54,19 +54,20 @@ struct PinnedItemsBannerView: View { @ViewBuilder private var viewAllButton: some View { - switch state { - case .loaded: - Button { onViewAllButtonTap() } label: { - Text(L10n.screenRoomPinnedBannerViewAllButtonTitle) - .font(.compound.bodyMDSemibold) - .foregroundStyle(Color.compound.textPrimary) - .padding(.horizontal, 16) - .padding(.vertical, 5) - } - case .loading: - ProgressView() + Button { onViewAllButtonTap() } label: { + Text(state.isLoading ? "" : L10n.screenRoomPinnedBannerViewAllButtonTitle) + .font(.compound.bodyMDSemibold) + .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(.vertical, 5) } + .disabled(state.isLoading) } private var content: some View { diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index 5c7305bcbf..39877a3410 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -220,7 +220,6 @@ struct RoomScreen_Previews: PreviewProvider, TestablePreview { static let roomViewModel = RoomScreenViewModel.mock(roomProxyMock: roomProxyMock) static let timelineViewModel = TimelineViewModel(roomProxy: roomProxyMock, timelineController: MockRoomTimelineController(), - isPinnedEventsTimeline: false, mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), diff --git a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift index 8de92a70ef..cdd2dc69d3 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift @@ -52,7 +52,6 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { init(roomProxy: JoinedRoomProxyProtocol, focussedEventID: String? = nil, timelineController: RoomTimelineControllerProtocol, - isPinnedEventsTimeline: Bool, mediaProvider: MediaProviderProtocol, mediaPlayerProvider: MediaPlayerProviderProtocol, voiceMessageMediaManager: VoiceMessageMediaManagerProtocol, @@ -81,7 +80,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { appSettings: appSettings, analyticsService: analyticsService) - super.init(initialViewState: TimelineViewState(isPinnedEventsTimeline: isPinnedEventsTimeline, + super.init(initialViewState: TimelineViewState(isPinnedEventsTimeline: timelineController.timelineKind == .pinned, roomID: roomProxy.id, isEncryptedOneToOneRoom: roomProxy.isEncryptedOneToOneRoom, 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")), focussedEventID: nil, timelineController: MockRoomTimelineController(), - isPinnedEventsTimeline: false, mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -838,8 +836,7 @@ extension TimelineViewModel { static let pinnedEventsTimelineMock = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Preview room")), focussedEventID: nil, - timelineController: MockRoomTimelineController(), - isPinnedEventsTimeline: true, + timelineController: MockRoomTimelineController(timelineKind: .pinned), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), diff --git a/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptsSummaryView.swift b/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptsSummaryView.swift index 4eda9a4d0f..ceb018162e 100644 --- a/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptsSummaryView.swift +++ b/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptsSummaryView.swift @@ -54,7 +54,6 @@ struct ReadReceiptsSummaryView_Previews: PreviewProvider, TestablePreview { let roomProxyMock = JoinedRoomProxyMock(.init(name: "Room", members: members)) let mock = TimelineViewModel(roomProxy: roomProxyMock, timelineController: MockRoomTimelineController(), - isPinnedEventsTimeline: false, mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), diff --git a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReadReceiptsView.swift b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReadReceiptsView.swift index eb35840f8a..f6a842dd24 100644 --- a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReadReceiptsView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReadReceiptsView.swift @@ -92,7 +92,6 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview { static let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Test", members: members)), timelineController: MockRoomTimelineController(), - isPinnedEventsTimeline: false, mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/HighlightedTimelineItemModifier.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/HighlightedTimelineItemModifier.swift index 889bc89cab..321a1df9de 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/HighlightedTimelineItemModifier.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/HighlightedTimelineItemModifier.swift @@ -99,7 +99,6 @@ struct HighlightedTimelineItemTimeline_Previews: PreviewProvider { static let timelineViewModel = TimelineViewModel(roomProxy: roomProxyMock, focussedEventID: focussedEventID, timelineController: MockRoomTimelineController(), - isPinnedEventsTimeline: false, mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineView.swift index 169700b8b6..574c18f1fe 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineView.swift @@ -84,7 +84,6 @@ struct TimelineView_Previews: PreviewProvider, TestablePreview { static let roomViewModel = RoomScreenViewModel.mock(roomProxyMock: roomProxyMock) static let timelineViewModel = TimelineViewModel(roomProxy: roomProxyMock, timelineController: MockRoomTimelineController(), - isPinnedEventsTimeline: false, mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), diff --git a/ElementX/Sources/Services/Room/JoinedRoomProxy.swift b/ElementX/Sources/Services/Room/JoinedRoomProxy.swift index 2a6902256e..fc124d0e23 100644 --- a/ElementX/Sources/Services/Room/JoinedRoomProxy.swift +++ b/ElementX/Sources/Services/Room/JoinedRoomProxy.swift @@ -44,7 +44,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { } 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() innerPinnedEventsTimeline = timeline return timeline @@ -172,7 +172,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { self.roomListItem = roomListItem self.room = room - timeline = try await TimelineProxy(timeline: room.timeline(), isLive: true) + timeline = try await TimelineProxy(timeline: room.timeline(), kind: .live) Task { await updateMembers() @@ -216,7 +216,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { func timelineFocusedOnEvent(eventID: String, numberOfEvents: UInt16) async -> Result { do { 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 { switch error { case .InvalidEventId(_, let error): diff --git a/ElementX/Sources/Services/Timeline/RoomTimelineProvider.swift b/ElementX/Sources/Services/Timeline/RoomTimelineProvider.swift index 03fa54ab6b..2ecc248b0a 100644 --- a/ElementX/Sources/Services/Timeline/RoomTimelineProvider.swift +++ b/ElementX/Sources/Services/Timeline/RoomTimelineProvider.swift @@ -42,7 +42,7 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol { .eraseToAnyPublisher() } - private(set) var isLive: Bool + let kind: TimelineKind private let membershipChangeSubject = PassthroughSubject() var membershipChangePublisher: AnyPublisher { @@ -54,10 +54,10 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol { roomTimelineObservationToken?.cancel() } - init(timeline: Timeline, isLive: Bool, paginationStatePublisher: AnyPublisher) { + init(timeline: Timeline, kind: TimelineKind, paginationStatePublisher: AnyPublisher) { serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomtimelineprovider", qos: .utility) itemProxiesSubject = CurrentValueSubject<[TimelineItemProxy], Never>([]) - self.isLive = isLive + self.kind = kind paginationStatePublisher .sink { [weak self] in diff --git a/ElementX/Sources/Services/Timeline/RoomTimelineProviderProtocol.swift b/ElementX/Sources/Services/Timeline/RoomTimelineProviderProtocol.swift index ee84e7fb8a..b01fd5df6c 100644 --- a/ElementX/Sources/Services/Timeline/RoomTimelineProviderProtocol.swift +++ b/ElementX/Sources/Services/Timeline/RoomTimelineProviderProtocol.swift @@ -45,8 +45,8 @@ protocol RoomTimelineProviderProtocol { var itemProxies: [TimelineItemProxy] { get } /// Whether the timeline is back/forward paginating or not (or has reached the start/end of the room). var paginationState: PaginationState { get } - /// Whether or not the provider is for a live timeline. - var isLive: Bool { get } + /// The kind of the timeline + var kind: TimelineKind { get } /// 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. diff --git a/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift index ccaa180f7f..133c15da2a 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift @@ -28,6 +28,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol { var roomProxy: JoinedRoomProxyProtocol? var roomID: String { roomProxy?.id ?? "MockRoomIdentifier" } + var timelineKind: TimelineKind let callbacks = PassthroughSubject() @@ -36,7 +37,8 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol { 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(.isLive(true)) diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift index 4c71f242da..bebbf93481 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift @@ -25,7 +25,6 @@ class RoomTimelineController: RoomTimelineControllerProtocol { private let timelineItemFactory: RoomTimelineItemFactoryProtocol private let appSettings: AppSettings private let serialDispatchQueue: DispatchQueue - private let shouldHideStart: Bool let callbacks = PassthroughSubject() @@ -42,17 +41,19 @@ class RoomTimelineController: RoomTimelineControllerProtocol { roomProxy.id } + var timelineKind: TimelineKind { + liveTimelineProvider.kind + } + init(roomProxy: JoinedRoomProxyProtocol, timelineProxy: TimelineProxyProtocol, initialFocussedEventID: String?, - shouldHideStart: Bool, timelineItemFactory: RoomTimelineItemFactoryProtocol, appSettings: AppSettings) { self.roomProxy = roomProxy liveTimelineProvider = timelineProxy.timelineProvider self.timelineItemFactory = timelineItemFactory self.appSettings = appSettings - self.shouldHideStart = shouldHideStart serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomtimelineprovider", qos: .utility) activeTimeline = timelineProxy @@ -312,7 +313,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol { // Inform the world that the initial items are loading from the store callbacks.send(.paginationState(.init(backward: .paginating, forward: .paginating))) - callbacks.send(.isLive(activeTimelineProvider.isLive)) + callbacks.send(.isLive(activeTimelineProvider.kind == .live)) updateTimelineItemsCancellable = activeTimelineProvider .updatePublisher @@ -367,7 +368,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol { // Check if we need to add anything to the top of the timeline. switch paginationState.backward { case .timelineEndReached: - if !shouldHideStart, !roomProxy.isEncryptedOneToOneRoom { + if timelineKind != .pinned, !roomProxy.isEncryptedOneToOneRoom { let timelineStart = TimelineStartRoomTimelineItem(name: roomProxy.name) newTimelineItems.insert(timelineStart, at: 0) } diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactory.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactory.swift index a2d100077d..456326001b 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactory.swift @@ -23,7 +23,6 @@ struct RoomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol { RoomTimelineController(roomProxy: roomProxy, timelineProxy: roomProxy.timeline, initialFocussedEventID: initialFocussedEventID, - shouldHideStart: false, timelineItemFactory: timelineItemFactory, appSettings: ServiceLocator.shared.settings) } @@ -36,7 +35,6 @@ struct RoomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol { return RoomTimelineController(roomProxy: roomProxy, timelineProxy: pinnedEventsTimeline, initialFocussedEventID: nil, - shouldHideStart: true, timelineItemFactory: timelineItemFactory, appSettings: ServiceLocator.shared.settings) } diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift index 832b7e539e..6faf11128c 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift @@ -38,6 +38,7 @@ enum RoomTimelineControllerError: Error { @MainActor protocol RoomTimelineControllerProtocol { var roomID: String { get } + var timelineKind: TimelineKind { get } var timelineItems: [RoomTimelineItemProtocol] { get } var callbacks: PassthroughSubject { get } diff --git a/ElementX/Sources/Services/Timeline/TimelineProxy.swift b/ElementX/Sources/Services/Timeline/TimelineProxy.swift index 38c106f623..9ee588edc0 100644 --- a/ElementX/Sources/Services/Timeline/TimelineProxy.swift +++ b/ElementX/Sources/Services/Timeline/TimelineProxy.swift @@ -27,7 +27,7 @@ final class TimelineProxy: TimelineProxyProtocol { private let backPaginationStatusSubject = CurrentValueSubject(.timelineEndReached) private let forwardPaginationStatusSubject = CurrentValueSubject(.timelineEndReached) - let isLive: Bool + private let kind: TimelineKind private var innerTimelineProvider: RoomTimelineProviderProtocol! var timelineProvider: RoomTimelineProviderProtocol { @@ -38,9 +38,9 @@ final class TimelineProxy: TimelineProxyProtocol { backPaginationStatusObservationToken?.cancel() } - init(timeline: Timeline, isLive: Bool) { + init(timeline: Timeline, kind: TimelineKind) { self.timeline = timeline - self.isLive = isLive + self.kind = kind } func subscribeForUpdates() async { @@ -56,7 +56,7 @@ final class TimelineProxy: TimelineProxyProtocol { 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 // determining whether or not the timeline should paginate to load more items. 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 // 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. - await if isLive { - paginateBackwardsOnLive(requestSize: requestSize) - } else { - focussedPaginate(.backwards, requestSize: requestSize) + switch kind { + case .live: + return await paginateBackwardsOnLive(requestSize: requestSize) + case .detached: + return await focussedPaginate(.backwards, requestSize: requestSize) + case .pinned: + return .success(()) } } func paginateForwards(requestSize: UInt16) async -> Result { - 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. @@ -538,7 +544,8 @@ final class TimelineProxy: TimelineProxyProtocol { } private func subscribeToPagination() async { - if isLive { + switch kind { + case .live: let backPaginationListener = RoomPaginationStatusListener { [weak self] status in guard let self else { return @@ -557,13 +564,15 @@ final class TimelineProxy: TimelineProxyProtocol { } catch { 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. 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) } } diff --git a/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift index 511c494099..8c98bb3e6f 100644 --- a/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift @@ -18,6 +18,12 @@ import Combine import Foundation import MatrixRustSDK +enum TimelineKind { + case live + case detached + case pinned +} + enum TimelineProxyError: Error { case sdkError(Error) diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index ec808c5c58..596d78df9b 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -649,7 +649,6 @@ class MockScreen: Identifiable { let timelineController = RoomTimelineController(roomProxy: roomProxy, timelineProxy: roomProxy.timeline, initialFocussedEventID: nil, - shouldHideStart: false, timelineItemFactory: RoomTimelineItemFactory(userID: "@alice:matrix.org", attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), stateEventStringBuilder: RoomStateEventStringBuilder(userID: "@alice:matrix.org")), diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsBannerView-iPhone-15-pseudo.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsBannerView-iPhone-15-pseudo.1.png index fcd8c4da61..707b7de805 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsBannerView-iPhone-15-pseudo.1.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsBannerView-iPhone-15-pseudo.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:919e3df86ce263d06f479c701b6981a66b0d6217e13cf3cff12445b9c6a24d77 -size 106938 +oid sha256:6ada9407d2f6c9da3cedb750353ffcf6e4cc6493177b3e2796d66b8245982042 +size 107677 diff --git a/UnitTests/Sources/PillContextTests.swift b/UnitTests/Sources/PillContextTests.swift index 7c72ea1ffc..c5956ef0a8 100644 --- a/UnitTests/Sources/PillContextTests.swift +++ b/UnitTests/Sources/PillContextTests.swift @@ -28,7 +28,6 @@ class PillContextTests: XCTestCase { proxyMock.membersPublisher = subject.asCurrentValuePublisher() let mock = TimelineViewModel(roomProxy: proxyMock, timelineController: MockRoomTimelineController(), - isPinnedEventsTimeline: false, mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -57,7 +56,6 @@ class PillContextTests: XCTestCase { proxyMock.membersPublisher = subject.asCurrentValuePublisher() let mock = TimelineViewModel(roomProxy: proxyMock, timelineController: MockRoomTimelineController(), - isPinnedEventsTimeline: false, mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -79,7 +77,6 @@ class PillContextTests: XCTestCase { mockController.roomProxy = proxyMock let mock = TimelineViewModel(roomProxy: proxyMock, timelineController: mockController, - isPinnedEventsTimeline: false, mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), diff --git a/UnitTests/Sources/TimelineViewModelTests.swift b/UnitTests/Sources/TimelineViewModelTests.swift index b4674c1e58..64a55ae0fb 100644 --- a/UnitTests/Sources/TimelineViewModelTests.swift +++ b/UnitTests/Sources/TimelineViewModelTests.swift @@ -345,7 +345,6 @@ class TimelineViewModelTests: XCTestCase { let viewModel = TimelineViewModel(roomProxy: roomProxy, timelineController: timelineController, - isPinnedEventsTimeline: false, mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -370,7 +369,6 @@ class TimelineViewModelTests: XCTestCase { timelineController.timelineItems = [message] let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "", members: [RoomMemberProxyMock.mockAlice, RoomMemberProxyMock.mockCharlie])), timelineController: timelineController, - isPinnedEventsTimeline: false, mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -396,7 +394,6 @@ class TimelineViewModelTests: XCTestCase { roomProxyMock.underlyingActionsPublisher = actionsSubject.eraseToAnyPublisher() let viewModel = TimelineViewModel(roomProxy: roomProxyMock, timelineController: MockRoomTimelineController(), - isPinnedEventsTimeline: false, mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -425,7 +422,6 @@ class TimelineViewModelTests: XCTestCase { roomProxyMock.underlyingActionsPublisher = actionsSubject.eraseToAnyPublisher() let viewModel = TimelineViewModel(roomProxy: roomProxyMock, timelineController: MockRoomTimelineController(), - isPinnedEventsTimeline: false, mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -455,7 +451,6 @@ class TimelineViewModelTests: XCTestCase { TimelineViewModel(roomProxy: roomProxy ?? JoinedRoomProxyMock(.init(name: "")), focussedEventID: focussedEventID, timelineController: timelineController, - isPinnedEventsTimeline: false, mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(),