mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-03 16:10:00 +00:00
Room select: update empty search UI and behavior.
This commit is contained in:
+1
-3
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
package io.element.android.libraries.roomselect.impl
|
package io.element.android.libraries.roomselect.impl
|
||||||
|
|
||||||
import androidx.compose.foundation.text.input.clearText
|
|
||||||
import androidx.compose.foundation.text.input.rememberTextFieldState
|
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -154,9 +153,8 @@ class RoomSelectPresenter(
|
|||||||
RoomSelectEvent.ToggleAccountListExpanded -> isAccountListExpanded = !isAccountListExpanded
|
RoomSelectEvent.ToggleAccountListExpanded -> isAccountListExpanded = !isAccountListExpanded
|
||||||
is RoomSelectEvent.SelectAccount -> {
|
is RoomSelectEvent.SelectAccount -> {
|
||||||
selectedSessionId = event.sessionId
|
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()
|
selectedRooms = persistentListOf()
|
||||||
queryState.clearText()
|
|
||||||
isAccountListExpanded = false
|
isAccountListExpanded = false
|
||||||
}
|
}
|
||||||
is RoomSelectEvent.UpdateVisibleRange -> coroutineScope.launch {
|
is RoomSelectEvent.UpdateVisibleRange -> coroutineScope.launch {
|
||||||
|
|||||||
+57
-9
@@ -10,12 +10,14 @@ package io.element.android.libraries.roomselect.impl
|
|||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.animate
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Arrangement.spacedBy
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
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.WindowInsetsSides
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.only
|
import androidx.compose.foundation.layout.only
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.safeDrawing
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
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.TopAppBarDefaults
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.key
|
import androidx.compose.runtime.key
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -46,6 +53,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.heading
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.semantics.stateDescription
|
import androidx.compose.ui.semantics.stateDescription
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
@@ -136,13 +144,27 @@ fun RoomSelectView(
|
|||||||
onBack = ::onBackButton,
|
onBack = ::onBackButton,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Use a new list state when the selected account changes, so that the list of rooms is scrolled to the top
|
val searchQuery = state.searchQuery.text.toString()
|
||||||
val lazyListState = key(state.selectedAccount.userId) { rememberLazyListState() }
|
// 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.
|
// 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.
|
// Use a new state when the selected account changes, so that the section is fully displayed again.
|
||||||
val accountSwitchScrollBehavior = key(state.selectedAccount.userId) {
|
val accountSwitchScrollBehavior = key(state.selectedAccount.userId) {
|
||||||
TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState())
|
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 ->
|
OnVisibleRangeChangeEffect(lazyListState) { visibleRange ->
|
||||||
state.eventSink(RoomSelectEvent.UpdateVisibleRange(visibleRange))
|
state.eventSink(RoomSelectEvent.UpdateVisibleRange(visibleRange))
|
||||||
}
|
}
|
||||||
@@ -217,7 +239,7 @@ fun RoomSelectView(
|
|||||||
)
|
)
|
||||||
when {
|
when {
|
||||||
state.hasRoomListError -> RoomListError()
|
state.hasRoomListError -> RoomListError()
|
||||||
state.resultState is SearchBarResultState.NoResultsFound -> NoResultsFound()
|
state.resultState is SearchBarResultState.NoResultsFound -> NoResultsView(searchQuery = searchQuery)
|
||||||
state.resultState is SearchBarResultState.Results -> {
|
state.resultState is SearchBarResultState.Results -> {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = lazyListState,
|
state = lazyListState,
|
||||||
@@ -225,8 +247,13 @@ fun RoomSelectView(
|
|||||||
) {
|
) {
|
||||||
if (state.resultState.results.isNotEmpty()) {
|
if (state.resultState.results.isNotEmpty()) {
|
||||||
item {
|
item {
|
||||||
|
val headerText = if (searchQuery.isEmpty()) {
|
||||||
|
stringResource(CommonStrings.common_header_rooms)
|
||||||
|
} else {
|
||||||
|
stringResource(CommonStrings.common_results_for, searchQuery)
|
||||||
|
}
|
||||||
ListSectionHeader(
|
ListSectionHeader(
|
||||||
title = stringResource(CommonStrings.common_header_rooms),
|
title = headerText,
|
||||||
hasDivider = false,
|
hasDivider = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -250,15 +277,36 @@ fun RoomSelectView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun NoResultsFound() {
|
private fun NoResultsView(
|
||||||
|
searchQuery: String,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.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(
|
||||||
text = stringResource(CommonStrings.common_no_results),
|
text = stringResource(CommonStrings.common_no_results),
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
color = ElementTheme.colors.textSecondary,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.semantics {
|
||||||
.padding(horizontal = 16.dp, vertical = 80.dp),
|
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
|
@Composable
|
||||||
|
|||||||
+27
@@ -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
|
@Test
|
||||||
fun `present - forward mode with several accounts does not show the account switch`() = runTest {
|
fun `present - forward mode with several accounts does not show the account switch`() = runTest {
|
||||||
val presenter = createRoomSelectPresenter(
|
val presenter = createRoomSelectPresenter(
|
||||||
|
|||||||
@@ -335,6 +335,7 @@ Reason: %1$s."</string>
|
|||||||
<string name="common_mute">"Mute"</string>
|
<string name="common_mute">"Mute"</string>
|
||||||
<string name="common_name">"Name"</string>
|
<string name="common_name">"Name"</string>
|
||||||
<string name="common_name_and_id">"%1$s (%2$s)"</string>
|
<string name="common_name_and_id">"%1$s (%2$s)"</string>
|
||||||
|
<string name="common_no_results_for">"There are no results for “%1$s”. Try a new search term."</string>
|
||||||
<string name="common_no_results">"No results"</string>
|
<string name="common_no_results">"No results"</string>
|
||||||
<string name="common_no_room_name">"No room name"</string>
|
<string name="common_no_room_name">"No room name"</string>
|
||||||
<string name="common_no_space_name">"No space name"</string>
|
<string name="common_no_space_name">"No space name"</string>
|
||||||
@@ -381,6 +382,7 @@ Reason: %1$s."</string>
|
|||||||
<string name="common_replying_to">"Replying to %1$s"</string>
|
<string name="common_replying_to">"Replying to %1$s"</string>
|
||||||
<string name="common_report_a_problem">"Report a problem"</string>
|
<string name="common_report_a_problem">"Report a problem"</string>
|
||||||
<string name="common_report_submitted">"Report submitted"</string>
|
<string name="common_report_submitted">"Report submitted"</string>
|
||||||
|
<string name="common_results_for">"Results for “%1$s”"</string>
|
||||||
<string name="common_rich_text_editor">"Rich text editor"</string>
|
<string name="common_rich_text_editor">"Rich text editor"</string>
|
||||||
<string name="common_role">"Role"</string>
|
<string name="common_role">"Role"</string>
|
||||||
<string name="common_room">"Room"</string>
|
<string name="common_room">"Room"</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user