From ed892fee94806f5ae9485790d85007997902e331 Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Thu, 22 Jan 2026 12:37:34 +0000 Subject: [PATCH] Add the empty state to SpaceScreen. (#4985) * Rename PaginationState.timelineEndReached to PaginationState.endReached. * Rename PaginationState to TimelinePaginationState. Also renames PaginationStatus to PaginationState so that a TimelinePaginationState consists of the forward and backward pagination states. * Add the empty state to SpaceScreen. Only has 1 of the 2 buttons for now. --- ElementX.xcodeproj/project.pbxproj | 4 ++ .../en-US.lproj/Localizable.strings | 1 + .../en.lproj/Localizable.strings | 1 + ElementX/Sources/Generated/Strings.swift | 2 + .../Mocks/Generated/GeneratedMocks.swift | 8 ++-- .../Sources/Mocks/TimelineProviderMock.swift | 2 +- .../Sources/Mocks/TimelineProxyMock.swift | 2 +- ElementX/Sources/Other/Pagination.swift | 18 +++++++ .../TimelineMediaPreviewDataSource.swift | 8 ++-- .../MediaEventsTimelineScreenViewModel.swift | 10 ++-- .../RoomPollsHistoryScreenViewModel.swift | 2 +- .../SpaceScreen/SpaceScreenModels.swift | 6 ++- .../SpaceScreen/SpaceScreenViewModel.swift | 11 +++-- .../Spaces/SpaceScreen/View/SpaceScreen.swift | 47 ++++++++++++++++--- .../Screens/Timeline/TimelineModels.swift | 2 +- .../TimelineTableViewController.swift | 2 +- .../Screens/Timeline/TimelineViewModel.swift | 2 +- .../MockTimelineController.swift | 10 ++-- .../TimelineController.swift | 12 ++--- .../TimelineControllerProtocol.swift | 4 +- .../Timeline/TimelineItemProvider.swift | 8 ++-- .../TimelineItemProviderProtocol.swift | 22 +++------ .../Services/Timeline/TimelineProxy.swift | 40 ++++++++-------- .../spaceScreen.New-Space-iPad-en-GB.png | 3 ++ .../spaceScreen.New-Space-iPad-pseudo.png | 3 ++ .../spaceScreen.New-Space-iPhone-en-GB.png | 3 ++ .../spaceScreen.New-Space-iPhone-pseudo.png | 3 ++ .../Sources/RoomScreenViewModelTests.swift | 6 +-- .../Sources/SpaceScreenViewModelTests.swift | 14 +++--- .../TimelineMediaPreviewViewModelTests.swift | 2 +- 30 files changed, 164 insertions(+), 94 deletions(-) create mode 100644 ElementX/Sources/Other/Pagination.swift create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/spaceScreen.New-Space-iPad-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/spaceScreen.New-Space-iPad-pseudo.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/spaceScreen.New-Space-iPhone-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/spaceScreen.New-Space-iPhone-pseudo.png diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index bc1a7ed5c..8bb3326a6 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -1155,6 +1155,7 @@ C8E1E4E06B7C7A3A8246FC9B /* MediaEventsTimelineScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8512B82404B1751D0BCC82D2 /* MediaEventsTimelineScreenCoordinator.swift */; }; C900127318820AD04D6C90B8 /* LabsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E43D8784B0054C048060FEB /* LabsScreenModels.swift */; }; C915347779B3C7FDD073A87A /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93E1FF0DFBB3768F79FDBF6D /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift */; }; + C960BACE42A9D8C535E8CB34 /* Pagination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1912062B53CE95E6F700DA60 /* Pagination.swift */; }; C969A62F3D9F14318481A33B /* KnockedRoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 858DA81F2ACF484B7CAD6AE4 /* KnockedRoomProxy.swift */; }; C97325EFDCCEE457432A9E82 /* MessageText.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E0B4A34E69BD2132BEC521 /* MessageText.swift */; }; C9A631FD968249B4BA0B7B3C /* ReactionsSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EE0FABA8ED6D6C1D6CE71D /* ReactionsSummaryView.swift */; }; @@ -1692,6 +1693,7 @@ 18B223FA339BF53085328DEE /* SpaceScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceScreenViewModelTests.swift; sourceTree = ""; }; 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxy.swift; sourceTree = ""; }; 190EC7285D3CFEF0D3011BCF /* GeoURI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoURI.swift; sourceTree = ""; }; + 1912062B53CE95E6F700DA60 /* Pagination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pagination.swift; sourceTree = ""; }; 196004E7695FBA292A7944AF /* ScreenTrackerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenTrackerViewModifier.swift; sourceTree = ""; }; 19DD166C3625EE426203FA29 /* AppLockSetupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupTests.swift; sourceTree = ""; }; 1A02406480C351B8C6E0682C /* MediaLoaderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoaderProtocol.swift; sourceTree = ""; }; @@ -6014,6 +6016,7 @@ 7B25F959A434BB9923A3223F /* ExpiringTaskRunner.swift */, 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */, 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */, + 1912062B53CE95E6F700DA60 /* Pagination.swift */, 53482ECA4B6633961EC224F5 /* ScrollViewAdapter.swift */, FE5CD2993048222B64C45006 /* SDKListener.swift */, 4481799F455B3DA243BDA2AC /* ShareToMapsAppActivity.swift */, @@ -8338,6 +8341,7 @@ 6F86349BDEAF4495EAE38931 /* PHGPostHogMock.swift in Sources */, F7D709D7ECABE46641BB8B6B /* PHGPostHogProtocol.swift in Sources */, 847DE3A7EB9FCA2C429C6E85 /* PINTextField.swift in Sources */, + C960BACE42A9D8C535E8CB34 /* Pagination.swift in Sources */, 7501442D52A65F73DF79FFD4 /* PaginationIndicatorRoomTimelineItem.swift in Sources */, BC7CA1379D7C24F47B1B8B7E /* PaginationIndicatorRoomTimelineView.swift in Sources */, EA2FECCD9E00D9784AC6017D /* PhishingDetector.swift in Sources */, diff --git a/ElementX/Resources/Localizations/en-US.lproj/Localizable.strings b/ElementX/Resources/Localizations/en-US.lproj/Localizable.strings index 4d1fe2850..89de5a15f 100644 --- a/ElementX/Resources/Localizations/en-US.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en-US.lproj/Localizable.strings @@ -723,6 +723,7 @@ "screen_security_and_privacy_room_visibility_section_footer" = "Addresses are a way to find and access rooms and spaces. This also ensures you can easily share them with others."; "screen_security_and_privacy_room_visibility_section_header" = "Visibility"; "screen_security_and_privacy_title" = "Security & privacy"; +"screen_space_empty_state_title" = "Add your first room"; "screen_space_menu_action_members" = "View members"; "screen_space_remove_rooms_confirmation_content" = "Removing a room will not affect the room access. To change the access go to Room info > Privacy & security."; "screen_space_remove_rooms_confirmation_title_ios" = "Remove rooms from %1$@?"; diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 1018e8e54..78b106ba4 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -723,6 +723,7 @@ "screen_security_and_privacy_room_visibility_section_footer" = "Addresses are a way to find and access rooms and spaces. This also ensures you can easily share them with others."; "screen_security_and_privacy_room_visibility_section_header" = "Visibility"; "screen_security_and_privacy_title" = "Security & privacy"; +"screen_space_empty_state_title" = "Add your first room"; "screen_space_menu_action_members" = "View members"; "screen_space_remove_rooms_confirmation_content" = "Removing a room will not affect the room access. To change the access go to Room info > Privacy & security."; "screen_space_remove_rooms_confirmation_title_ios" = "Remove rooms from %1$@?"; diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index a631b9e98..cc5d4e524 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -3159,6 +3159,8 @@ internal enum L10n { internal static var screenSpaceAnnouncementSubtitle: String { return L10n.tr("Localizable", "screen_space_announcement_subtitle") } /// Introducing Spaces internal static var screenSpaceAnnouncementTitle: String { return L10n.tr("Localizable", "screen_space_announcement_title") } + /// Add your first room + internal static var screenSpaceEmptyStateTitle: String { return L10n.tr("Localizable", "screen_space_empty_state_title") } /// Spaces you have created or joined. internal static var screenSpaceListDescription: String { return L10n.tr("Localizable", "screen_space_list_description") } /// %1$@ • %2$@ diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 5ea9b8946..5361b683a 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -17622,17 +17622,17 @@ class TimelineControllerFactoryMock: TimelineControllerFactoryProtocol, @uncheck } } class TimelineItemProviderMock: TimelineItemProviderProtocol, @unchecked Sendable { - var updatePublisher: AnyPublisher<([TimelineItemProxy], PaginationState), Never> { + var updatePublisher: AnyPublisher<([TimelineItemProxy], TimelinePaginationState), Never> { get { return underlyingUpdatePublisher } set(value) { underlyingUpdatePublisher = value } } - var underlyingUpdatePublisher: AnyPublisher<([TimelineItemProxy], PaginationState), Never>! + var underlyingUpdatePublisher: AnyPublisher<([TimelineItemProxy], TimelinePaginationState), Never>! var itemProxies: [TimelineItemProxy] = [] - var paginationState: PaginationState { + var paginationState: TimelinePaginationState { get { return underlyingPaginationState } set(value) { underlyingPaginationState = value } } - var underlyingPaginationState: PaginationState! + var underlyingPaginationState: TimelinePaginationState! var kind: TimelineKind { get { return underlyingKind } set(value) { underlyingKind = value } diff --git a/ElementX/Sources/Mocks/TimelineProviderMock.swift b/ElementX/Sources/Mocks/TimelineProviderMock.swift index ff0844f32..7748df64d 100644 --- a/ElementX/Sources/Mocks/TimelineProviderMock.swift +++ b/ElementX/Sources/Mocks/TimelineProviderMock.swift @@ -15,7 +15,7 @@ import MatrixRustSDKMocks class AutoUpdatingTimelineItemProviderMock: TimelineItemProvider { static var timelineListener: TimelineListener? - private let innerPaginationStatePublisher: PassthroughSubject + private let innerPaginationStatePublisher: PassthroughSubject init() { innerPaginationStatePublisher = .init() diff --git a/ElementX/Sources/Mocks/TimelineProxyMock.swift b/ElementX/Sources/Mocks/TimelineProxyMock.swift index 549014952..433099afc 100644 --- a/ElementX/Sources/Mocks/TimelineProxyMock.swift +++ b/ElementX/Sources/Mocks/TimelineProxyMock.swift @@ -30,7 +30,7 @@ extension TimelineProxyMock { underlyingTimelineItemProvider = AutoUpdatingTimelineItemProviderMock() } else { let timelineItemProvider = TimelineItemProviderMock() - timelineItemProvider.paginationState = .init(backward: configuration.timelineStartReached ? .timelineEndReached : .idle, forward: .timelineEndReached) + timelineItemProvider.paginationState = .init(backward: configuration.timelineStartReached ? .endReached : .idle, forward: .endReached) timelineItemProvider.underlyingMembershipChangePublisher = PassthroughSubject().eraseToAnyPublisher() underlyingTimelineItemProvider = timelineItemProvider } diff --git a/ElementX/Sources/Other/Pagination.swift b/ElementX/Sources/Other/Pagination.swift new file mode 100644 index 000000000..d8ef2e20e --- /dev/null +++ b/ElementX/Sources/Other/Pagination.swift @@ -0,0 +1,18 @@ +// +// Copyright 2026 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import Foundation + +enum PaginationDirection: String { + case backwards, forwards +} + +enum PaginationState { + case idle + case endReached + case paginating +} diff --git a/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewDataSource.swift b/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewDataSource.swift index a6a3bf6d0..e626251c5 100644 --- a/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewDataSource.swift +++ b/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewDataSource.swift @@ -33,12 +33,12 @@ class TimelineMediaPreviewDataSource: NSObject, QLPreviewControllerDataSource { private var backwardPadding: Int private var forwardPadding: Int - var paginationState: PaginationState + var paginationState: TimelinePaginationState init(itemViewStates: [RoomTimelineItemViewState], initialItem: EventBasedMessageTimelineItemProtocol, initialPadding: Int = 100, - paginationState: PaginationState) { + paginationState: TimelinePaginationState) { previewItems = itemViewStates.compactMap(TimelineMediaPreviewItem.Media.init) self.initialItem = initialItem @@ -125,9 +125,9 @@ class TimelineMediaPreviewDataSource: NSObject, QLPreviewControllerDataSource { let arrayIndex = index - backwardPadding if index < firstPreviewItemIndex { - return paginationState.backward == .timelineEndReached ? TimelineMediaPreviewItem.Loading.timelineStart : .paginatingBackwards + return paginationState.backward == .endReached ? TimelineMediaPreviewItem.Loading.timelineStart : .paginatingBackwards } else if index > lastPreviewItemIndex { - return paginationState.forward == .timelineEndReached ? TimelineMediaPreviewItem.Loading.timelineEnd : .paginatingForwards + return paginationState.forward == .endReached ? TimelineMediaPreviewItem.Loading.timelineEnd : .paginatingForwards } else { return previewItems[arrayIndex] } diff --git a/ElementX/Sources/Screens/MediaEventsTimelineScreen/MediaEventsTimelineScreenViewModel.swift b/ElementX/Sources/Screens/MediaEventsTimelineScreen/MediaEventsTimelineScreenViewModel.swift index e576700bd..33a91676d 100644 --- a/ElementX/Sources/Screens/MediaEventsTimelineScreen/MediaEventsTimelineScreenViewModel.swift +++ b/ElementX/Sources/Screens/MediaEventsTimelineScreen/MediaEventsTimelineScreenViewModel.swift @@ -123,7 +123,7 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType updateWithTimelineViewState(activeTimelineViewModel.context.viewState) case .oldestItemDidAppear: isOldestItemVisible = true - backPaginateIfNecessary(paginationStatus: activeTimelineViewModel.context.viewState.timelineState.paginationState.backward) + backPaginateIfNecessary(backPaginationState: activeTimelineViewModel.context.viewState.timelineState.paginationState.backward) case .oldestItemDidDisappear: isOldestItemVisible = false case .tappedItem(let item): @@ -205,12 +205,12 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType state.groups = newGroups state.isBackPaginating = timelineViewState.timelineState.paginationState.backward == .paginating - state.shouldShowEmptyState = newGroups.isEmpty && timelineViewState.timelineState.paginationState.backward == .timelineEndReached - backPaginateIfNecessary(paginationStatus: timelineViewState.timelineState.paginationState.backward) + state.shouldShowEmptyState = newGroups.isEmpty && timelineViewState.timelineState.paginationState.backward == .endReached + backPaginateIfNecessary(backPaginationState: timelineViewState.timelineState.paginationState.backward) } - private func backPaginateIfNecessary(paginationStatus: PaginationStatus) { - if paginationStatus == .idle, isOldestItemVisible { + private func backPaginateIfNecessary(backPaginationState: PaginationState) { + if backPaginationState == .idle, isOldestItemVisible { activeTimelineViewModel.context.send(viewAction: .paginateBackwards) } } diff --git a/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenViewModel.swift b/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenViewModel.swift index 9bfa8a3f7..3e6a1209d 100644 --- a/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenViewModel.swift @@ -75,7 +75,7 @@ class RoomPollsHistoryScreenViewModel: RoomPollsHistoryScreenViewModelType, Room case .updatedTimelineItems: self.updatePollsList(filter: state.bindings.filter) case .paginationState(let paginationState): - let canBackPaginate = paginationState.backward != .timelineEndReached + let canBackPaginate = paginationState.backward != .endReached if self.state.canBackPaginate != canBackPaginate { self.state.canBackPaginate = canBackPaginate } diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift index 92bcb5356..69fff17aa 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift @@ -25,7 +25,7 @@ struct SpaceScreenViewState: BindableState { var permalink: URL? var roomProxy: JoinedRoomProxyProtocol? - var isPaginating = false + var paginationState: PaginationState = .idle var rooms: [SpaceServiceRoomProtocol] var selectedSpaceRoomID: String? var joiningRoomIDs: Set = [] @@ -40,6 +40,10 @@ struct SpaceScreenViewState: BindableState { var bindings = SpaceScreenViewStateBindings() + var shouldShowEmptyState: Bool { + rooms.isEmpty && paginationState == .endReached && canEditChildren + } + var visibleRooms: [SpaceServiceRoomProtocol] { if editMode == .inactive { rooms diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift index 691a2fa86..be96271a6 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift @@ -57,13 +57,16 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc spaceRoomListProxy.paginationStatePublisher .receive(on: DispatchQueue.main) .sink { [weak self] paginationState in + guard let self else { return } + switch paginationState { - case .idle(let endReached): - self?.state.isPaginating = false - guard !endReached else { return } + case .idle(endReached: false): + state.paginationState = .idle Task { await spaceRoomListProxy.paginate() } + case .idle(endReached: true): + state.paginationState = .endReached case .loading: - self?.state.isPaginating = true + state.paginationState = .paginating } } .store(in: &cancellables) diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift index edd5210dc..390c6c827 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift @@ -22,7 +22,11 @@ struct SpaceScreen: View { mediaProvider: context.mediaProvider) } - rooms + if context.viewState.shouldShowEmptyState { + emptyState + } else { + rooms + } } } .environment(\.editMode, .constant(context.viewState.editMode)) @@ -53,12 +57,32 @@ struct SpaceScreen: View { } } - if context.viewState.isPaginating { + if context.viewState.paginationState == .paginating { ProgressView() .padding() } } + var emptyState: some View { + VStack(spacing: 24) { + TitleAndIcon(title: L10n.screenSpaceEmptyStateTitle, + icon: \.room, + iconStyle: .defaultSolid) + .padding(.horizontal, 24) + + VStack(spacing: 16) { + Button { context.send(viewAction: .addExistingRooms) } label: { + Label(L10n.actionAddExistingRooms, icon: \.plus) + } + .buttonStyle(.compound(.primary)) + + // @Velin92 Create Room button goes here. + } + .padding(.horizontal, 16) + } + .padding(.top, 40) + } + @ToolbarContentBuilder var toolbar: some ToolbarContent { if isEditModeActive { @@ -150,6 +174,7 @@ struct SpaceScreen: View { struct SpaceScreen_Previews: PreviewProvider, TestablePreview { static let viewModel = makeViewModel() static let managingViewModel = makeViewModel(isManagingRooms: true) + static let newSpaceViewModel = makeViewModel(isNewSpace: true) static var previews: some View { NavigationStack { @@ -160,9 +185,19 @@ struct SpaceScreen_Previews: PreviewProvider, TestablePreview { SpaceScreen(context: managingViewModel.context) } .previewDisplayName("Managing") + + NavigationStack { + SpaceScreen(context: newSpaceViewModel.context) + } + .previewDisplayName("New Space") + .snapshotPreferences(expect: newSpaceViewModel.context.observe(\.viewState.shouldShowEmptyState)) } - static func makeViewModel(isManagingRooms: Bool = false) -> SpaceScreenViewModel { + static func makeViewModel(isManagingRooms: Bool = false, isNewSpace: Bool = false) -> SpaceScreenViewModel { + let appSettings = AppSettings() + appSettings.spaceSettingsEnabled = true + appSettings.createSpaceEnabled = true + let spaceServiceRoom = SpaceServiceRoomMock(.init(id: "!eng-space:matrix.org", name: "Engineering Team", isSpace: true, @@ -173,11 +208,11 @@ struct SpaceScreen_Previews: PreviewProvider, TestablePreview { canonicalAlias: "#engineering-team:element.io", joinRule: .knockRestricted(rules: [.roomMembership(roomId: "")]))) let spaceRoomListProxy = SpaceRoomListProxyMock(.init(spaceServiceRoom: spaceServiceRoom, - initialSpaceRooms: .mockSpaceList)) + initialSpaceRooms: isNewSpace ? [] : .mockSpaceList)) let clientProxy = ClientProxyMock(.init()) clientProxy.roomForIdentifierClosure = { _ in - .joined(JoinedRoomProxyMock(.init())) + .joined(JoinedRoomProxyMock(.init(members: .allMembersAsAdmin))) } let userSession = UserSessionMock(.init(clientProxy: clientProxy)) @@ -185,7 +220,7 @@ struct SpaceScreen_Previews: PreviewProvider, TestablePreview { spaceServiceProxy: SpaceServiceProxyMock(.init()), selectedSpaceRoomPublisher: .init(nil), userSession: userSession, - appSettings: AppSettings(), + appSettings: appSettings, userIndicatorController: UserIndicatorControllerMock()) if isManagingRooms { diff --git a/ElementX/Sources/Screens/Timeline/TimelineModels.swift b/ElementX/Sources/Screens/Timeline/TimelineModels.swift index 0ab827c67..3926ac886 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineModels.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineModels.swift @@ -210,7 +210,7 @@ struct RoomMemberState { /// Is also nice to have this as a wrapper for any state that is directly connected to the timeline. struct TimelineState { var isLive = true - var paginationState = PaginationState.initial + var paginationState = TimelinePaginationState.initial /// The room is in the process of loading items from a new timeline (switching to/from a detached timeline). var isSwitchingTimelines = false diff --git a/ElementX/Sources/Screens/Timeline/TimelineTableViewController.swift b/ElementX/Sources/Screens/Timeline/TimelineTableViewController.swift index 30b3369b4..e8fb6abf6 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineTableViewController.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineTableViewController.swift @@ -109,7 +109,7 @@ class TimelineTableViewController: UIViewController { } /// The state of pagination (in both directions) of the current timeline. - var paginationState: PaginationState = .initial { + var paginationState: TimelinePaginationState = .initial { didSet { // Paginate again if the threshold hasn't been satisfied. paginatePublisher.send(()) diff --git a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift index 47b0cb12e..adf96ba04 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift @@ -614,7 +614,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { break } - if state.timelineState.paginationState.forward == .timelineEndReached { + if state.timelineState.paginationState.forward == .endReached { focusLive() } diff --git a/ElementX/Sources/Services/Timeline/TimelineController/MockTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/MockTimelineController.swift index fc5daedb6..843e95b54 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/MockTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/MockTimelineController.swift @@ -26,7 +26,7 @@ class MockTimelineController: TimelineControllerProtocol { let callbacks = PassthroughSubject() - var paginationState: PaginationState = .initial { + var paginationState: TimelinePaginationState = .initial { didSet { callbacks.send(.paginationState(paginationState)) } @@ -45,7 +45,7 @@ class MockTimelineController: TimelineControllerProtocol { static var emptyMediaGallery: MockTimelineController { let mock = MockTimelineController(timelineKind: .media(.mediaFilesScreen)) - mock.paginationState = PaginationState(backward: .timelineEndReached, forward: .timelineEndReached) + mock.paginationState = TimelinePaginationState(backward: .endReached, forward: .endReached) return mock } @@ -86,7 +86,7 @@ class MockTimelineController: TimelineControllerProtocol { func paginateBackwards(requestSize: UInt16) async -> Result { paginateBackwardsCallCount += 1 - paginationState = PaginationState(backward: .paginating, forward: .timelineEndReached) + paginationState = TimelinePaginationState(backward: .paginating, forward: .endReached) if client == nil { try? await simulateBackPagination() @@ -331,8 +331,8 @@ class MockTimelineController: TimelineControllerProtocol { /// Prepends the next chunk of items to the `timelineItems` array. private func simulateBackPagination() async throws { defer { - paginationState = PaginationState(backward: backPaginationResponses.isEmpty ? .timelineEndReached : .idle, - forward: .timelineEndReached) + paginationState = TimelinePaginationState(backward: backPaginationResponses.isEmpty ? .endReached : .idle, + forward: .endReached) } guard !backPaginationResponses.isEmpty else { return } diff --git a/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift index 9f437f611..4fbc60a35 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift @@ -30,7 +30,7 @@ class TimelineController: TimelineControllerProtocol { private(set) var timelineItems = [RoomTimelineItemProtocol]() - private(set) var paginationState: PaginationState = .initial { + private(set) var paginationState: TimelinePaginationState = .initial { didSet { callbacks.send(.paginationState(paginationState)) } @@ -65,7 +65,7 @@ class TimelineController: TimelineControllerProtocol { } Task { - paginationState = PaginationState(backward: .paginating, forward: .paginating) + paginationState = TimelinePaginationState(backward: .paginating, forward: .paginating) switch await focusOnEvent(initialFocussedEventID, timelineSize: 100) { case .success: @@ -396,7 +396,7 @@ class TimelineController: TimelineControllerProtocol { isSwitchingTimelines = true // Inform the world that the initial items are loading from the store - paginationState = PaginationState(backward: .paginating, forward: .paginating) + paginationState = TimelinePaginationState(backward: .paginating, forward: .paginating) callbacks.send(.isLive(activeTimelineItemProvider.kind == .live)) updateTimelineItemsCancellable = Task { [weak self, activeTimelineItemProvider] in @@ -411,7 +411,7 @@ class TimelineController: TimelineControllerProtocol { }.asCancellable() } - private func updateTimelineItems(itemProxies: [TimelineItemProxy], paginationState: PaginationState) async { + private func updateTimelineItems(itemProxies: [TimelineItemProxy], paginationState: TimelinePaginationState) async { let isNewTimeline = isSwitchingTimelines isSwitchingTimelines = false @@ -462,14 +462,14 @@ class TimelineController: TimelineControllerProtocol { switch paginationState.backward { case .paginating: newTimelineItems.insert(PaginationIndicatorRoomTimelineItem(position: .start), at: 0) - case .idle, .timelineEndReached: + case .idle, .endReached: break } switch paginationState.forward { case .paginating: newTimelineItems.insert(PaginationIndicatorRoomTimelineItem(position: .end), at: newTimelineItems.count) - case .idle, .timelineEndReached: + case .idle, .endReached: break } diff --git a/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift index 94c9e903f..e7cb7762b 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift @@ -12,7 +12,7 @@ import SwiftUI enum TimelineControllerCallback { case updatedTimelineItems(timelineItems: [RoomTimelineItemProtocol], isSwitchingTimelines: Bool) - case paginationState(PaginationState) + case paginationState(TimelinePaginationState) case isLive(Bool) } @@ -49,7 +49,7 @@ protocol TimelineControllerProtocol { var timelineItems: [RoomTimelineItemProtocol] { get } /// The current pagination state, use only for setting up the intial state - var paginationState: PaginationState { get } + var paginationState: TimelinePaginationState { get } var callbacks: PassthroughSubject { get } diff --git a/ElementX/Sources/Services/Timeline/TimelineItemProvider.swift b/ElementX/Sources/Services/Timeline/TimelineItemProvider.swift index 587d41631..07ef423a8 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItemProvider.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItemProvider.swift @@ -16,8 +16,8 @@ class TimelineItemProvider: TimelineItemProviderProtocol { private var roomTimelineObservationToken: TaskHandle? - private let paginationStateSubject = CurrentValueSubject(.initial) - var paginationState: PaginationState { + private let paginationStateSubject = CurrentValueSubject(.initial) + var paginationState: TimelinePaginationState { paginationStateSubject.value } @@ -28,7 +28,7 @@ class TimelineItemProvider: TimelineItemProviderProtocol { } } - var updatePublisher: AnyPublisher<([TimelineItemProxy], PaginationState), Never> { + var updatePublisher: AnyPublisher<([TimelineItemProxy], TimelinePaginationState), Never> { itemProxiesSubject .combineLatest(paginationStateSubject) .eraseToAnyPublisher() @@ -46,7 +46,7 @@ class TimelineItemProvider: TimelineItemProviderProtocol { roomTimelineObservationToken?.cancel() } - init(timeline: Timeline, kind: TimelineKind, paginationStatePublisher: AnyPublisher) { + init(timeline: Timeline, kind: TimelineKind, paginationStatePublisher: AnyPublisher) { serialDispatchQueue = DispatchQueue(label: "io.element.elementx.timeline_item_provider", qos: .utility) itemProxiesSubject = CurrentValueSubject<[TimelineItemProxy], Never>([]) self.kind = kind diff --git a/ElementX/Sources/Services/Timeline/TimelineItemProviderProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineItemProviderProtocol.swift index ed7bff308..67359a8e2 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItemProviderProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItemProviderProtocol.swift @@ -10,22 +10,12 @@ import Combine import Foundation import MatrixRustSDK -enum PaginationDirection: String { - case backwards, forwards -} - -enum PaginationStatus { - case idle - case timelineEndReached - case paginating -} - -struct PaginationState: Equatable { +struct TimelinePaginationState: Equatable { /// An initial state that is used to prevent pagination whilst loading the timeline. /// Once the initial items are loaded the TimelineProxy will publish the correct value. - static var initial = PaginationState(backward: .timelineEndReached, forward: .timelineEndReached) - let backward: PaginationStatus - let forward: PaginationStatus + static var initial = TimelinePaginationState(backward: .endReached, forward: .endReached) + let backward: PaginationState + let forward: PaginationState } /// Entities implementing this protocol are responsible for processings diffs coming from the rust timeline @@ -34,11 +24,11 @@ struct PaginationState: Equatable { @MainActor protocol TimelineItemProviderProtocol { /// A publisher that signals when ``itemProxies`` or ``paginationState`` are changed. - var updatePublisher: AnyPublisher<([TimelineItemProxy], PaginationState), Never> { get } + var updatePublisher: AnyPublisher<([TimelineItemProxy], TimelinePaginationState), Never> { get } /// The current set of items in the timeline. 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 } + var paginationState: TimelinePaginationState { 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`. diff --git a/ElementX/Sources/Services/Timeline/TimelineProxy.swift b/ElementX/Sources/Services/Timeline/TimelineProxy.swift index 4ae6eec76..9104e7c88 100644 --- a/ElementX/Sources/Services/Timeline/TimelineProxy.swift +++ b/ElementX/Sources/Services/Timeline/TimelineProxy.swift @@ -13,15 +13,15 @@ import MatrixRustSDK final class TimelineProxy: TimelineProxyProtocol { private let timeline: Timeline - private var backPaginationStatusObservationToken: TaskHandle? + private var backPaginationStateObservationToken: TaskHandle? // The default values shouldn't matter here as they will be updated when calling subscribeToPagination // but empirically we randomly see the timeline start virtual item when we shouldn't. // We believe there's a race condition between the default values the status publisher // so we're going to default the backwards one to .idle. Worst case it's going to do // one extra back pagination. - private let backPaginationStatusSubject = CurrentValueSubject(.idle) - private let forwardPaginationStatusSubject = CurrentValueSubject(.timelineEndReached) + private let backPaginationStateSubject = CurrentValueSubject(.idle) + private let forwardPaginationStateSubject = CurrentValueSubject(.endReached) private let kind: TimelineKind @@ -31,7 +31,7 @@ final class TimelineProxy: TimelineProxyProtocol { } deinit { - backPaginationStatusObservationToken?.cancel() + backPaginationStateObservationToken?.cancel() } init(timeline: Timeline, kind: TimelineKind) { @@ -45,9 +45,9 @@ final class TimelineProxy: TimelineProxyProtocol { return } - let paginationStatePublisher = backPaginationStatusSubject - .combineLatest(forwardPaginationStatusSubject) - .map { PaginationState(backward: $0.0, forward: $0.1) } + let paginationStatePublisher = backPaginationStateSubject + .combineLatest(forwardPaginationStateSubject) + .map { TimelinePaginationState(backward: $0.0, forward: $0.1) } .eraseToAnyPublisher() await subscribeToPagination() @@ -131,8 +131,8 @@ final class TimelineProxy: TimelineProxyProtocol { /// Rust subscription isn't allowed on focussed/detached timelines. private func focussedPaginate(_ direction: PaginationDirection, requestSize: UInt16) async -> Result { let subject = switch direction { - case .backwards: backPaginationStatusSubject - case .forwards: forwardPaginationStatusSubject + case .backwards: backPaginationStateSubject + case .forwards: forwardPaginationStateSubject } // This extra check is necessary as detached timelines don't support subscribing to pagination status. @@ -150,7 +150,7 @@ final class TimelineProxy: TimelineProxyProtocol { } MXLog.info("Finished paginating \(direction.rawValue)") - subject.send(timelineEndReached ? .timelineEndReached : .idle) + subject.send(timelineEndReached ? .endReached : .idle) return .success(()) } catch { MXLog.error("Failed paginating \(direction.rawValue) with error: \(error)") @@ -592,28 +592,28 @@ final class TimelineProxy: TimelineProxyProtocol { switch status { case .idle(let hitStartOfTimeline): - backPaginationStatusSubject.send(hitStartOfTimeline ? .timelineEndReached : .idle) + backPaginationStateSubject.send(hitStartOfTimeline ? .endReached : .idle) case .paginating: - backPaginationStatusSubject.send(.paginating) + backPaginationStateSubject.send(.paginating) } } do { - backPaginationStatusObservationToken = try await timeline.subscribeToBackPaginationStatus(listener: backPaginationListener) + backPaginationStateObservationToken = try await timeline.subscribeToBackPaginationStatus(listener: backPaginationListener) } catch { MXLog.error("Failed to subscribe to back pagination status with error: \(error)") } - forwardPaginationStatusSubject.send(.timelineEndReached) + forwardPaginationStateSubject.send(.endReached) case .detached, .thread: // Detached timelines don't support observation, set the initial state ourself. - backPaginationStatusSubject.send(.idle) - forwardPaginationStatusSubject.send(.idle) + backPaginationStateSubject.send(.idle) + forwardPaginationStateSubject.send(.idle) case .media(let presentation): - backPaginationStatusSubject.send(presentation == .pinnedEventsScreen ? .timelineEndReached : .idle) - forwardPaginationStatusSubject.send(presentation == .roomScreenDetached ? .idle : .timelineEndReached) + backPaginationStateSubject.send(presentation == .pinnedEventsScreen ? .endReached : .idle) + forwardPaginationStateSubject.send(presentation == .roomScreenDetached ? .idle : .endReached) case .pinned: - backPaginationStatusSubject.send(.timelineEndReached) - forwardPaginationStatusSubject.send(.timelineEndReached) + backPaginationStateSubject.send(.endReached) + forwardPaginationStateSubject.send(.endReached) } } } diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceScreen.New-Space-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceScreen.New-Space-iPad-en-GB.png new file mode 100644 index 000000000..8bd038ca4 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceScreen.New-Space-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:65f117e0aa251da2d4bc8d2d1f98d36a653dd3049613d0a8ff69486debaca72f +size 131043 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceScreen.New-Space-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceScreen.New-Space-iPad-pseudo.png new file mode 100644 index 000000000..d232ff18c --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceScreen.New-Space-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:989cff06585632050964aff0d5cb8a9da15e8be009acbc584f0fde3477c3cee1 +size 137412 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceScreen.New-Space-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceScreen.New-Space-iPhone-en-GB.png new file mode 100644 index 000000000..4b097b6b7 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceScreen.New-Space-iPhone-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1c645564a1248975e005dc8208ee9b8fc5e7d85f8c6c03167fcb0ef5b1681b0 +size 86553 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceScreen.New-Space-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceScreen.New-Space-iPhone-pseudo.png new file mode 100644 index 000000000..46fb469eb --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceScreen.New-Space-iPhone-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b6753951ad86e77d21fadc03ddd6faedc24dd59256225901d83d5f253e23f65 +size 96839 diff --git a/UnitTests/Sources/RoomScreenViewModelTests.swift b/UnitTests/Sources/RoomScreenViewModelTests.swift index 360646e2d..fe500e3b2 100644 --- a/UnitTests/Sources/RoomScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomScreenViewModelTests.swift @@ -73,7 +73,7 @@ class RoomScreenViewModelTests: XCTestCase { // setup the loaded pinned events injection in the timeline let pinnedTimelineMock = TimelineProxyMock() let pinnedTimelineItemProviderMock = TimelineItemProviderMock() - let providerUpdateSubject = PassthroughSubject<([TimelineItemProxy], PaginationState), Never>() + let providerUpdateSubject = PassthroughSubject<([TimelineItemProxy], TimelinePaginationState), Never>() pinnedTimelineItemProviderMock.underlyingUpdatePublisher = providerUpdateSubject.eraseToAnyPublisher() pinnedTimelineMock.timelineItemProvider = pinnedTimelineItemProviderMock pinnedTimelineItemProviderMock.itemProxies = [.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test1")), uniqueID: .init("1"))), @@ -116,7 +116,7 @@ class RoomScreenViewModelTests: XCTestCase { let pinnedTimelineMock = TimelineProxyMock() let pinnedTimelineItemProviderMock = TimelineItemProviderMock() pinnedTimelineMock.timelineItemProvider = pinnedTimelineItemProviderMock - pinnedTimelineItemProviderMock.underlyingUpdatePublisher = Empty<([TimelineItemProxy], PaginationState), Never>().eraseToAnyPublisher() + pinnedTimelineItemProviderMock.underlyingUpdatePublisher = Empty<([TimelineItemProxy], TimelinePaginationState), Never>().eraseToAnyPublisher() pinnedTimelineItemProviderMock.itemProxies = [.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test1")), uniqueID: .init("1"))), .event(.init(item: EventTimelineItem(configuration: .init(eventID: "test2")), uniqueID: .init("2"))), .event(.init(item: EventTimelineItem(configuration: .init(eventID: "test3")), uniqueID: .init("3")))] @@ -175,7 +175,7 @@ class RoomScreenViewModelTests: XCTestCase { let pinnedTimelineMock = TimelineProxyMock() let pinnedTimelineItemProviderMock = TimelineItemProviderMock() pinnedTimelineMock.timelineItemProvider = pinnedTimelineItemProviderMock - pinnedTimelineItemProviderMock.underlyingUpdatePublisher = Empty<([TimelineItemProxy], PaginationState), Never>().eraseToAnyPublisher() + pinnedTimelineItemProviderMock.underlyingUpdatePublisher = Empty<([TimelineItemProxy], TimelinePaginationState), Never>().eraseToAnyPublisher() pinnedTimelineItemProviderMock.itemProxies = [.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test1")), uniqueID: .init("1"))), .event(.init(item: EventTimelineItem(configuration: .init(eventID: "test2")), uniqueID: .init("2"))), .event(.init(item: EventTimelineItem(configuration: .init(eventID: "test3")), uniqueID: .init("3")))] diff --git a/UnitTests/Sources/SpaceScreenViewModelTests.swift b/UnitTests/Sources/SpaceScreenViewModelTests.swift index 17f60030e..39b578bab 100644 --- a/UnitTests/Sources/SpaceScreenViewModelTests.swift +++ b/UnitTests/Sources/SpaceScreenViewModelTests.swift @@ -31,7 +31,7 @@ class SpaceScreenViewModelTests: XCTestCase { func testInitialState() { setupViewModel() - XCTAssertFalse(context.viewState.isPaginating) + XCTAssertEqual(context.viewState.paginationState, .idle) XCTAssertTrue(context.viewState.rooms.isEmpty) XCTAssertFalse(spaceRoomListProxy.paginateCalled) } @@ -41,7 +41,7 @@ class SpaceScreenViewModelTests: XCTestCase { let response = mockSpaceRooms.prefix(3) setupViewModel(paginationResponses: [Array(response)]) - XCTAssertFalse(context.viewState.isPaginating) + XCTAssertEqual(context.viewState.paginationState, .idle) XCTAssertTrue(context.viewState.rooms.isEmpty) XCTAssertFalse(spaceRoomListProxy.paginateCalled) XCTAssertFalse(response.isEmpty, "There should be some test rooms.") @@ -52,7 +52,7 @@ class SpaceScreenViewModelTests: XCTestCase { try await deferred.fulfill() // Then the screen should show a paginating indicator. - XCTAssertTrue(context.viewState.isPaginating) + XCTAssertEqual(context.viewState.paginationState, .paginating) XCTAssertEqual(spaceRoomListProxy.paginateCallsCount, 1) // When waiting for the pagination to finish. @@ -60,7 +60,7 @@ class SpaceScreenViewModelTests: XCTestCase { try await deferred.fulfill() // Then no more pagination requests should be made the the space rooms should be populated. - XCTAssertFalse(context.viewState.isPaginating) + XCTAssertEqual(context.viewState.paginationState, .endReached) XCTAssertEqual(spaceRoomListProxy.paginateCallsCount, 1) XCTAssertEqual(context.viewState.rooms.map(\.id), response.map(\.id)) } @@ -71,14 +71,14 @@ class SpaceScreenViewModelTests: XCTestCase { let response2 = mockSpaceRooms.suffix(mockSpaceRooms.count - 3) setupViewModel(paginationResponses: [Array(response1), Array(response2)]) - XCTAssertFalse(context.viewState.isPaginating) + XCTAssertEqual(context.viewState.paginationState, .idle) XCTAssertTrue(context.viewState.rooms.isEmpty) XCTAssertFalse(spaceRoomListProxy.paginateCalled) XCTAssertFalse(response1.isEmpty, "There should be some test rooms.") XCTAssertFalse(response2.isEmpty, "There should be more test rooms.") // When the pagination is triggered. - let deferredIsPaginating = deferFulfillment(context.observe(\.viewState.isPaginating), transitionValues: [true, false, true, false]) + let deferredIsPaginating = deferFulfillment(context.observe(\.viewState.paginationState), transitionValues: [.paginating, .idle, .paginating, .endReached]) let deferredState = deferFulfillment(spaceRoomListProxy.paginationStatePublisher, keyPath: \.self, transitionValues: [.loading, .idle(endReached: false), .loading, @@ -89,7 +89,7 @@ class SpaceScreenViewModelTests: XCTestCase { try await deferredIsPaginating.fulfill() try await deferredState.fulfill() - XCTAssertFalse(context.viewState.isPaginating) + XCTAssertEqual(context.viewState.paginationState, .endReached) XCTAssertEqual(spaceRoomListProxy.paginateCallsCount, 2) XCTAssertEqual(context.viewState.rooms.map(\.id), mockSpaceRooms.map(\.id)) } diff --git a/UnitTests/Sources/TimelineMediaPreviewViewModelTests.swift b/UnitTests/Sources/TimelineMediaPreviewViewModelTests.swift index 67ff50019..984e78d41 100644 --- a/UnitTests/Sources/TimelineMediaPreviewViewModelTests.swift +++ b/UnitTests/Sources/TimelineMediaPreviewViewModelTests.swift @@ -91,7 +91,7 @@ class TimelineMediaPreviewViewModelTests: XCTestCase { XCTAssertEqual(timelineController.paginateBackwardsCallCount, 0) // When swiping to a "loading more" item and there are more media items to load. - timelineController.paginationState = .init(backward: .idle, forward: .timelineEndReached) + timelineController.paginationState = .init(backward: .idle, forward: .endReached) timelineController.backPaginationResponses.append(RoomTimelineItemFixtures.mediaChunk) let failure = deferFailure(viewModel.state.previewControllerDriver, timeout: 1) { $0.isItemLoaded } context.send(viewAction: .updateCurrentItem(.loading(.paginatingBackwards)))