From 59bc387c7d41891afbf0f6008bb83ad088dc7c84 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 8 Oct 2026 10:08:34 +0200 Subject: [PATCH 1/7] Replace the SearchBar by a SearchField in the room select screen The room select screen was using a SearchBar, with an active mode displaying the search results instead of the room list. Use a SearchField instead, displayed above the room list, which filters the rooms directly. The account switch section and the selected rooms are now also displayed while searching. - Remove isSearchActive from the state, and the ToggleSearchActive event. - The "No results" message is displayed when the query is not blank and no room matches it. - The back button now always closes the screen. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../roomselect/impl/RoomSelectEvent.kt | 1 - .../roomselect/impl/RoomSelectPresenter.kt | 5 +- .../roomselect/impl/RoomSelectState.kt | 1 - .../impl/RoomSelectStatePreviewParam.kt | 6 +- .../roomselect/impl/RoomSelectView.kt | 114 +++++++----------- .../impl/RoomSelectPresenterTest.kt | 15 --- .../roomselect/impl/RoomSelectViewTest.kt | 11 ++ 7 files changed, 59 insertions(+), 94 deletions(-) diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectEvent.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectEvent.kt index 0f7baecf12..d08bce5026 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectEvent.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectEvent.kt @@ -13,7 +13,6 @@ import io.element.android.libraries.matrix.ui.model.SelectRoomInfo sealed interface RoomSelectEvent { data class ToggleSelectedRoom(val room: SelectRoomInfo) : RoomSelectEvent - data object ToggleSearchActive : RoomSelectEvent data class UpdateVisibleRange(val range: IntRange) : RoomSelectEvent data object ToggleAccountListExpanded : RoomSelectEvent data class SelectAccount(val sessionId: SessionId) : RoomSelectEvent diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt index 3de9d22ba8..da2972aed2 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt @@ -63,7 +63,6 @@ class RoomSelectPresenter( override fun present(): RoomSelectState { var selectedRooms by remember { mutableStateOf(persistentListOf()) } val queryState = rememberTextFieldState() - var isSearchActive by remember { mutableStateOf(false) } var isAccountListExpanded by remember { mutableStateOf(false) } var selectedSessionId by remember { mutableStateOf(initialSessionId) } val sessions by remember { sessionStore.sessionsFlow() }.collectAsState(initial = emptyList()) @@ -136,7 +135,7 @@ class RoomSelectPresenter( derivedStateOf { when { roomSummaryDetailsList.isNotEmpty() -> SearchBarResultState.Results(roomSummaryDetailsList.toImmutableList()) - isSearchActive -> SearchBarResultState.NoResultsFound + queryState.text.isNotBlank() -> SearchBarResultState.NoResultsFound else -> SearchBarResultState.Initial } } @@ -152,7 +151,6 @@ class RoomSelectPresenter( selectedRooms.adding(event.room) } } - RoomSelectEvent.ToggleSearchActive -> isSearchActive = !isSearchActive RoomSelectEvent.ToggleAccountListExpanded -> isAccountListExpanded = !isAccountListExpanded is RoomSelectEvent.SelectAccount -> { selectedSessionId = event.sessionId @@ -172,7 +170,6 @@ class RoomSelectPresenter( maxNumberOfRooms = maxNumberOfRooms, resultState = searchResults, searchQuery = queryState, - isSearchActive = isSearchActive, selectedRooms = selectedRooms, selectedAccount = selectedAccount, hasRoomListError = hasRoomListError, diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectState.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectState.kt index 7af7b28ddf..7ec5fdf0a6 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectState.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectState.kt @@ -20,7 +20,6 @@ data class RoomSelectState( val maxNumberOfRooms: Int, val resultState: SearchBarResultState>, val searchQuery: TextFieldState, - val isSearchActive: Boolean, val selectedRooms: ImmutableList, val selectedAccount: MatrixUser, /** diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStatePreviewParam.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStatePreviewParam.kt index 0c46c652ef..440179e55d 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStatePreviewParam.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStatePreviewParam.kt @@ -26,17 +26,15 @@ open class RoomSelectStatePreviewParam : PreviewParameterProvider get() = sequenceOf( aRoomSelectState(), - aRoomSelectState(searchQuery = "Test", isSearchActive = true), + aRoomSelectState(searchQuery = "Test", resultState = SearchBarResultState.NoResultsFound), aRoomSelectState(resultState = SearchBarResultState.Results(aRoomSelectRoomList())), aRoomSelectState( resultState = SearchBarResultState.Results(aRoomSelectRoomList()), searchQuery = "Test", - isSearchActive = true, ), aRoomSelectState( resultState = SearchBarResultState.Results(aRoomSelectRoomList()), searchQuery = "Test", - isSearchActive = true, selectedRooms = aRoomSelectRoomList().subList(0, 1), ), aRoomSelectState( @@ -76,7 +74,6 @@ internal fun aRoomSelectState( maxNumberOfRooms: Int = 10, resultState: SearchBarResultState> = SearchBarResultState.Initial, searchQuery: String = "", - isSearchActive: Boolean = false, selectedRooms: ImmutableList = persistentListOf(), selectedAccount: MatrixUser = MatrixUser(UserId("@alice:example.org")), hasRoomListError: Boolean = false, @@ -88,7 +85,6 @@ internal fun aRoomSelectState( maxNumberOfRooms = maxNumberOfRooms, resultState = resultState, searchQuery = TextFieldState(initialText = searchQuery), - isSearchActive = isSearchActive, selectedRooms = selectedRooms, selectedAccount = selectedAccount, hasRoomListError = hasRoomListError, diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt index a201faaa3d..96f3ac50b9 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt @@ -49,6 +49,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -70,8 +71,8 @@ import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.ListItem import io.element.android.libraries.designsystem.theme.components.ListSectionHeader import io.element.android.libraries.designsystem.theme.components.Scaffold -import io.element.android.libraries.designsystem.theme.components.SearchBar import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.designsystem.theme.components.SearchField import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TopAppBar @@ -123,10 +124,8 @@ fun RoomSelectView( } var canHandleBack by remember { mutableStateOf(true) } - fun onBackButton(state: RoomSelectState) { - if (state.isSearchActive) { - state.eventSink(RoomSelectEvent.ToggleSearchActive) - } else if (canHandleBack) { + fun onBackButton() { + if (canHandleBack) { canHandleBack = false onDismiss() } @@ -134,7 +133,7 @@ fun RoomSelectView( BackHandler( enabled = canHandleBack, - onBack = { onBackButton(state) } + onBack = ::onBackButton, ) // Use a new list state when the selected account changes, so that the list of rooms is scrolled to the top @@ -159,7 +158,7 @@ fun RoomSelectView( navigationIcon = { BackButton( enabled = canHandleBack, - onClick = { onBackButton(state) } + onClick = ::onBackButton, ) }, actions = { @@ -179,81 +178,47 @@ fun RoomSelectView( .consumeWindowInsets(paddingValues) .then( // Only when the account switch section is displayed, else its unknown height would consume all the scroll events - if (state.showAccountSwitch && !state.isSearchActive) { + if (state.showAccountSwitch) { Modifier.nestedScroll(accountSwitchScrollBehavior.nestedScrollConnection) } else { Modifier } ) ) { - SearchBar( + SearchField( + state = state.searchQuery, modifier = Modifier .fillMaxWidth() .padding( WindowInsets.safeDrawing .only(WindowInsetsSides.Horizontal) .asPaddingValues() - ), - placeHolderTitle = stringResource(CommonStrings.action_search), - queryState = state.searchQuery, - active = state.isSearchActive, - onActiveChange = { state.eventSink(RoomSelectEvent.ToggleSearchActive) }, - resultState = state.resultState, - showBackButton = false, - ) { summaries -> - LazyColumn( - state = lazyListState, - contentPadding = lazyColumnContentPadding, - ) { - if (summaries.isNotEmpty()) { - item { - ListSectionHeader( - title = stringResource(CommonStrings.common_header_rooms), - hasDivider = false, - ) - } - } - item { - SelectedRoomsHelper( - selectedRooms = state.selectedRooms, - showVerticalSpace = false, - ) - } - items(summaries, key = { it.roomId.value }) { roomSummary -> - RoomSummaryView( - roomSummary, - isSelected = state.selectedRooms.any { it.roomId == roomSummary.roomId }, - onSelection = { roomSummary -> - state.eventSink(RoomSelectEvent.ToggleSelectedRoom(roomSummary)) - }, - canBeSelected = state.canSelectMoreRooms, - ) - } + ) + .padding(horizontal = 16.dp, vertical = 8.dp), + placeholder = stringResource(CommonStrings.action_search), + ) + if (state.showAccountSwitch) { + // Keep this space outside the collapsing section, so that the section does not touch the search field when it is collapsing + Spacer(modifier = Modifier.height(8.dp)) + TopAppBarScrollBehaviorLayout(scrollBehavior = accountSwitchScrollBehavior) { + AccountSwitchSection( + selectedAccount = state.selectedAccount, + otherAccounts = state.otherAccounts, + isExpanded = state.isAccountListExpanded, + onToggleExpand = { state.eventSink(RoomSelectEvent.ToggleAccountListExpanded) }, + onSelectAccount = { state.eventSink(RoomSelectEvent.SelectAccount(it)) }, + ) } } - - if (!state.isSearchActive) { - if (state.showAccountSwitch) { - // Keep this space outside the collapsing section, so that the section does not touch the search bar when it is collapsing - Spacer(modifier = Modifier.height(8.dp)) - TopAppBarScrollBehaviorLayout(scrollBehavior = accountSwitchScrollBehavior) { - AccountSwitchSection( - selectedAccount = state.selectedAccount, - otherAccounts = state.otherAccounts, - isExpanded = state.isAccountListExpanded, - onToggleExpand = { state.eventSink(RoomSelectEvent.ToggleAccountListExpanded) }, - onSelectAccount = { state.eventSink(RoomSelectEvent.SelectAccount(it)) }, - ) - } - } - SelectedRoomsHelper( - selectedRooms = state.selectedRooms, - // showVerticalSpace only if there is no other accounts - showVerticalSpace = !state.showAccountSwitch, - ) - if (state.hasRoomListError) { - RoomListError() - } else if (state.resultState is SearchBarResultState.Results) { + SelectedRoomsHelper( + selectedRooms = state.selectedRooms, + // showVerticalSpace only if there is no other accounts + showVerticalSpace = !state.showAccountSwitch, + ) + when { + state.hasRoomListError -> RoomListError() + state.resultState is SearchBarResultState.NoResultsFound -> NoResultsFound() + state.resultState is SearchBarResultState.Results -> { LazyColumn( state = lazyListState, contentPadding = lazyColumnContentPadding, @@ -278,11 +243,24 @@ fun RoomSelectView( } } } + else -> Unit } } } } +@Composable +private fun NoResultsFound() { + Text( + text = stringResource(CommonStrings.common_no_results), + textAlign = TextAlign.Center, + color = ElementTheme.colors.textSecondary, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 80.dp), + ) +} + @Composable private fun RoomListError() { IconTitleSubtitleMolecule( diff --git a/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt b/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt index 0694cc27f0..50987f5ce6 100644 --- a/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt +++ b/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt @@ -59,7 +59,6 @@ class RoomSelectPresenterTest { val initialState = awaitItem() assertThat(initialState.selectedRooms).isEmpty() assertThat(initialState.resultState).isInstanceOf(SearchBarResultState.Initial::class.java) - assertThat(initialState.isSearchActive).isFalse() assertThat(initialState.maxNumberOfRooms).isEqualTo(10) assertThat(initialState.canSelectMoreRooms).isTrue() assertThat(initialState.selectedAccount).isEqualTo(MatrixUser(A_SESSION_ID)) @@ -284,18 +283,6 @@ class RoomSelectPresenterTest { ), ) - @Test - fun `present - toggle search active`() = runTest { - val presenter = createRoomSelectPresenter() - presenter.test { - val initialState = awaitItem() - initialState.eventSink(RoomSelectEvent.ToggleSearchActive) - assertThat(awaitItem().isSearchActive).isTrue() - initialState.eventSink(RoomSelectEvent.ToggleSearchActive) - assertThat(awaitItem().isSearchActive).isFalse() - } - } - @Test fun `present - update query`() = runTest { val roomSummary = aRoomSummary() @@ -314,8 +301,6 @@ class RoomSelectPresenterTest { // Do not compare the lambda because they will be different. So copy the lambda from expectedRoomSummary to result val result = (awaitItem().resultState as SearchBarResultState.Results).results assertThat(result).isEqualTo(listOf(expectedRoomInfo)) - initialState.eventSink(RoomSelectEvent.ToggleSearchActive) - skipItems(1) initialState.searchQuery.setTextAndPlaceCursorAtEnd("string not contained") assertThat( roomList.currentFilter.value diff --git a/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectViewTest.kt b/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectViewTest.kt index 71284e6ec5..3f60508816 100644 --- a/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectViewTest.kt +++ b/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectViewTest.kt @@ -155,6 +155,17 @@ class RoomSelectViewTest : RobolectricTest() { onNodeWithText("Room 0").assertIsNotDisplayed() } + @Test + fun `a message is displayed when no room matches the search query`() = runAndroidComposeUiTest { + setRoomSelectView( + aRoomSelectState( + searchQuery = "Test", + resultState = SearchBarResultState.NoResultsFound, + ), + ) + onNodeWithText(activity!!.getString(CommonStrings.common_no_results)).assertIsDisplayed() + } + @Test fun `an error is displayed instead of the rooms if they cannot be loaded`() = runAndroidComposeUiTest { setRoomSelectView( From 0c6da39acb5cf99eee4d63e79a05e9c140446153 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 8 Oct 2026 17:29:44 +0200 Subject: [PATCH 2/7] Room select: update empty search UI and behavior. --- .../roomselect/impl/RoomSelectPresenter.kt | 4 +- .../roomselect/impl/RoomSelectView.kt | 72 +++++++++++++++---- .../impl/RoomSelectPresenterTest.kt | 27 +++++++ .../src/main/res/values/localazy.xml | 2 + 4 files changed, 90 insertions(+), 15 deletions(-) diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt index da2972aed2..9229718f10 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt @@ -8,7 +8,6 @@ package io.element.android.libraries.roomselect.impl -import androidx.compose.foundation.text.input.clearText import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -154,9 +153,8 @@ class RoomSelectPresenter( RoomSelectEvent.ToggleAccountListExpanded -> isAccountListExpanded = !isAccountListExpanded is RoomSelectEvent.SelectAccount -> { selectedSessionId = event.sessionId - // The selected rooms and the search query are specific to the session + // The selected rooms are specific to the session. The search query is kept, and applied to the rooms of the new session. selectedRooms = persistentListOf() - queryState.clearText() isAccountListExpanded = false } is RoomSelectEvent.UpdateVisibleRange -> coroutineScope.launch { diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt index 96f3ac50b9..023032bd43 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt @@ -10,12 +10,14 @@ package io.element.android.libraries.roomselect.impl import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animate import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -24,18 +26,23 @@ import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf @@ -46,6 +53,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.text.font.FontStyle @@ -136,13 +144,27 @@ fun RoomSelectView( onBack = ::onBackButton, ) - // Use a new list state when the selected account changes, so that the list of rooms is scrolled to the top - val lazyListState = key(state.selectedAccount.userId) { rememberLazyListState() } + val searchQuery = state.searchQuery.text.toString() + // Use a new list state when the selected account or the search query changes, so that the list of rooms is scrolled to the top + val lazyListState = key(state.selectedAccount.userId, searchQuery) { rememberLazyListState() } // Hide the account switch section when the room list is scrolled up, and show it again as soon as it is scrolled down. // Use a new state when the selected account changes, so that the section is fully displayed again. val accountSwitchScrollBehavior = key(state.selectedAccount.userId) { TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()) } + // Collapse the account switch section when the user starts searching. It can be displayed again by scrolling down. + val isSearching = searchQuery.isNotEmpty() + LaunchedEffect(isSearching) { + if (isSearching) { + val topAppBarState = accountSwitchScrollBehavior.state + animate( + initialValue = topAppBarState.heightOffset, + targetValue = topAppBarState.heightOffsetLimit, + ) { value, _ -> + topAppBarState.heightOffset = value + } + } + } OnVisibleRangeChangeEffect(lazyListState) { visibleRange -> state.eventSink(RoomSelectEvent.UpdateVisibleRange(visibleRange)) } @@ -217,7 +239,7 @@ fun RoomSelectView( ) when { state.hasRoomListError -> RoomListError() - state.resultState is SearchBarResultState.NoResultsFound -> NoResultsFound() + state.resultState is SearchBarResultState.NoResultsFound -> NoResultsView(searchQuery = searchQuery) state.resultState is SearchBarResultState.Results -> { LazyColumn( state = lazyListState, @@ -225,8 +247,13 @@ fun RoomSelectView( ) { if (state.resultState.results.isNotEmpty()) { item { + val headerText = if (searchQuery.isEmpty()) { + stringResource(CommonStrings.common_header_rooms) + } else { + stringResource(CommonStrings.common_results_for, searchQuery) + } ListSectionHeader( - title = stringResource(CommonStrings.common_header_rooms), + title = headerText, hasDivider = false, ) } @@ -250,15 +277,36 @@ fun RoomSelectView( } @Composable -private fun NoResultsFound() { - Text( - text = stringResource(CommonStrings.common_no_results), - textAlign = TextAlign.Center, - color = ElementTheme.colors.textSecondary, +private fun NoResultsView( + searchQuery: String, +) { + Column( modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 80.dp), - ) + .fillMaxSize() + // Scrollable, so that the account switch section can be displayed again by scrolling down + .verticalScroll(rememberScrollState()) + .padding(horizontal = 24.dp, vertical = 40.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = spacedBy(8.dp), + ) { + Text( + text = stringResource(CommonStrings.common_no_results), + modifier = Modifier + .semantics { + heading() + }, + textAlign = TextAlign.Center, + style = ElementTheme.typography.fontHeadingMdBold, + color = ElementTheme.colors.textPrimary, + ) + Text( + text = stringResource(CommonStrings.common_no_results_for, searchQuery), + modifier = Modifier.widthIn(max = 300.dp), + textAlign = TextAlign.Center, + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + ) + } } @Composable diff --git a/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt b/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt index 50987f5ce6..2d04d28dc4 100644 --- a/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt +++ b/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt @@ -263,6 +263,33 @@ class RoomSelectPresenterTest { } } + @Test + fun `present - select another account keeps the search query and applies it to the rooms of this account`() = runTest { + val roomList2 = FakeDynamicRoomList(summaries = MutableStateFlow(listOf(aRoomSummary(roomId = A_ROOM_ID_2)))) + val roomListServices = mapOf( + A_SESSION_ID to FakeRoomListService(createRoomListLambda = { FakeDynamicRoomList(summaries = MutableStateFlow(listOf(aRoomSummary()))) }), + A_SESSION_ID_2 to FakeRoomListService(createRoomListLambda = { roomList2 }), + ) + val presenter = createRoomSelectPresenter( + mode = RoomSelectMode.Share, + sessionStore = aSessionStoreWithTwoAccounts(), + matrixClientProvider = FakeMatrixClientProvider { sessionId -> + Result.success(FakeMatrixClient(sessionId = sessionId, roomListService = roomListServices.getValue(sessionId))) + }, + ) + presenter.test { + val state = awaitLastSequentialItem() + state.searchQuery.setTextAndPlaceCursorAtEnd("query") + state.eventSink(RoomSelectEvent.SelectAccount(A_SESSION_ID_2)) + val finalState = consumeItemsUntilPredicate { + it.selectedAccount.userId == A_SESSION_ID_2 && it.resultState is SearchBarResultState.Results + }.last() + assertThat(finalState.searchQuery.text.toString()).isEqualTo("query") + assertThat(finalState.showAccountSwitch).isTrue() + assertThat(roomList2.currentFilter.value).isEqualTo(RoomListFilter.NormalizedMatchRoomName("query")) + } + } + @Test fun `present - forward mode with several accounts does not show the account switch`() = runTest { val presenter = createRoomSelectPresenter( diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 0eeb366d72..d2cf805c69 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -335,6 +335,7 @@ Reason: %1$s." "Mute" "Name" "%1$s (%2$s)" + "There are no results for ā€œ%1$sā€. Try a new search term." "No results" "No room name" "No space name" @@ -381,6 +382,7 @@ Reason: %1$s." "Replying to %1$s" "Report a problem" "Report submitted" + "Results for ā€œ%1$sā€" "Rich text editor" "Role" "Room" From 2177a62ff12c357f40dcab35ec661afc75859c54 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 8 Oct 2026 17:42:29 +0200 Subject: [PATCH 3/7] Apply the room list filter requested before the controller is created The dynamic entries controller of a room list is created asynchronously, once the entries are observed. Filters set before that were silently dropped, so for instance the search query was not applied to the rooms of a newly selected account in the room select screen. RustDynamicRoomList now owns the controller, remembers the last requested filter, and applies it when the controller is created if it differs from the initial one. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../impl/roomlist/RoomListExtensions.kt | 2 +- .../matrix/impl/roomlist/RoomListFactory.kt | 19 +++--- .../impl/roomlist/RustDynamicRoomList.kt | 43 +++++++++--- ...FakeFfiRoomListDynamicEntriesController.kt | 25 +++++++ .../impl/roomlist/RustDynamicRoomListTest.kt | 67 +++++++++++++++++++ 5 files changed, 134 insertions(+), 22 deletions(-) create mode 100644 libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomListDynamicEntriesController.kt create mode 100644 libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustDynamicRoomListTest.kt diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt index bf57e4295b..301fb44bea 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt @@ -57,7 +57,7 @@ fun RoomListInterface.loadingStateFlow(): Flow = internal fun RoomListInterface.entriesFlow( pageSize: Int, initialFilterKind: RoomListEntriesDynamicFilterKind, - onControllerCreated: (RoomListDynamicEntriesController) -> Unit, + onControllerCreated: suspend (RoomListDynamicEntriesController) -> Unit, ): Flow> = callbackFlow { val listener = object : RoomListEntriesListener { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt index 1d90b07104..c12ddbede8 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt @@ -23,7 +23,6 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import org.matrix.rustcomponents.sdk.RoomListDynamicEntriesController import org.matrix.rustcomponents.sdk.RoomListLoadingState import org.matrix.rustcomponents.sdk.RoomListService import kotlin.coroutines.CoroutineContext @@ -49,7 +48,13 @@ internal class RoomListFactory( val summariesFlow = MutableSharedFlow>(replay = 1, extraBufferCapacity = 1) val processor = RoomSummaryListProcessor(summariesFlow, innerRoomListService, coroutineContext, roomSummaryFactory, analyticsService) var innerRoomList: InnerRoomList? = null - var dynamicController: RoomListDynamicEntriesController? = null + val dynamicRoomList = RustDynamicRoomList( + summaries = summariesFlow, + loadingState = loadingStateFlow, + processor = processor, + pageSize = pageSize, + initialFilter = initialFilter, + ) val firstRoomsTransaction = analyticsService.startTransaction("Load first set of rooms", "innerRoomList.entriesFlow") @@ -60,7 +65,7 @@ internal class RoomListFactory( pageSize = pageSize, initialFilterKind = RoomListFilterMapper.toRustFilter(initialFilter), onControllerCreated = { controller -> - dynamicController = controller + dynamicRoomList.onControllerCreated(controller, initialFilter) } ).onEach { update -> if (!firstRoomsTransaction.isFinished()) { @@ -80,13 +85,7 @@ internal class RoomListFactory( }.invokeOnCompletion { innerRoomList?.destroy() } - return RustDynamicRoomList( - summaries = summariesFlow, - loadingState = loadingStateFlow, - processor = processor, - pageSize = pageSize, - dynamicController = { dynamicController } - ) + return dynamicRoomList } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustDynamicRoomList.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustDynamicRoomList.kt index 0d7b2394b3..5161b3d13a 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustDynamicRoomList.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustDynamicRoomList.kt @@ -24,37 +24,58 @@ internal class RustDynamicRoomList( override val loadingState: MutableStateFlow, private val processor: RoomSummaryListProcessor, override val pageSize: Int, - private val dynamicController: () -> RoomListDynamicEntriesController?, + initialFilter: RoomListFilter, private val addPagesCount: Int = DEFAULT_ADD_PAGES_COUNT ) : DynamicRoomList { private val mutex = Mutex() + // The controller is created asynchronously, once the entries of the room list are observed + private var dynamicController: RoomListDynamicEntriesController? = null + + // The last requested filter, so that it can be applied when the controller is created + private var currentFilter: RoomListFilter = initialFilter + + /** + * To be called when the controller is created. The controller already has the initial filter set, so apply the filter + * only if it has been updated in the meantime. + */ + suspend fun onControllerCreated(controller: RoomListDynamicEntriesController, initialFilter: RoomListFilter) { + mutex.withLock { + dynamicController = controller + if (currentFilter != initialFilter) { + controller.applyFilter(currentFilter) + } + } + } + override suspend fun rebuildSummaries() { processor.rebuildRoomSummaries() } override suspend fun updateFilter(filter: RoomListFilter) { mutex.withLock { - dynamicController()?.let { controller -> - // Reset pagination when filter changes - controller.resetToOnePage() - val rustFilter = RoomListFilterMapper.toRustFilter(filter) - controller.setFilter(rustFilter) - // Then preload some pages - controller.addPages(addPagesCount) - } + currentFilter = filter + dynamicController?.applyFilter(filter) } } + private fun RoomListDynamicEntriesController.applyFilter(filter: RoomListFilter) { + // Reset pagination when filter changes + resetToOnePage() + setFilter(RoomListFilterMapper.toRustFilter(filter)) + // Then preload some pages + addPages(addPagesCount) + } + override suspend fun loadMore() { mutex.withLock { - dynamicController()?.addPages(addPagesCount) + dynamicController?.addPages(addPagesCount) } } override suspend fun reset() { mutex.withLock { - dynamicController()?.resetToOnePage() + dynamicController?.resetToOnePage() } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomListDynamicEntriesController.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomListDynamicEntriesController.kt new file mode 100644 index 0000000000..9a80841069 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomListDynamicEntriesController.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 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. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import org.matrix.rustcomponents.sdk.NoHandle +import org.matrix.rustcomponents.sdk.RoomListDynamicEntriesController +import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind + +class FakeFfiRoomListDynamicEntriesController : RoomListDynamicEntriesController(NoHandle) { + val filters = mutableListOf() + + override fun setFilter(kind: RoomListEntriesDynamicFilterKind): Boolean { + filters.add(kind) + return true + } + + override fun resetToOnePage() = Unit + + override fun addOnePage() = Unit +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustDynamicRoomListTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustDynamicRoomListTest.kt new file mode 100644 index 0000000000..fc2686f882 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustDynamicRoomListTest.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 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. + */ + +package io.element.android.libraries.matrix.impl.roomlist + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.roomlist.RoomList +import io.element.android.libraries.matrix.api.roomlist.RoomListFilter +import io.element.android.libraries.matrix.api.roomlist.RoomSummary +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomListDynamicEntriesController +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomListService +import io.element.android.services.analytics.test.FakeAnalyticsService +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class RustDynamicRoomListTest { + @Test + fun `updateFilter before the controller is created applies the filter when it is created`() = runTest { + val sut = createRustDynamicRoomList() + sut.updateFilter(RoomListFilter.NormalizedMatchRoomName("query")) + val controller = FakeFfiRoomListDynamicEntriesController() + sut.onControllerCreated(controller, initialFilter = RoomListFilter.all()) + assertThat(controller.filters).containsExactly(RoomListFilterMapper.toRustFilter(RoomListFilter.NormalizedMatchRoomName("query"))) + } + + @Test + fun `when the filter is not updated, the controller filter is not changed when it is created`() = runTest { + val sut = createRustDynamicRoomList() + val controller = FakeFfiRoomListDynamicEntriesController() + sut.onControllerCreated(controller, initialFilter = RoomListFilter.all()) + assertThat(controller.filters).isEmpty() + } + + @Test + fun `updateFilter after the controller is created applies the filter`() = runTest { + val sut = createRustDynamicRoomList() + val controller = FakeFfiRoomListDynamicEntriesController() + sut.onControllerCreated(controller, initialFilter = RoomListFilter.all()) + sut.updateFilter(RoomListFilter.NormalizedMatchRoomName("query")) + assertThat(controller.filters).containsExactly(RoomListFilterMapper.toRustFilter(RoomListFilter.NormalizedMatchRoomName("query"))) + } + + private fun TestScope.createRustDynamicRoomList(): RustDynamicRoomList { + val summaries = MutableSharedFlow>(replay = 1) + return RustDynamicRoomList( + summaries = summaries, + loadingState = MutableStateFlow(RoomList.LoadingState.NotLoaded), + processor = RoomSummaryListProcessor( + summaries, + FakeFfiRoomListService(), + coroutineContext = StandardTestDispatcher(testScheduler), + roomSummaryFactory = RoomSummaryFactory(), + analyticsService = FakeAnalyticsService(), + ), + pageSize = 10, + initialFilter = RoomListFilter.all(), + ) + } +} From 9429bd9420a5cba2f87e054d41e72a621341efda Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 9 Oct 2026 09:04:41 +0200 Subject: [PATCH 4/7] Clean up --- .../android/libraries/roomselect/impl/RoomSelectView.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt index 023032bd43..4c5ad390e7 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt @@ -17,7 +17,6 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -287,7 +286,7 @@ private fun NoResultsView( .verticalScroll(rememberScrollState()) .padding(horizontal = 24.dp, vertical = 40.dp), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { Text( text = stringResource(CommonStrings.common_no_results), From 2f7e2316e13f007bbf2cfed0b91bf66a4a81d7b0 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Fri, 9 Oct 2026 07:23:33 +0000 Subject: [PATCH 5/7] Update screenshots --- .../libraries.roomselect.impl_RoomSelectView_Day_0_en.png | 4 ++-- .../libraries.roomselect.impl_RoomSelectView_Day_1_en.png | 4 ++-- .../libraries.roomselect.impl_RoomSelectView_Day_2_en.png | 4 ++-- .../libraries.roomselect.impl_RoomSelectView_Day_3_en.png | 4 ++-- .../libraries.roomselect.impl_RoomSelectView_Day_4_en.png | 4 ++-- .../libraries.roomselect.impl_RoomSelectView_Day_5_en.png | 4 ++-- .../libraries.roomselect.impl_RoomSelectView_Day_6_en.png | 4 ++-- .../libraries.roomselect.impl_RoomSelectView_Day_7_en.png | 4 ++-- .../libraries.roomselect.impl_RoomSelectView_Day_8_en.png | 4 ++-- .../libraries.roomselect.impl_RoomSelectView_Day_9_en.png | 4 ++-- .../libraries.roomselect.impl_RoomSelectView_Night_0_en.png | 4 ++-- .../libraries.roomselect.impl_RoomSelectView_Night_1_en.png | 4 ++-- .../libraries.roomselect.impl_RoomSelectView_Night_2_en.png | 4 ++-- .../libraries.roomselect.impl_RoomSelectView_Night_3_en.png | 4 ++-- .../libraries.roomselect.impl_RoomSelectView_Night_4_en.png | 4 ++-- .../libraries.roomselect.impl_RoomSelectView_Night_5_en.png | 4 ++-- .../libraries.roomselect.impl_RoomSelectView_Night_6_en.png | 4 ++-- .../libraries.roomselect.impl_RoomSelectView_Night_7_en.png | 4 ++-- .../libraries.roomselect.impl_RoomSelectView_Night_8_en.png | 4 ++-- .../libraries.roomselect.impl_RoomSelectView_Night_9_en.png | 4 ++-- 20 files changed, 40 insertions(+), 40 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_0_en.png index ed28276533..df5eb341fb 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf1e3ce457d2e28ee929ade5bfc1abfc0a62ef2dc8443824300d15f78e843b75 -size 11233 +oid sha256:9dad60d90d63d832243181f9eb3517a1ad15817bba3a0686e3c275a489d42921 +size 12363 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_1_en.png index eacac1fc43..a4d55009a3 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:90452c32320a3eaad1c520885c487dc6242e1333f2084940c85e8f4060940b1e -size 9166 +oid sha256:27198b09382c662a0fd3b35ede7afa185ffea636f4fbfd236b00163169baa4c5 +size 21598 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_2_en.png index 7ac7728536..f7c327daab 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96da17f60cf84b6e5052d9753e3e81fc6f27e1ca78bbadb4501c9e4c864b6eeb -size 34329 +oid sha256:83c813b6e07f8b22ade72dbec568417c7ed7b15aba661ecc0c59d67b9b962a26 +size 35361 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_3_en.png index 48e6dbced5..06ae089b98 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da7a0e6c7ed39c6d1c8f900e32d6139da70333bac1356b017148f68ea7c1dcbf -size 32126 +oid sha256:fab8e57274220f8261e4650c227aeae92fc8994300285148a96ed844b0d7d8ea +size 36277 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_4_en.png index 19362d4a71..679cea54de 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c234a4c6b0c3219e4cfc6330601b15f7dc1db385b4024c18bd7f3376c27f7122 -size 36112 +oid sha256:15a9aeba4a79ce52622bcb1761483603b415d34a14017390ecc481df59d6b640 +size 39922 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_5_en.png index ce8da40ea2..b6628b455d 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:512f9d0246ac666d024395672ef7ff33473a40d2fe261bc84b70c1bb4381a1bd -size 33822 +oid sha256:adb459b531ec73c8d0ec33ff7a1d55685f4829458588998c021b737743584d88 +size 34886 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_6_en.png index 16a6ac408f..ed4d758af3 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea0a573af47011a6f19ecc304d4cbb78522a1e6c41a353923425aeb7c8ea8e07 -size 37392 +oid sha256:66f4c2db94fa0e1230d46f4afb132a31053d6bbf66d876c7a2d71415c9d50a64 +size 38470 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_7_en.png index d17e9a0c3d..ab906e63cf 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:06a3b2ffff122289becfa847e8ff4979987ba17aeab85e2c6d277f0d2234878a -size 44276 +oid sha256:a2dd9df048447b7cc742e81fa3e8436a4494b953132cb77e0614261c0e0ae3c8 +size 45326 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_8_en.png index 718c7d031a..3e5a59a127 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:454810b015c969009fd610b5b0141a892f68234147b87724d5dad0a0d7b5aab3 -size 48658 +oid sha256:aafa2a411ed5dbc23948f70ac2745883ed5824a7148fc0e9344deac2d69fafca +size 49763 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_9_en.png index 2625c34346..76829c2224 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e2d9e7c6b7b866b610df385ff5c7a61a5bb716efd4479f9a7f07e4c61e69487 -size 37335 +oid sha256:1013a91d529084a8f65776c2b097be3409d49344bf45c6f5634179e61d21aac4 +size 38419 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_0_en.png index 149038c4f5..760d0f8033 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:71b4ecc3a18de7be9eb8ef453852f6f9a5c44ae8edb0a14cdf09a4a240c3faab -size 10703 +oid sha256:757573392dec052edf0ed522a344edca4777075ede316ae3fe757ec72ae47a03 +size 11757 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_1_en.png index 9287ee04f3..7178aae2c0 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e4a4c20eb92cf93d5e7a76973b46863ed79b32e36c9d970b311b9681a382b23 -size 8957 +oid sha256:476aecd18345c3f0049a3cf455b3d0c6d9799774772cfc7044708a7eefdeec1c +size 20845 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_2_en.png index c4d89d3fb0..20787d9daf 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f472811552d7499bd0acd7c929224a643957816cb4122c1a95ceaf5a7b32f1f3 -size 34198 +oid sha256:210355754bec4be43cc9a183c2125a0baf1de282cd8d1e88e85919c326159e4c +size 35225 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_3_en.png index 60b3f75e62..20273cb08a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d722f376b5cc87f8a11be2d42347fbf90f31b04699f9ec6373ed068454545372 -size 32336 +oid sha256:091fb942406728012bb8d95f17465166c92d2506b11703198fb75211865bf28d +size 36138 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_4_en.png index c27956224c..bc19519280 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f45a3dff7b5b669c643394ba12e0df55344ff76177030d1ae1de57275c57c4c6 -size 36401 +oid sha256:207c51ecd3b0b39d20e4d54631aa2160120ba4711f121626f6ee56a00765b7fc +size 39859 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_5_en.png index 0c2e69b595..62ebafd5df 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c402aa3102e2f31c8b9ec911fdfeddd626d65032973f3f5753b2117a90118edb -size 33636 +oid sha256:000e055f5b9cb2c002b4879977aee3d85bedf377794c74aa18f65527ff34ee43 +size 34657 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_6_en.png index 8d1929baf4..75c8fef6bb 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54d3269f34abec26529050d7a89b581bdc3d5d1e20980d63993a08e73ec7a655 -size 37244 +oid sha256:03678c2b6caca9a63811717ba48d823924c855fa9b5b350eea6f6b942d3820d5 +size 38245 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_7_en.png index 267a25e404..cd2b8e2a8f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e37a74fe450f9f150963f0099d25d51b5fca93905ebf24dd243e703344eec2d7 -size 44540 +oid sha256:152d862155867306be3bbede1b1734b6c76465bc0baabf77fd9b6af462ef99e8 +size 45521 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_8_en.png index 075b7ea1ed..d93ab616e8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0cd1e2aba9e07a10237b310359f1011434ac4d55fe68acc79bf2f83461fbb88 -size 48843 +oid sha256:dae86fa884bb76b3f88a76d07894490e7779c1d73bc218de3f26f5e5c22b4680 +size 49785 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_9_en.png index 9dc7466680..1374addcb3 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a327f4c2c4e3204a14a4ee601a1ddb784354a921525825868e0273f5c941f595 -size 36987 +oid sha256:389b46a2546a221927a0b9f632b10e347fd30fa088b7d26dd0f8a844b981e110 +size 37997 From 10afe7c18c64829a9050b109c35a65c202eaec20 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 9 Oct 2026 14:28:17 +0200 Subject: [PATCH 6/7] Release room list entries resources even if onControllerCreated is cancelled Use try/finally instead of the awaitClose block, so that the entries stream, the controller and the dynamic adapters are destroyed even if the suspending onControllerCreated callback is cancelled or throws before awaitClose is reached. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../matrix/impl/roomlist/RoomListExtensions.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt index 301fb44bea..2352f82c3b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt @@ -70,9 +70,13 @@ internal fun RoomListInterface.entriesFlow( listener = listener, ) val controller = result.controller() - controller.setFilter(initialFilterKind) - onControllerCreated(controller) - awaitClose { + // Use try/finally rather than awaitClose's block, so that the resources are released even if + // onControllerCreated is cancelled or throws before awaitClose is reached + try { + controller.setFilter(initialFilterKind) + onControllerCreated(controller) + awaitClose() + } finally { result.entriesStream().cancelAndDestroy() controller.destroy() result.destroy() From 85224988af726eddf854b014ff457bf92fb79560 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 9 Oct 2026 14:29:28 +0200 Subject: [PATCH 7/7] Room select: address review remarks - Scroll the list to the top using a LaunchedEffect instead of recreating the LazyListState on each search query change. - Use isBlank to check the search query, to match the logic of RoomSelectSearchDataSource. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../libraries/roomselect/impl/RoomSelectView.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt index 4c5ad390e7..dcae5b9f87 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt @@ -144,15 +144,19 @@ fun RoomSelectView( ) val searchQuery = state.searchQuery.text.toString() - // Use a new list state when the selected account or the search query changes, so that the list of rooms is scrolled to the top - val lazyListState = key(state.selectedAccount.userId, searchQuery) { rememberLazyListState() } + // Use a new list state when the selected account changes, so that the list of rooms is scrolled to the top + val lazyListState = key(state.selectedAccount.userId) { rememberLazyListState() } + // Scroll the list of rooms to the top when the search query changes + LaunchedEffect(lazyListState, searchQuery) { + lazyListState.scrollToItem(0) + } // Hide the account switch section when the room list is scrolled up, and show it again as soon as it is scrolled down. // Use a new state when the selected account changes, so that the section is fully displayed again. val accountSwitchScrollBehavior = key(state.selectedAccount.userId) { TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()) } // Collapse the account switch section when the user starts searching. It can be displayed again by scrolling down. - val isSearching = searchQuery.isNotEmpty() + val isSearching = searchQuery.isNotBlank() LaunchedEffect(isSearching) { if (isSearching) { val topAppBarState = accountSwitchScrollBehavior.state @@ -246,7 +250,7 @@ fun RoomSelectView( ) { if (state.resultState.results.isNotEmpty()) { item { - val headerText = if (searchQuery.isEmpty()) { + val headerText = if (searchQuery.isBlank()) { stringResource(CommonStrings.common_header_rooms) } else { stringResource(CommonStrings.common_results_for, searchQuery)