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"