From 0c6da39acb5cf99eee4d63e79a05e9c140446153 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 8 Oct 2026 17:29:44 +0200 Subject: [PATCH] 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"