From 690a165411485f7ca19bd76f0c6d4ab03c1922cc Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 4 Jan 2024 21:27:44 +0100 Subject: [PATCH] User search : show a loader when fetching results --- .../impl/components/SearchUserBar.kt | 7 +++ .../impl/components/UserListView.kt | 1 + .../impl/userlist/DefaultUserListPresenter.kt | 20 ++++--- .../createroom/impl/userlist/UserListState.kt | 1 + .../impl/userlist/UserListStateProvider.kt | 6 +- .../userlist/DefaultUserListPresenterTests.kt | 10 ++-- .../impl/invite/RoomInviteMembersPresenter.kt | 30 +++++++--- .../impl/invite/RoomInviteMembersState.kt | 1 + .../invite/RoomInviteMembersStateProvider.kt | 6 +- .../impl/invite/RoomInviteMembersView.kt | 8 +++ .../impl/members/RoomMemberListPresenter.kt | 6 +- .../members/RoomMemberListStateProvider.kt | 4 +- .../invite/RoomInviteMembersPresenterTest.kt | 4 +- .../members/RoomMemberListPresenterTests.kt | 4 +- .../theme/components/SearchBar.kt | 31 +++++----- .../roomselect/impl/RoomSelectPresenter.kt | 4 +- .../impl/RoomSelectStateProvider.kt | 2 +- .../impl/RoomSelectPresenterTests.kt | 6 +- .../usersearch/api/UserRepository.kt | 2 +- .../usersearch/api/UserSearchResult.kt | 5 ++ .../usersearch/impl/MatrixUserRepository.kt | 58 +++++++++++-------- 21 files changed, 135 insertions(+), 81 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchUserBar.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchUserBar.kt index bc562627cc..a9292e5a24 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchUserBar.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchUserBar.kt @@ -35,6 +35,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.components.async.AsyncLoading import io.element.android.libraries.designsystem.theme.components.HorizontalDivider import io.element.android.libraries.designsystem.theme.components.SearchBar import io.element.android.libraries.designsystem.theme.components.SearchBarResultState @@ -49,6 +50,7 @@ import kotlinx.collections.immutable.ImmutableList fun SearchUserBar( query: String, state: SearchBarResultState>, + isSearching: Boolean, selectedUsers: ImmutableList, active: Boolean, isMultiSelectionEnabled: Boolean, @@ -99,6 +101,11 @@ fun SearchUserBar( ) } }, + contentSuffix = { + if (isSearching) { + AsyncLoading() + } + }, resultState = state, resultHandler = { users -> LazyColumn(state = columnState) { diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/UserListView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/UserListView.kt index 45d898577f..8d5a3690db 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/UserListView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/UserListView.kt @@ -48,6 +48,7 @@ fun UserListView( state = state.searchResults, selectedUsers = state.selectedUsers, active = state.isSearchActive, + isSearching = state.isFetchingSearchResults, isMultiSelectionEnabled = state.isMultiSelectionEnabled, showBackButton = showBackButton, onActiveChanged = { state.eventSink(UserListEvents.OnSearchActiveChanged(it)) }, diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenter.kt index 867fdc9a60..19a858dd78 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenter.kt @@ -34,6 +34,8 @@ import io.element.android.libraries.usersearch.api.UserRepository import io.element.android.libraries.usersearch.api.UserSearchResult import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach class DefaultUserListPresenter @AssistedInject constructor( @Assisted val args: UserListPresenterArgs, @@ -57,18 +59,21 @@ class DefaultUserListPresenter @AssistedInject constructor( val selectedUsers by userListDataStore.selectedUsers().collectAsState(emptyList()) var searchQuery by rememberSaveable { mutableStateOf("") } var searchResults: SearchBarResultState> by remember { - mutableStateOf(SearchBarResultState.NotSearching()) + mutableStateOf(SearchBarResultState.Empty()) } + var isFetchingSearchResults by remember { mutableStateOf(false) } LaunchedEffect(searchQuery) { - searchResults = SearchBarResultState.NotSearching() - - userRepository.search(searchQuery).collect { + searchResults = SearchBarResultState.Empty() + isFetchingSearchResults = false + userRepository.search(searchQuery).onEach { state -> + isFetchingSearchResults = state.isFetchingSearchResults searchResults = when { - it.isEmpty() -> SearchBarResultState.NoResults() - else -> SearchBarResultState.Results(it.toImmutableList()) + state.results.isEmpty() && state.isFetchingSearchResults -> SearchBarResultState.Empty() + state.results.isEmpty() && !state.isFetchingSearchResults -> SearchBarResultState.NoResultsFound() + else -> SearchBarResultState.Results(state.results.toImmutableList()) } - } + }.launchIn(this) } return UserListState( @@ -76,6 +81,7 @@ class DefaultUserListPresenter @AssistedInject constructor( searchResults = searchResults, selectedUsers = selectedUsers.toImmutableList(), isSearchActive = isSearchActive, + isFetchingSearchResults = isFetchingSearchResults, selectionMode = args.selectionMode, eventSink = { event -> when (event) { diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListState.kt index 60a5bea506..5d59735e46 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListState.kt @@ -24,6 +24,7 @@ import kotlinx.collections.immutable.ImmutableList data class UserListState( val searchQuery: String, val searchResults: SearchBarResultState>, + val isFetchingSearchResults: Boolean, val selectedUsers: ImmutableList, val isSearchActive: Boolean, val selectionMode: SelectionMode, diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListStateProvider.kt index 31d1f6953a..ec7fba57b5 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListStateProvider.kt @@ -51,17 +51,19 @@ open class UserListStateProvider : PreviewParameterProvider { aUserListState().copy( isSearchActive = true, searchQuery = "something-with-no-results", - searchResults = SearchBarResultState.NoResults() + searchResults = SearchBarResultState.NoResultsFound() ), + aUserListState().copy(isSearchActive = true, searchQuery = "someone", selectionMode = SelectionMode.Single), ) } fun aUserListState() = UserListState( isSearchActive = false, searchQuery = "", - searchResults = SearchBarResultState.NotSearching(), + searchResults = SearchBarResultState.Empty(), selectedUsers = persistentListOf(), selectionMode = SelectionMode.Single, + isFetchingSearchResults = false, eventSink = {} ) diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenterTests.kt index a591a7a8e9..7f5eff5c30 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenterTests.kt @@ -55,7 +55,7 @@ class DefaultUserListPresenterTests { assertThat(initialState.isMultiSelectionEnabled).isFalse() assertThat(initialState.isSearchActive).isFalse() assertThat(initialState.selectedUsers).isEmpty() - assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.NotSearching::class.java) + assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Empty::class.java) } } @@ -76,7 +76,7 @@ class DefaultUserListPresenterTests { assertThat(initialState.isMultiSelectionEnabled).isTrue() assertThat(initialState.isSearchActive).isFalse() assertThat(initialState.selectedUsers).isEmpty() - assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.NotSearching::class.java) + assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Empty::class.java) } } @@ -131,7 +131,7 @@ class DefaultUserListPresenterTests { val initialState = awaitItem() initialState.eventSink(UserListEvents.UpdateSearchQuery("alice")) - assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.NotSearching::class.java) + assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Empty::class.java) assertThat(userRepository.providedQuery).isEqualTo("alice") skipItems(2) @@ -170,13 +170,13 @@ class DefaultUserListPresenterTests { val initialState = awaitItem() initialState.eventSink(UserListEvents.UpdateSearchQuery("alice")) - assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.NotSearching::class.java) + assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Empty::class.java) assertThat(userRepository.providedQuery).isEqualTo("alice") skipItems(2) // When the results list is empty, the state is set to NoResults userRepository.emitResult(emptyList()) - assertThat(awaitItem().searchResults).isInstanceOf(SearchBarResultState.NoResults::class.java) + assertThat(awaitItem().searchResults).isInstanceOf(SearchBarResultState.NoResultsFound::class.java) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenter.kt index cc0886c3af..4cd540bd35 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenter.kt @@ -37,6 +37,8 @@ import io.element.android.libraries.usersearch.api.UserRepository import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext import javax.inject.Inject @@ -50,16 +52,22 @@ class RoomInviteMembersPresenter @Inject constructor( override fun present(): RoomInviteMembersState { val roomMembers = remember { mutableStateOf>>(Async.Loading()) } val selectedUsers = remember { mutableStateOf>(persistentListOf()) } - val searchResults = remember { mutableStateOf>>(SearchBarResultState.NotSearching()) } + val searchResults = remember { mutableStateOf>>(SearchBarResultState.Empty()) } var searchQuery by rememberSaveable { mutableStateOf("") } var searchActive by rememberSaveable { mutableStateOf(false) } + var isFetchingSearchResults = rememberSaveable { mutableStateOf(false) } LaunchedEffect(Unit) { fetchMembers(roomMembers) } - LaunchedEffect(searchQuery, roomMembers) { - performSearch(searchResults, roomMembers, selectedUsers, searchQuery) + performSearch( + searchResults = searchResults, + roomMembers = roomMembers, + selectedUsers = selectedUsers, + isFetchingSearchResults = isFetchingSearchResults, + searchQuery = searchQuery + ) } return RoomInviteMembersState( @@ -68,6 +76,7 @@ class RoomInviteMembersPresenter @Inject constructor( searchQuery = searchQuery, isSearchActive = searchActive, searchResults = searchResults.value, + isFetchingSearchResults = isFetchingSearchResults.value, eventSink = { when (it) { is RoomInviteMembersEvents.OnSearchActiveChanged -> { @@ -117,16 +126,19 @@ class RoomInviteMembersPresenter @Inject constructor( searchResults: MutableState>>, roomMembers: MutableState>>, selectedUsers: MutableState>, + isFetchingSearchResults: MutableState, searchQuery: String, ) = withContext(coroutineDispatchers.io) { - searchResults.value = SearchBarResultState.NotSearching() - + searchResults.value = SearchBarResultState.Empty() + isFetchingSearchResults.value = false val joinedMembers = roomMembers.value.dataOrNull().orEmpty() - userRepository.search(searchQuery).collect { + userRepository.search(searchQuery).onEach { state -> + isFetchingSearchResults.value = state.isFetchingSearchResults searchResults.value = when { - it.isEmpty() -> SearchBarResultState.NoResults() - else -> SearchBarResultState.Results(it.map { result -> + state.results.isEmpty() && state.isFetchingSearchResults -> SearchBarResultState.Empty() + state.results.isEmpty() && !state.isFetchingSearchResults -> SearchBarResultState.NoResultsFound() + else -> SearchBarResultState.Results(state.results.map { result -> val existingMembership = joinedMembers.firstOrNull { j -> j.userId == result.matrixUser.userId }?.membership val isJoined = existingMembership == RoomMembershipState.JOIN val isInvited = existingMembership == RoomMembershipState.INVITE @@ -139,7 +151,7 @@ class RoomInviteMembersPresenter @Inject constructor( ) }.toImmutableList()) } - } + }.launchIn(this) } private suspend fun fetchMembers(roomMembers: MutableState>>) { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersState.kt index 16436debf0..1fb2dc1e78 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersState.kt @@ -23,6 +23,7 @@ import kotlinx.collections.immutable.ImmutableList data class RoomInviteMembersState( val canInvite: Boolean, val searchQuery: String, + val isFetchingSearchResults: Boolean, val searchResults: SearchBarResultState>, val selectedUsers: ImmutableList, val isSearchActive: Boolean, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersStateProvider.kt index f44d518fb5..c9ae8a606a 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersStateProvider.kt @@ -32,7 +32,7 @@ internal class RoomInviteMembersStateProvider : PreviewParameterProvider> = SearchBarResultState.NotSearching(), + searchResults: SearchBarResultState> = SearchBarResultState.Empty(), selectedUsers: ImmutableList = persistentListOf(), isSearchActive: Boolean = false, + isFetchingSearchResults: Boolean = false, ): RoomInviteMembersState { return RoomInviteMembersState( canInvite = canInvite, @@ -80,6 +81,7 @@ private fun aRoomInviteMembersState( searchResults = searchResults, selectedUsers = selectedUsers, isSearchActive = isSearchActive, + isFetchingSearchResults = isFetchingSearchResults, eventSink = {}, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt index 6c19b45686..73f6de54a8 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt @@ -50,6 +50,7 @@ import io.element.android.libraries.matrix.ui.components.SelectedUsersList import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getBestName import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.designsystem.components.async.AsyncLoading import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList @@ -86,6 +87,7 @@ fun RoomInviteMembersView( RoomInviteMembersSearchBar( modifier = Modifier.fillMaxWidth(), query = state.searchQuery, + isSearching = state.isFetchingSearchResults, selectedUsers = state.selectedUsers, state = state.searchResults, active = state.isSearchActive, @@ -139,6 +141,7 @@ private fun RoomInviteMembersTopBar( private fun RoomInviteMembersSearchBar( query: String, state: SearchBarResultState>, + isSearching: Boolean, selectedUsers: ImmutableList, active: Boolean, onActiveChanged: (Boolean) -> Unit, @@ -167,6 +170,11 @@ private fun RoomInviteMembersSearchBar( }, showBackButton = false, resultState = state, + contentSuffix = { + if (isSearching) { + AsyncLoading() + } + }, resultHandler = { results -> Text( text = stringResource(id = CommonStrings.common_search_results), diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt index 49ae479d40..e779af5f61 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt @@ -47,7 +47,7 @@ class RoomMemberListPresenter @Inject constructor( var roomMembers by remember { mutableStateOf>(Async.Loading()) } var searchQuery by rememberSaveable { mutableStateOf("") } var searchResults by remember { - mutableStateOf>(SearchBarResultState.NotSearching()) + mutableStateOf>(SearchBarResultState.Empty()) } var isSearchActive by rememberSaveable { mutableStateOf(false) } @@ -71,10 +71,10 @@ class RoomMemberListPresenter @Inject constructor( LaunchedEffect(searchQuery) { withContext(coroutineDispatchers.io) { searchResults = if (searchQuery.isEmpty()) { - SearchBarResultState.NotSearching() + SearchBarResultState.Empty() } else { val results = roomMemberListDataSource.search(searchQuery).groupBy { it.membership } - if (results.isEmpty()) SearchBarResultState.NoResults() + if (results.isEmpty()) SearchBarResultState.NoResultsFound() else SearchBarResultState.Results( RoomMembers( invited = results.getOrDefault(RoomMembershipState.INVITE, emptyList()).toImmutableList(), diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt index 4954af8efc..f6eefa4385 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt @@ -53,14 +53,14 @@ internal class RoomMemberListStateProvider : PreviewParameterProvider = Async.Uninitialized, - searchResults: SearchBarResultState = SearchBarResultState.NotSearching(), + searchResults: SearchBarResultState = SearchBarResultState.Empty(), ) = RoomMemberListState( roomMembers = roomMembers, searchQuery = "", diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt index 9763ed280b..7ecdccf7f1 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt @@ -63,7 +63,7 @@ internal class RoomInviteMembersPresenterTest { }.test { val initialState = awaitItem() - assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.NotSearching::class.java) + assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Empty::class.java) assertThat(initialState.isSearchActive).isFalse() assertThat(initialState.canInvite).isFalse() assertThat(initialState.searchQuery).isEmpty() @@ -115,7 +115,7 @@ internal class RoomInviteMembersPresenterTest { skipItems(1) val resultState = awaitItem() - assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.NoResults::class.java) + assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.NoResultsFound::class.java) } } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt index 097919a5e1..3a2620cad0 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt @@ -55,7 +55,7 @@ class RoomMemberListPresenterTests { val initialState = awaitItem() assertThat(initialState.roomMembers).isInstanceOf(Async.Loading::class.java) assertThat(initialState.searchQuery).isEmpty() - assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.NotSearching::class.java) + assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Empty::class.java) assertThat(initialState.isSearchActive).isFalse() val loadedState = awaitItem() assertThat(loadedState.roomMembers).isInstanceOf(Async.Success::class.java) @@ -92,7 +92,7 @@ class RoomMemberListPresenterTests { val searchQueryUpdatedState = awaitItem() assertThat(searchQueryUpdatedState.searchQuery).isEqualTo("something") val searchSearchResultDelivered = awaitItem() - assertThat(searchSearchResultDelivered.searchResults).isInstanceOf(SearchBarResultState.NoResults::class.java) + assertThat(searchSearchResultDelivered.searchResults).isInstanceOf(SearchBarResultState.NoResultsFound::class.java) } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt index 7e208b88d9..c53fbb6cb6 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt @@ -59,7 +59,7 @@ fun SearchBar( modifier: Modifier = Modifier, enabled: Boolean = true, showBackButton: Boolean = true, - resultState: SearchBarResultState = SearchBarResultState.NotSearching(), + resultState: SearchBarResultState = SearchBarResultState.Empty(), shape: Shape = SearchBarDefaults.inputFieldShape, tonalElevation: Dp = SearchBarDefaults.TonalElevation, windowInsets: WindowInsets = SearchBarDefaults.windowInsets, @@ -129,7 +129,7 @@ fun SearchBar( resultHandler(resultState.results) } - is SearchBarResultState.NoResults -> { + is SearchBarResultState.NoResultsFound -> { // No results found, show a message Spacer(Modifier.size(80.dp)) @@ -184,10 +184,10 @@ object ElementSearchBarDefaults { @Immutable sealed interface SearchBarResultState { /** No search results are available yet (e.g. because the user hasn't entered a search term). */ - class NotSearching : SearchBarResultState + class Empty : SearchBarResultState /** The search has completed, but no results were found. */ - class NoResults : SearchBarResultState + class NoResultsFound : SearchBarResultState /** The search has completed, and some matching users were found. */ data class Results(val results: T) : SearchBarResultState @@ -199,7 +199,7 @@ internal fun SearchBarInactivePreview() = ElementThemedPreview { ContentToPrevie @Preview(group = PreviewGroup.Search) @Composable -internal fun SearchBarActiveEmptyQueryPreview() = ElementThemedPreview { +internal fun SearchBarActiveNoneQueryPreview() = ElementThemedPreview { ContentToPreview( query = "", active = true, @@ -231,7 +231,7 @@ internal fun SearchBarActiveWithNoResultsPreview() = ElementThemedPreview { ContentToPreview( query = "search term", active = true, - resultState = SearchBarResultState.NoResults(), + resultState = SearchBarResultState.NoResultsFound(), ) } @@ -257,16 +257,15 @@ internal fun SearchBarActiveWithContentPreview() = ElementThemedPreview { .background(color = Color.Blue) .fillMaxWidth() ) - }, - resultHandler = { - Text( - text = "Results go here", - modifier = Modifier - .background(color = Color.Green) - .fillMaxWidth() - ) } - ) + ) { + Text( + text = "Results go here", + modifier = Modifier + .background(color = Color.Green) + .fillMaxWidth() + ) + } } @OptIn(ExperimentalMaterial3Api::class) @@ -275,7 +274,7 @@ private fun ContentToPreview( query: String = "", active: Boolean = false, showBackButton: Boolean = true, - resultState: SearchBarResultState = SearchBarResultState.NotSearching(), + resultState: SearchBarResultState = SearchBarResultState.Empty(), contentPrefix: @Composable ColumnScope.() -> Unit = {}, contentSuffix: @Composable ColumnScope.() -> Unit = {}, resultHandler: @Composable ColumnScope.(String) -> Unit = {}, 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 16cd7b813d..1bc1e8a4b5 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 @@ -51,7 +51,7 @@ class RoomSelectPresenter @AssistedInject constructor( var selectedRooms by remember { mutableStateOf(persistentListOf()) } var query by remember { mutableStateOf("") } var isSearchActive by remember { mutableStateOf(false) } - var results: SearchBarResultState> by remember { mutableStateOf(SearchBarResultState.NotSearching()) } + var results: SearchBarResultState> by remember { mutableStateOf(SearchBarResultState.Empty()) } val summaries by client.roomListService.allRooms.summaries.collectAsState() @@ -64,7 +64,7 @@ class RoomSelectPresenter @AssistedInject constructor( results = if (filteredSummaries.isNotEmpty()) { SearchBarResultState.Results(filteredSummaries) } else { - SearchBarResultState.NoResults() + SearchBarResultState.NoResultsFound() } } diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStateProvider.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStateProvider.kt index d06e0be6d6..8cc082d702 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStateProvider.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStateProvider.kt @@ -48,7 +48,7 @@ open class RoomSelectStateProvider : PreviewParameterProvider { } private fun aRoomSelectState( - resultState: SearchBarResultState> = SearchBarResultState.NotSearching(), + resultState: SearchBarResultState> = SearchBarResultState.Empty(), query: String = "", isSearchActive: Boolean = false, selectedRooms: ImmutableList = persistentListOf(), diff --git a/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTests.kt b/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTests.kt index 88ae560bb2..9c6e7a0adb 100644 --- a/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTests.kt +++ b/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTests.kt @@ -45,11 +45,11 @@ class RoomSelectPresenterTests { }.test { val initialState = awaitItem() assertThat(initialState.selectedRooms).isEmpty() - assertThat(initialState.resultState).isInstanceOf(SearchBarResultState.NotSearching::class.java) + assertThat(initialState.resultState).isInstanceOf(SearchBarResultState.Empty::class.java) assertThat(initialState.isSearchActive).isFalse() // Search is run automatically val searchState = awaitItem() - assertThat(searchState.resultState).isInstanceOf(SearchBarResultState.NoResults::class.java) + assertThat(searchState.resultState).isInstanceOf(SearchBarResultState.NoResultsFound::class.java) } } @@ -85,7 +85,7 @@ class RoomSelectPresenterTests { initialState.eventSink(RoomSelectEvents.UpdateQuery("string not contained")) assertThat(awaitItem().query).isEqualTo("string not contained") - assertThat(awaitItem().resultState).isInstanceOf(SearchBarResultState.NoResults::class.java) + assertThat(awaitItem().resultState).isInstanceOf(SearchBarResultState.NoResultsFound::class.java) } } diff --git a/libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserRepository.kt b/libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserRepository.kt index 03e9952c92..9b0af6d2b0 100644 --- a/libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserRepository.kt +++ b/libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserRepository.kt @@ -20,5 +20,5 @@ import kotlinx.coroutines.flow.Flow interface UserRepository { - suspend fun search(query: String): Flow> + fun search(query: String): Flow } diff --git a/libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserSearchResult.kt b/libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserSearchResult.kt index e67a7af46f..c3f7b7b966 100644 --- a/libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserSearchResult.kt +++ b/libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserSearchResult.kt @@ -22,3 +22,8 @@ data class UserSearchResult( val matrixUser: MatrixUser, val isUnresolved: Boolean = false, ) + +data class UserSearchResultsState( + val results: List, + val isFetchingSearchResults: Boolean +) diff --git a/libraries/usersearch/impl/src/main/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserRepository.kt b/libraries/usersearch/impl/src/main/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserRepository.kt index 0714528836..ca06a6e83a 100644 --- a/libraries/usersearch/impl/src/main/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserRepository.kt +++ b/libraries/usersearch/impl/src/main/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserRepository.kt @@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.usersearch.api.UserListDataSource import io.element.android.libraries.usersearch.api.UserRepository import io.element.android.libraries.usersearch.api.UserSearchResult +import io.element.android.libraries.usersearch.api.UserSearchResultsState import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow @@ -36,36 +37,45 @@ class MatrixUserRepository @Inject constructor( private val dataSource: UserListDataSource ) : UserRepository { - override suspend fun search(query: String): Flow> = flow { - // If the search term is a MXID that's not ours, we'll show a 'fake' result for that user, then update it when we get search results. + override fun search(query: String): Flow = flow { val shouldQueryProfile = MatrixPatterns.isUserId(query) && !client.isMe(UserId(query)) - if (shouldQueryProfile) { - emit(listOf(UserSearchResult(MatrixUser(UserId(query))))) + val shouldFetchSearchResults = query.length >= MINIMUM_SEARCH_LENGTH + // If the search term is a MXID that's not ours, we'll show a 'fake' result for that user, then update it when we get search results. + val fakeSearchResult = if (shouldQueryProfile) { + UserSearchResult(MatrixUser(UserId(query)), isUnresolved = true) + } else { + null } - - if (query.length >= MINIMUM_SEARCH_LENGTH) { - // Debounce - delay(DEBOUNCE_TIME_MILLIS) - - val results = dataSource - .search(query, MAXIMUM_SEARCH_RESULTS) - .filter { !client.isMe(it.userId) } - .map { UserSearchResult(it) } - .toMutableList() - - // If the query is another user's MXID and the result doesn't contain that user ID, query the profile information explicitly - if (shouldQueryProfile && results.none { it.matrixUser.userId.value == query }) { - results.add( - 0, - dataSource.getProfile(UserId(query)) - ?.let { UserSearchResult(it) } - ?: UserSearchResult(MatrixUser(UserId(query)), isUnresolved = true)) - } - + if (shouldQueryProfile || shouldFetchSearchResults) { + emit(UserSearchResultsState(isFetchingSearchResults = shouldFetchSearchResults, results = listOfNotNull(fakeSearchResult))) + } + if (shouldFetchSearchResults) { + val results = fetchSearchResults(query, shouldQueryProfile) emit(results) } } + private suspend fun fetchSearchResults(query: String, shouldQueryProfile: Boolean): UserSearchResultsState { + // Debounce + delay(DEBOUNCE_TIME_MILLIS) + val results = dataSource + .search(query, MAXIMUM_SEARCH_RESULTS) + .filter { !client.isMe(it.userId) } + .map { UserSearchResult(it) } + .toMutableList() + + // If the query is another user's MXID and the result doesn't contain that user ID, query the profile information explicitly + if (shouldQueryProfile && results.none { it.matrixUser.userId.value == query }) { + results.add( + 0, + dataSource.getProfile(UserId(query)) + ?.let { UserSearchResult(it) } + ?: UserSearchResult(MatrixUser(UserId(query)), isUnresolved = true)) + } + + return UserSearchResultsState(results = results, isFetchingSearchResults = false) + } + companion object { private const val DEBOUNCE_TIME_MILLIS = 250L private const val MINIMUM_SEARCH_LENGTH = 3