Fix the room list flashing empty on launch

The room summary provider publishes its loading state and its rooms through two
independent streams, so the state can report loaded before the first summaries
have been applied. Flipping the room list mode to .rooms at that point renders
an empty list until the summaries arrive.

Combine the two publishers into a single subscription so that the mode and the
rooms are always updated from the same pair of values, and keep the skeletons up
until the rooms have published. Only the initial transition is held back, an
empty list later on is a filter or a search result and must not send the mode
back to the skeletons.
This commit is contained in:
Stefan Ceriu
2026-08-18 15:47:27 +03:00
parent bb0e80ba16
commit 8c9edd5ea9
2 changed files with 60 additions and 14 deletions
@@ -384,7 +384,52 @@ final class HomeScreenViewModelTests {
enum InviteType { case rooms, spaces }
private func setupViewModel(securityStatePublisher: CurrentValuePublisher<SessionSecurityState, Never>? = nil, invites: InviteType? = nil) {
@Test
func roomListModeWaitsForTheRoomsToPublish() async throws {
let (roomListSubject, stateSubject) = setupViewModelWithManualProvider()
#expect(context.viewState.roomListMode == .skeletons)
// The provider reports loaded before the first summaries have been published.
stateSubject.send(.loaded(totalNumberOfRooms: 8))
try await Task.sleep(for: .milliseconds(100))
#expect(context.viewState.roomListMode == .skeletons)
let deferred = deferFulfillment(context.$viewState) { $0.roomListMode == .rooms }
roomListSubject.send(.mockRooms)
try await deferred.fulfill()
}
@Test
func roomListModeDoesntReturnToSkeletonsWhenTheRoomsAreFilteredOut() async throws {
let (roomListSubject, stateSubject) = setupViewModelWithManualProvider()
stateSubject.send(.loaded(totalNumberOfRooms: 8))
let deferred = deferFulfillment(context.$viewState) { $0.roomListMode == .rooms }
roomListSubject.send(.mockRooms)
try await deferred.fulfill()
// A filter or a search without matches empties the list, the mode must not regress.
roomListSubject.send([])
stateSubject.send(.loaded(totalNumberOfRooms: 9))
try await Task.sleep(for: .milliseconds(100))
#expect(context.viewState.roomListMode == .rooms)
}
private func setupViewModelWithManualProvider() -> (CurrentValueSubject<[RoomSummary], Never>, CurrentValueSubject<RoomSummaryProviderState, Never>) {
let roomListSubject = CurrentValueSubject<[RoomSummary], Never>([])
let stateSubject = CurrentValueSubject<RoomSummaryProviderState, Never>(.notLoaded)
let provider = RoomSummaryProviderMock()
provider.roomListPublisher = roomListSubject.asCurrentValuePublisher()
provider.statePublisher = stateSubject.asCurrentValuePublisher()
setupViewModel(roomSummaryProvider: provider)
return (roomListSubject, stateSubject)
}
private func setupViewModel(securityStatePublisher: CurrentValuePublisher<SessionSecurityState, Never>? = nil, invites: InviteType? = nil, roomSummaryProvider: RoomSummaryProviderMock? = nil) {
cancellables.removeAll()
var rooms: [RoomSummary] = .mockRooms
@@ -398,10 +443,10 @@ final class HomeScreenViewModelTests {
break
}
roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(rooms)))
self.roomSummaryProvider = roomSummaryProvider ?? RoomSummaryProviderMock(.init(state: .loaded(rooms)))
clientProxy = ClientProxyMock(.init(userID: "@mock:client.com",
roomSummaryProvider: roomSummaryProvider))
roomSummaryProvider: self.roomSummaryProvider))
clientProxy.joinRoomViaReturnValue = .success(())
clientProxy.joinRoomAliasReturnValue = .success(())