diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift index b367705ed..b984ad2d4 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift @@ -53,12 +53,16 @@ enum HomeScreenViewAction { } enum HomeScreenRoomListMode: CustomStringConvertible { + /// Shows nothing briefly on launch as the cached rooms usually publish in time to go straight to them. + case awaitingCachedRooms case skeletons case empty case rooms var description: String { switch self { + case .awaitingCachedRooms: + return "Awaiting cached rooms" case .skeletons: return "Showing placeholders" case .empty: @@ -96,7 +100,7 @@ struct HomeScreenViewState: BindableState { var shouldShowNewSoundBanner = false var rooms: [HomeScreenRoom] = [] - var roomListMode: HomeScreenRoomListMode = .skeletons + var roomListMode: HomeScreenRoomListMode = .awaitingCachedRooms var hasPendingInvitations = false @@ -113,7 +117,7 @@ struct HomeScreenViewState: BindableState { var isRoomListSearchEnabled = true var visibleRooms: [HomeScreenRoom] { - if roomListMode == .skeletons { + if roomListMode == .skeletons || roomListMode == .awaitingCachedRooms { return placeholderRooms } diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index 6f2bcf015..24ff5fc0a 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -181,6 +181,13 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol updateRoomListMode(with: roomSummaryProvider.statePublisher.value, hasRooms: !roomSummaryProvider.roomListPublisher.value.isEmpty) } + + // Fall back to the skeletons if the cached rooms take too long to publish. + Task { [weak self] in + try? await Task.sleep(for: .milliseconds(500)) + guard let self, state.roomListMode == .awaitingCachedRooms else { return } + state.roomListMode = .skeletons + } } // MARK: - Public @@ -342,8 +349,8 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol .empty // Loaded, there are no rooms at all. } else if hasRooms || isFiltering { .rooms // Loaded and the summaries have published (or filtered down to nothing). - } else if state.roomListMode == .skeletons { - .skeletons // Loaded but nothing published yet, flipping to .rooms would flash an empty list. + } else if state.roomListMode == .skeletons || state.roomListMode == .awaitingCachedRooms { + state.roomListMode // Loaded but nothing published yet, flipping to .rooms would flash an empty list. } else { .rooms } @@ -352,7 +359,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol return } - if roomListMode == .rooms, state.roomListMode == .skeletons { + if roomListMode == .rooms, state.roomListMode == .skeletons || state.roomListMode == .awaitingCachedRooms { analyticsService.signpost.finishTransaction(.cachedRoomList) } diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift index ca8397708..18ddcd984 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift @@ -219,7 +219,7 @@ struct HomeScreen_Previews: PreviewProvider, TestablePreview { let userID = "@alice:example.com" let roomSummaryProviderState: RoomSummaryProviderMockConfigurationState = switch mode { - case .skeletons: + case .awaitingCachedRooms, .skeletons: .loading case .empty: .loaded([]) diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenContent.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenContent.swift index 7d78d11c8..af409ab22 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenContent.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenContent.swift @@ -25,7 +25,7 @@ struct HomeScreenContent: View { GeometryReader { geometry in ScrollView { switch context.viewState.roomListMode { - case .skeletons: + case .awaitingCachedRooms, .skeletons: LazyVStack(spacing: 0) { ForEach(context.viewState.visibleRooms) { room in HomeScreenRoomCell(room: room, isSelected: false, mediaProvider: context.mediaProvider, action: context.send) @@ -34,6 +34,8 @@ struct HomeScreenContent: View { } } .disabled(true) + // Laid out but hidden while awaiting so the rooms replace it without a growing animation. + .opacity(context.viewState.roomListMode == .awaitingCachedRooms ? 0 : 1) .accessibilityRepresentation { Text(L10n.commonLoading) } @@ -103,7 +105,7 @@ struct HomeScreenContent: View { } } .scrollDismissesKeyboard(.immediately) - .scrollDisabled(context.viewState.roomListMode == .skeletons) + .scrollDisabled(context.viewState.roomListMode == .skeletons || context.viewState.roomListMode == .awaitingCachedRooms) .scrollBounceBehavior(context.viewState.roomListMode == .empty ? .basedOnSize : .automatic) .animation(.elementDefault, value: context.viewState.roomListMode) .animation(.none, value: context.viewState.visibleRooms) diff --git a/UnitTests/Sources/HomeScreenViewModelTests.swift b/UnitTests/Sources/HomeScreenViewModelTests.swift index 72fa1012d..15d30bd0c 100644 --- a/UnitTests/Sources/HomeScreenViewModelTests.swift +++ b/UnitTests/Sources/HomeScreenViewModelTests.swift @@ -421,6 +421,25 @@ final class HomeScreenViewModelTests { try await deferred.fulfill() } + @Test + func roomListModeGoesStraightToTheCachedRooms() async throws { + let (roomListSubject, _) = setupViewModelWithManualProvider(state: .loaded(totalNumberOfRooms: 8)) + #expect(context.viewState.roomListMode == .awaitingCachedRooms) + + let deferred = deferFulfillment(context.$viewState) { $0.roomListMode == .rooms } + roomListSubject.send(.mockRooms) + try await deferred.fulfill() + } + + @Test + func roomListModeShowsSkeletonsWhenTheCachedRoomsAreSlow() async throws { + _ = setupViewModelWithManualProvider(state: .loaded(totalNumberOfRooms: 8)) + #expect(context.viewState.roomListMode == .awaitingCachedRooms) + + let deferred = deferFulfillment(context.$viewState) { $0.roomListMode == .skeletons } + try await deferred.fulfill() + } + @Test func roomListModeDoesntReturnToSkeletonsWhenTheRoomsAreFilteredOut() async throws { let (roomListSubject, stateSubject) = setupViewModelWithManualProvider() @@ -437,9 +456,9 @@ final class HomeScreenViewModelTests { try await failure.fulfill() } - private func setupViewModelWithManualProvider() -> (CurrentValueSubject<[RoomSummary], Never>, CurrentValueSubject) { + private func setupViewModelWithManualProvider(state: RoomSummaryProviderState = .notLoaded) -> (CurrentValueSubject<[RoomSummary], Never>, CurrentValueSubject) { let roomListSubject = CurrentValueSubject<[RoomSummary], Never>([]) - let stateSubject = CurrentValueSubject(.notLoaded) + let stateSubject = CurrentValueSubject(state) let provider = RoomSummaryProviderMock() provider.roomListPublisher = roomListSubject.asCurrentValuePublisher()