Search for users to start a new DM. (#376)
Search for users to start a new DM. Hooks up the create room UI to the matrix client to get search results. Searches are debounced for 500ms and only executed when 3 or more characters are entered. Wrap the result state so we can distinguish between "no results because we haven't searched yet" and "no results because the API returned nothing", and add a "No results found" message in the UI for the latter case. Closes #95
This commit is contained in:
parent
c411faa93d
commit
99f571b4eb
48 changed files with 432 additions and 71 deletions
|
|
@ -17,17 +17,37 @@
|
|||
package io.element.android.features.createroom.impl
|
||||
|
||||
import io.element.android.features.userlist.api.UserListDataSource
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.usersearch.MatrixUserProfile
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import javax.inject.Inject
|
||||
|
||||
// TODO this is empty as we currently don't have an endpoint to perform user search
|
||||
class AllMatrixUsersDataSource @Inject constructor() : UserListDataSource {
|
||||
class AllMatrixUsersDataSource @Inject constructor(
|
||||
private val client: MatrixClient
|
||||
) : UserListDataSource {
|
||||
override suspend fun search(query: String): List<MatrixUser> {
|
||||
return emptyList()
|
||||
val res = client.searchUsers(query, MAX_SEARCH_RESULTS)
|
||||
return res.getOrNull()?.results?.map(::toMatrixUser).orEmpty()
|
||||
}
|
||||
|
||||
override suspend fun getProfile(userId: UserId): MatrixUser? {
|
||||
// TODO hook up to matrix client
|
||||
return null
|
||||
}
|
||||
|
||||
private fun toMatrixUser(matrixUserProfile: MatrixUserProfile) = MatrixUser(
|
||||
id = matrixUserProfile.userId,
|
||||
username = matrixUserProfile.displayName,
|
||||
avatarData = AvatarData(
|
||||
id = matrixUserProfile.userId.value,
|
||||
name = matrixUserProfile.displayName,
|
||||
url = matrixUserProfile.avatarUrl,
|
||||
)
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val MAX_SEARCH_RESULTS = 5L
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package io.element.android.features.createroom.impl.addpeople
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.userlist.api.SelectionMode
|
||||
import io.element.android.features.userlist.api.UserListState
|
||||
import io.element.android.features.userlist.api.UserSearchResultState
|
||||
import io.element.android.features.userlist.api.aListOfSelectedUsers
|
||||
import io.element.android.features.userlist.api.aUserListState
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
|
|
@ -29,13 +30,13 @@ open class AddPeopleUserListStateProvider : PreviewParameterProvider<UserListSta
|
|||
get() = sequenceOf(
|
||||
aUserListState(),
|
||||
aUserListState().copy(
|
||||
searchResults = aMatrixUserList().toImmutableList(),
|
||||
searchResults = UserSearchResultState.Results(aMatrixUserList().toImmutableList()),
|
||||
selectedUsers = aListOfSelectedUsers(),
|
||||
isSearchActive = false,
|
||||
selectionMode = SelectionMode.Multiple,
|
||||
),
|
||||
aUserListState().copy(
|
||||
searchResults = aMatrixUserList().toImmutableList(),
|
||||
searchResults = UserSearchResultState.Results(aMatrixUserList().toImmutableList()),
|
||||
selectedUsers = aListOfSelectedUsers(),
|
||||
isSearchActive = true,
|
||||
selectionMode = SelectionMode.Multiple,
|
||||
|
|
|
|||
|
|
@ -21,11 +21,11 @@ import dagger.Binds
|
|||
import dagger.Module
|
||||
import io.element.android.features.createroom.impl.AllMatrixUsersDataSource
|
||||
import io.element.android.features.userlist.api.UserListDataSource
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import javax.inject.Named
|
||||
|
||||
@Module
|
||||
@ContributesTo(AppScope::class)
|
||||
@ContributesTo(SessionScope::class)
|
||||
interface CreateRoomModule {
|
||||
|
||||
@Binds
|
||||
|
|
|
|||
|
|
@ -46,7 +46,11 @@ class CreateRoomRootPresenter @Inject constructor(
|
|||
|
||||
private val presenter by lazy {
|
||||
presenterFactory.create(
|
||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||
UserListPresenterArgs(
|
||||
selectionMode = SelectionMode.Single,
|
||||
minimumSearchLength = 3,
|
||||
searchDebouncePeriodMillis = 500,
|
||||
),
|
||||
userListDataSource,
|
||||
userListDataStore,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.features.createroom.impl.root
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.userlist.api.UserSearchResultState
|
||||
import io.element.android.features.userlist.api.aUserListState
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
|
|
@ -31,7 +32,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRoot
|
|||
userListState = aMatrixUser().let {
|
||||
aUserListState().copy(
|
||||
searchQuery = it.id.value,
|
||||
searchResults = persistentListOf(it),
|
||||
searchResults = UserSearchResultState.Results(persistentListOf(it)),
|
||||
selectedUsers = persistentListOf(it),
|
||||
isSearchActive = true,
|
||||
)
|
||||
|
|
@ -42,7 +43,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRoot
|
|||
userListState = aMatrixUser().let {
|
||||
aUserListState().copy(
|
||||
searchQuery = it.id.value,
|
||||
searchResults = persistentListOf(it),
|
||||
searchResults = UserSearchResultState.Results(persistentListOf(it)),
|
||||
selectedUsers = persistentListOf(it),
|
||||
isSearchActive = true,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.createroom.impl
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.usersearch.MatrixSearchUserResults
|
||||
import io.element.android.libraries.matrix.api.usersearch.MatrixUserProfile
|
||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class AllMatrixUsersDataSourceTest {
|
||||
|
||||
@Test
|
||||
fun `search - returns users on success`() = runTest {
|
||||
val matrixClient = FakeMatrixClient()
|
||||
matrixClient.givenSearchUsersResult(
|
||||
searchTerm = "test",
|
||||
result = Result.success(
|
||||
MatrixSearchUserResults(
|
||||
results = listOf(aMatrixUserProfile(), aMatrixUserProfile(userId = A_USER_ID_2)),
|
||||
limited = false
|
||||
)
|
||||
)
|
||||
)
|
||||
val dataSource = AllMatrixUsersDataSource(matrixClient)
|
||||
|
||||
val results = dataSource.search("test")
|
||||
Truth.assertThat(results).containsExactly(
|
||||
MatrixUser(
|
||||
id = A_USER_ID,
|
||||
username = A_USER_NAME,
|
||||
avatarData = AvatarData(id = A_USER_ID.value, name = A_USER_NAME, url = AN_AVATAR_URL)
|
||||
),
|
||||
MatrixUser(
|
||||
id = A_USER_ID_2,
|
||||
username = A_USER_NAME,
|
||||
avatarData = AvatarData(id = A_USER_ID_2.value, name = A_USER_NAME, url = AN_AVATAR_URL)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `search - returns empty list on error`() = runTest {
|
||||
val matrixClient = FakeMatrixClient()
|
||||
matrixClient.givenSearchUsersResult(
|
||||
searchTerm = "test",
|
||||
result = Result.failure(Throwable("Ruhroh"))
|
||||
)
|
||||
val dataSource = AllMatrixUsersDataSource(matrixClient)
|
||||
|
||||
val results = dataSource.search("test")
|
||||
Truth.assertThat(results).isEmpty()
|
||||
}
|
||||
|
||||
private fun aMatrixUserProfile(
|
||||
userId: UserId = A_USER_ID,
|
||||
displayName: String = A_USER_NAME,
|
||||
avatarUrl: String = AN_AVATAR_URL
|
||||
) = MatrixUserProfile(userId, displayName, avatarUrl)
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.features.roomdetails.impl.members
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.userlist.api.UserSearchResultState
|
||||
import io.element.android.features.userlist.api.aUserListState
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
|
|
@ -33,7 +34,7 @@ internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMember
|
|||
}
|
||||
|
||||
internal fun aRoomMemberListState(
|
||||
searchResults: ImmutableList<MatrixUser> = persistentListOf(),
|
||||
searchResults: UserSearchResultState = UserSearchResultState.NotSearching,
|
||||
allUsers: Async<ImmutableList<MatrixUser>> = Async.Uninitialized,
|
||||
) =
|
||||
RoomMemberListState(
|
||||
|
|
|
|||
|
|
@ -26,15 +26,14 @@ import io.element.android.features.userlist.api.UserListDataSource
|
|||
import io.element.android.features.userlist.api.UserListDataStore
|
||||
import io.element.android.features.userlist.api.UserListPresenter
|
||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
||||
import io.element.android.features.userlist.api.UserSearchResultState
|
||||
import io.element.android.features.userlist.impl.DefaultUserListPresenter
|
||||
import io.element.android.features.userlist.test.FakeUserListDataSource
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.internal.toImmutableList
|
||||
import org.junit.Test
|
||||
|
|
@ -72,7 +71,7 @@ class RoomMemberListPresenterTests {
|
|||
val initialState = awaitItem()
|
||||
Truth.assertThat(initialState.allUsers).isInstanceOf(Async.Loading::class.java)
|
||||
Truth.assertThat(initialState.userListState.isSearchActive).isFalse()
|
||||
Truth.assertThat(initialState.userListState.searchResults).isEmpty()
|
||||
Truth.assertThat(initialState.userListState.searchResults).isEqualTo(UserSearchResultState.NotSearching)
|
||||
Truth.assertThat(initialState.userListState.selectionMode).isEqualTo(SelectionMode.Single)
|
||||
|
||||
val loadedState = awaitItem()
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
alias(libs.plugins.ksp)
|
||||
}
|
||||
|
||||
android {
|
||||
|
|
@ -27,4 +28,5 @@ dependencies {
|
|||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
ksp(libs.showkase.processor)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ package io.element.android.features.userlist.api
|
|||
|
||||
data class UserListPresenterArgs(
|
||||
val selectionMode: SelectionMode,
|
||||
val minimumSearchLength: Int = 1,
|
||||
val searchDebouncePeriodMillis: Long = 0,
|
||||
)
|
||||
|
||||
enum class SelectionMode {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
|
||||
data class UserListState(
|
||||
val searchQuery: String,
|
||||
val searchResults: ImmutableList<MatrixUser>,
|
||||
val searchResults: UserSearchResultState,
|
||||
val selectedUsers: ImmutableList<MatrixUser>,
|
||||
val isSearchActive: Boolean,
|
||||
val selectionMode: SelectionMode,
|
||||
|
|
@ -29,3 +29,14 @@ data class UserListState(
|
|||
) {
|
||||
val isMultiSelectionEnabled = selectionMode == SelectionMode.Multiple
|
||||
}
|
||||
|
||||
sealed interface UserSearchResultState {
|
||||
/** No search results are available yet (e.g. because the user hasn't entered a (long enough) search term). */
|
||||
object NotSearching : UserSearchResultState
|
||||
|
||||
/** The search has completed, but no results were found. */
|
||||
object NoResults : UserSearchResultState
|
||||
|
||||
/** The search has completed, and some matching users were found. */
|
||||
data class Results(val results: ImmutableList<MatrixUser>) : UserSearchResultState
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,22 +37,27 @@ open class UserListStateProvider : PreviewParameterProvider<UserListState> {
|
|||
isSearchActive = true,
|
||||
searchQuery = "@someone:matrix.org",
|
||||
selectedUsers = aListOfSelectedUsers(),
|
||||
searchResults = aMatrixUserList().toImmutableList(),
|
||||
searchResults = UserSearchResultState.Results(aMatrixUserList().toImmutableList()),
|
||||
),
|
||||
aUserListState().copy(
|
||||
isSearchActive = true,
|
||||
searchQuery = "@someone:matrix.org",
|
||||
selectionMode = SelectionMode.Multiple,
|
||||
selectedUsers = aListOfSelectedUsers(),
|
||||
searchResults = aMatrixUserList().toImmutableList(),
|
||||
)
|
||||
searchResults = UserSearchResultState.Results(aMatrixUserList().toImmutableList()),
|
||||
),
|
||||
aUserListState().copy(
|
||||
isSearchActive = true,
|
||||
searchQuery = "something-with-no-results",
|
||||
searchResults = UserSearchResultState.NoResults
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun aUserListState() = UserListState(
|
||||
isSearchActive = false,
|
||||
searchQuery = "",
|
||||
searchResults = persistentListOf(),
|
||||
searchResults = UserSearchResultState.NotSearching,
|
||||
selectedUsers = persistentListOf(),
|
||||
selectionMode = SelectionMode.Single,
|
||||
eventSink = {}
|
||||
|
|
|
|||
|
|
@ -17,14 +17,17 @@
|
|||
package io.element.android.features.userlist.api.components
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SearchBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -32,7 +35,9 @@ import androidx.compose.ui.draw.alpha
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.userlist.api.UserSearchResultState
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||
|
|
@ -46,7 +51,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
@Composable
|
||||
fun SearchUserBar(
|
||||
query: String,
|
||||
results: ImmutableList<MatrixUser>,
|
||||
state: UserSearchResultState,
|
||||
selectedUsers: ImmutableList<MatrixUser>,
|
||||
active: Boolean,
|
||||
isMultiSelectionEnabled: Boolean,
|
||||
|
|
@ -91,6 +96,7 @@ fun SearchUserBar(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
!active -> {
|
||||
{
|
||||
Icon(
|
||||
|
|
@ -100,6 +106,7 @@ fun SearchUserBar(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> null
|
||||
},
|
||||
colors = if (!active) SearchBarDefaults.colors() else SearchBarDefaults.colors(containerColor = Color.Transparent),
|
||||
|
|
@ -113,31 +120,43 @@ fun SearchUserBar(
|
|||
)
|
||||
}
|
||||
|
||||
LazyColumn {
|
||||
if (isMultiSelectionEnabled) {
|
||||
items(results) { matrixUser ->
|
||||
SearchMultipleUsersResultItem(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
matrixUser = matrixUser,
|
||||
isUserSelected = selectedUsers.find { it.id == matrixUser.id } != null,
|
||||
onCheckedChange = { checked ->
|
||||
if (checked) {
|
||||
onUserSelected(matrixUser)
|
||||
} else {
|
||||
onUserDeselected(matrixUser)
|
||||
|
||||
if (state is UserSearchResultState.Results) {
|
||||
LazyColumn {
|
||||
if (isMultiSelectionEnabled) {
|
||||
items(state.results) { matrixUser ->
|
||||
SearchMultipleUsersResultItem(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
matrixUser = matrixUser,
|
||||
isUserSelected = selectedUsers.find { it.id == matrixUser.id } != null,
|
||||
onCheckedChange = { checked ->
|
||||
if (checked) {
|
||||
onUserSelected(matrixUser)
|
||||
} else {
|
||||
onUserDeselected(matrixUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
items(results) { matrixUser ->
|
||||
SearchSingleUserResultItem(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
matrixUser = matrixUser,
|
||||
onClick = { onUserSelected(matrixUser) }
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
items(state.results) { matrixUser ->
|
||||
SearchSingleUserResultItem(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
matrixUser = matrixUser,
|
||||
onClick = { onUserSelected(matrixUser) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (state is UserSearchResultState.NoResults) {
|
||||
Spacer(Modifier.size(80.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.common_no_results),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ fun UserListView(
|
|||
SearchUserBar(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
query = state.searchQuery,
|
||||
results = state.searchResults,
|
||||
state = state.searchResults,
|
||||
selectedUsers = state.selectedUsers,
|
||||
active = state.isSearchActive,
|
||||
isMultiSelectionEnabled = state.isMultiSelectionEnabled,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ package io.element.android.features.userlist.impl
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
|
@ -35,13 +34,14 @@ import io.element.android.features.userlist.api.UserListEvents
|
|||
import io.element.android.features.userlist.api.UserListPresenter
|
||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
||||
import io.element.android.features.userlist.api.UserListState
|
||||
import io.element.android.features.userlist.api.UserSearchResultState
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.MatrixPatterns
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
class DefaultUserListPresenter @AssistedInject constructor(
|
||||
@Assisted val args: UserListPresenterArgs,
|
||||
|
|
@ -62,45 +62,47 @@ class DefaultUserListPresenter @AssistedInject constructor(
|
|||
@Composable
|
||||
override fun present(): UserListState {
|
||||
var isSearchActive by rememberSaveable { mutableStateOf(false) }
|
||||
val selectedUsers = userListDataStore.selectedUsers().collectAsState(emptyList())
|
||||
val selectedUsers by userListDataStore.selectedUsers().collectAsState(emptyList())
|
||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||
val searchResults: MutableState<ImmutableList<MatrixUser>> = remember {
|
||||
mutableStateOf(persistentListOf())
|
||||
}
|
||||
|
||||
fun handleEvents(event: UserListEvents) {
|
||||
when (event) {
|
||||
is UserListEvents.OnSearchActiveChanged -> isSearchActive = event.active
|
||||
is UserListEvents.UpdateSearchQuery -> searchQuery = event.query
|
||||
is UserListEvents.AddToSelection -> userListDataStore.selectUser(event.matrixUser)
|
||||
is UserListEvents.RemoveFromSelection -> userListDataStore.removeUserFromSelection(event.matrixUser)
|
||||
}
|
||||
var searchResults: UserSearchResultState by remember {
|
||||
mutableStateOf(UserSearchResultState.NotSearching)
|
||||
}
|
||||
|
||||
LaunchedEffect(searchQuery) {
|
||||
// Clear the search results before performing the search, manually add a fake result with the matrixId, if any
|
||||
searchResults.value = if (MatrixPatterns.isUserId(searchQuery)) {
|
||||
persistentListOf(MatrixUser(UserId(searchQuery)))
|
||||
searchResults = if (MatrixPatterns.isUserId(searchQuery)) {
|
||||
UserSearchResultState.Results(persistentListOf(MatrixUser(UserId(searchQuery))))
|
||||
} else {
|
||||
persistentListOf()
|
||||
UserSearchResultState.NotSearching
|
||||
}
|
||||
|
||||
// Debounce
|
||||
delay(args.searchDebouncePeriodMillis)
|
||||
|
||||
// Perform the search asynchronously
|
||||
if (searchQuery.isNotEmpty()) {
|
||||
searchResults.value = performSearch(searchQuery)
|
||||
if (searchQuery.length >= args.minimumSearchLength) {
|
||||
searchResults = performSearch(searchQuery)
|
||||
}
|
||||
}
|
||||
|
||||
return UserListState(
|
||||
searchQuery = searchQuery,
|
||||
searchResults = searchResults.value,
|
||||
selectedUsers = selectedUsers.value.toImmutableList(),
|
||||
searchResults = searchResults,
|
||||
selectedUsers = selectedUsers.toImmutableList(),
|
||||
isSearchActive = isSearchActive,
|
||||
selectionMode = args.selectionMode,
|
||||
eventSink = ::handleEvents,
|
||||
eventSink = { event ->
|
||||
when (event) {
|
||||
is UserListEvents.OnSearchActiveChanged -> isSearchActive = event.active
|
||||
is UserListEvents.UpdateSearchQuery -> searchQuery = event.query
|
||||
is UserListEvents.AddToSelection -> userListDataStore.selectUser(event.matrixUser)
|
||||
is UserListEvents.RemoveFromSelection -> userListDataStore.removeUserFromSelection(event.matrixUser)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun performSearch(query: String): ImmutableList<MatrixUser> {
|
||||
private suspend fun performSearch(query: String): UserSearchResultState {
|
||||
val isMatrixId = MatrixPatterns.isUserId(query)
|
||||
val results = userListDataSource.search(query).toMutableList()
|
||||
if (isMatrixId && results.none { it.id.value == query }) {
|
||||
|
|
@ -108,6 +110,6 @@ class DefaultUserListPresenter @AssistedInject constructor(
|
|||
val profile = getProfileResult ?: MatrixUser(UserId(query))
|
||||
results.add(0, profile)
|
||||
}
|
||||
return results.toImmutableList()
|
||||
return if (results.isEmpty()) UserSearchResultState.NoResults else UserSearchResultState.Results(results.toImmutableList())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,12 +25,14 @@ import io.element.android.features.userlist.api.SelectionMode
|
|||
import io.element.android.features.userlist.api.UserListDataStore
|
||||
import io.element.android.features.userlist.api.UserListEvents
|
||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
||||
import io.element.android.features.userlist.api.UserSearchResultState
|
||||
import io.element.android.features.userlist.test.FakeUserListDataSource
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import io.mockk.coJustRun
|
||||
import io.mockk.mockkConstructor
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
|
@ -55,7 +57,7 @@ class DefaultUserListPresenterTests {
|
|||
assertThat(initialState.isMultiSelectionEnabled).isFalse()
|
||||
assertThat(initialState.isSearchActive).isFalse()
|
||||
assertThat(initialState.selectedUsers).isEmpty()
|
||||
assertThat(initialState.searchResults).isEmpty()
|
||||
assertThat(initialState.searchResults).isEqualTo(UserSearchResultState.NotSearching)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -74,7 +76,7 @@ class DefaultUserListPresenterTests {
|
|||
assertThat(initialState.isMultiSelectionEnabled).isTrue()
|
||||
assertThat(initialState.isSearchActive).isFalse()
|
||||
assertThat(initialState.selectedUsers).isEmpty()
|
||||
assertThat(initialState.searchResults).isEmpty()
|
||||
assertThat(initialState.searchResults).isEqualTo(UserSearchResultState.NotSearching)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -96,18 +98,42 @@ class DefaultUserListPresenterTests {
|
|||
val matrixIdQuery = "@name:matrix.org"
|
||||
initialState.eventSink(UserListEvents.UpdateSearchQuery(matrixIdQuery))
|
||||
assertThat(awaitItem().searchQuery).isEqualTo(matrixIdQuery)
|
||||
assertThat(awaitItem().searchResults).containsExactly(MatrixUser(UserId(matrixIdQuery)))
|
||||
assertThat(awaitItem().searchResults).isEqualTo(UserSearchResultState.Results(persistentListOf(MatrixUser(UserId(matrixIdQuery)))))
|
||||
|
||||
val notMatrixIdQuery = "name"
|
||||
initialState.eventSink(UserListEvents.UpdateSearchQuery(notMatrixIdQuery))
|
||||
assertThat(awaitItem().searchQuery).isEqualTo(notMatrixIdQuery)
|
||||
assertThat(awaitItem().searchResults).isEmpty()
|
||||
assertThat(awaitItem().searchResults).isEqualTo(UserSearchResultState.NoResults)
|
||||
|
||||
initialState.eventSink(UserListEvents.OnSearchActiveChanged(false))
|
||||
assertThat(awaitItem().isSearchActive).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - searches when minimum length exceeded`() = runTest {
|
||||
val presenter = DefaultUserListPresenter(
|
||||
UserListPresenterArgs(selectionMode = SelectionMode.Single, minimumSearchLength = 3),
|
||||
userListDataSource,
|
||||
UserListDataStore(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
|
||||
// When the search term is too short, nothing happens
|
||||
initialState.eventSink(UserListEvents.UpdateSearchQuery("al"))
|
||||
assertThat(awaitItem().searchResults).isEqualTo(UserSearchResultState.NotSearching)
|
||||
|
||||
// When it reaches the minimum length, a search is performed asynchronously
|
||||
userListDataSource.givenSearchResult(listOf(aMatrixUser()))
|
||||
initialState.eventSink(UserListEvents.UpdateSearchQuery("alice"))
|
||||
assertThat(awaitItem().searchResults).isEqualTo(UserSearchResultState.NotSearching)
|
||||
assertThat(awaitItem().searchResults).isEqualTo(UserSearchResultState.Results(persistentListOf(aMatrixUser())))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - select a user`() = runTest {
|
||||
mockkConstructor(LazyListState::class)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue