User search : show a loader when fetching results
This commit is contained in:
parent
c6d109c424
commit
690a165411
21 changed files with 135 additions and 81 deletions
|
|
@ -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<ImmutableList<UserSearchResult>>,
|
||||
isSearching: Boolean,
|
||||
selectedUsers: ImmutableList<MatrixUser>,
|
||||
active: Boolean,
|
||||
isMultiSelectionEnabled: Boolean,
|
||||
|
|
@ -99,6 +101,11 @@ fun SearchUserBar(
|
|||
)
|
||||
}
|
||||
},
|
||||
contentSuffix = {
|
||||
if (isSearching) {
|
||||
AsyncLoading()
|
||||
}
|
||||
},
|
||||
resultState = state,
|
||||
resultHandler = { users ->
|
||||
LazyColumn(state = columnState) {
|
||||
|
|
|
|||
|
|
@ -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)) },
|
||||
|
|
|
|||
|
|
@ -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<ImmutableList<UserSearchResult>> 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) {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
data class UserListState(
|
||||
val searchQuery: String,
|
||||
val searchResults: SearchBarResultState<ImmutableList<UserSearchResult>>,
|
||||
val isFetchingSearchResults: Boolean,
|
||||
val selectedUsers: ImmutableList<MatrixUser>,
|
||||
val isSearchActive: Boolean,
|
||||
val selectionMode: SelectionMode,
|
||||
|
|
|
|||
|
|
@ -51,17 +51,19 @@ open class UserListStateProvider : PreviewParameterProvider<UserListState> {
|
|||
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 = {}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<ImmutableList<RoomMember>>>(Async.Loading()) }
|
||||
val selectedUsers = remember { mutableStateOf<ImmutableList<MatrixUser>>(persistentListOf()) }
|
||||
val searchResults = remember { mutableStateOf<SearchBarResultState<ImmutableList<InvitableUser>>>(SearchBarResultState.NotSearching()) }
|
||||
val searchResults = remember { mutableStateOf<SearchBarResultState<ImmutableList<InvitableUser>>>(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<SearchBarResultState<ImmutableList<InvitableUser>>>,
|
||||
roomMembers: MutableState<Async<ImmutableList<RoomMember>>>,
|
||||
selectedUsers: MutableState<ImmutableList<MatrixUser>>,
|
||||
isFetchingSearchResults: MutableState<Boolean>,
|
||||
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<Async<ImmutableList<RoomMember>>>) {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
data class RoomInviteMembersState(
|
||||
val canInvite: Boolean,
|
||||
val searchQuery: String,
|
||||
val isFetchingSearchResults: Boolean,
|
||||
val searchResults: SearchBarResultState<ImmutableList<InvitableUser>>,
|
||||
val selectedUsers: ImmutableList<MatrixUser>,
|
||||
val isSearchActive: Boolean,
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ internal class RoomInviteMembersStateProvider : PreviewParameterProvider<RoomInv
|
|||
aRoomInviteMembersState(canInvite = true, selectedUsers = aMatrixUserList().toImmutableList()),
|
||||
aRoomInviteMembersState(isSearchActive = true, searchQuery = "some query"),
|
||||
aRoomInviteMembersState(isSearchActive = true, searchQuery = "some query", selectedUsers = aMatrixUserList().toImmutableList()),
|
||||
aRoomInviteMembersState(isSearchActive = true, searchQuery = "some query", searchResults = SearchBarResultState.NoResults()),
|
||||
aRoomInviteMembersState(isSearchActive = true, searchQuery = "some query", searchResults = SearchBarResultState.NoResultsFound()),
|
||||
aRoomInviteMembersState(
|
||||
isSearchActive = true,
|
||||
canInvite = true,
|
||||
|
|
@ -70,9 +70,10 @@ internal class RoomInviteMembersStateProvider : PreviewParameterProvider<RoomInv
|
|||
private fun aRoomInviteMembersState(
|
||||
canInvite: Boolean = false,
|
||||
searchQuery: String = "",
|
||||
searchResults: SearchBarResultState<ImmutableList<InvitableUser>> = SearchBarResultState.NotSearching(),
|
||||
searchResults: SearchBarResultState<ImmutableList<InvitableUser>> = SearchBarResultState.Empty(),
|
||||
selectedUsers: ImmutableList<MatrixUser> = 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 = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ImmutableList<InvitableUser>>,
|
||||
isSearching: Boolean,
|
||||
selectedUsers: ImmutableList<MatrixUser>,
|
||||
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),
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ class RoomMemberListPresenter @Inject constructor(
|
|||
var roomMembers by remember { mutableStateOf<Async<RoomMembers>>(Async.Loading()) }
|
||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||
var searchResults by remember {
|
||||
mutableStateOf<SearchBarResultState<RoomMembers>>(SearchBarResultState.NotSearching())
|
||||
mutableStateOf<SearchBarResultState<RoomMembers>>(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(),
|
||||
|
|
|
|||
|
|
@ -53,14 +53,14 @@ internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMember
|
|||
aRoomMemberListState().copy(
|
||||
isSearchActive = true,
|
||||
searchQuery = "something-with-no-results",
|
||||
searchResults = SearchBarResultState.NoResults()
|
||||
searchResults = SearchBarResultState.NoResultsFound()
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aRoomMemberListState(
|
||||
roomMembers: Async<RoomMembers> = Async.Uninitialized,
|
||||
searchResults: SearchBarResultState<RoomMembers> = SearchBarResultState.NotSearching(),
|
||||
searchResults: SearchBarResultState<RoomMembers> = SearchBarResultState.Empty(),
|
||||
) = RoomMemberListState(
|
||||
roomMembers = roomMembers,
|
||||
searchQuery = "",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue