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
c196174a95
commit
63168d4656
48 changed files with 432 additions and 71 deletions
1
changelog.d/95.feature
Normal file
1
changelog.d/95.feature
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
[Create and join rooms] Search for users to start a DM
|
||||||
|
|
@ -426,6 +426,7 @@ Rageshake can be very useful to get logs from a release version of the applicati
|
||||||
- When this is possible, prefer using `sealed interface` instead of `sealed class`;
|
- When this is possible, prefer using `sealed interface` instead of `sealed class`;
|
||||||
- When writing temporary code, using the string "DO NOT COMMIT" in a comment can help to avoid committing things by mistake. If committed and pushed, the CI
|
- When writing temporary code, using the string "DO NOT COMMIT" in a comment can help to avoid committing things by mistake. If committed and pushed, the CI
|
||||||
will detect this String and will warn the user about it. (TODO Not supported yet!)
|
will detect this String and will warn the user about it. (TODO Not supported yet!)
|
||||||
|
- Very occasionally the gradle cache misbehaves and causes problems with Dagger. Try building with `--no-build-cache` if Dagger isn't behaving how you expect.
|
||||||
|
|
||||||
## Happy coding!
|
## Happy coding!
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,17 +17,37 @@
|
||||||
package io.element.android.features.createroom.impl
|
package io.element.android.features.createroom.impl
|
||||||
|
|
||||||
import io.element.android.features.userlist.api.UserListDataSource
|
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.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.usersearch.MatrixUserProfile
|
||||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
// TODO this is empty as we currently don't have an endpoint to perform user search
|
class AllMatrixUsersDataSource @Inject constructor(
|
||||||
class AllMatrixUsersDataSource @Inject constructor() : UserListDataSource {
|
private val client: MatrixClient
|
||||||
|
) : UserListDataSource {
|
||||||
override suspend fun search(query: String): List<MatrixUser> {
|
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? {
|
override suspend fun getProfile(userId: UserId): MatrixUser? {
|
||||||
|
// TODO hook up to matrix client
|
||||||
return null
|
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 androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.userlist.api.SelectionMode
|
import io.element.android.features.userlist.api.SelectionMode
|
||||||
import io.element.android.features.userlist.api.UserListState
|
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.aListOfSelectedUsers
|
||||||
import io.element.android.features.userlist.api.aUserListState
|
import io.element.android.features.userlist.api.aUserListState
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
|
|
@ -29,13 +30,13 @@ open class AddPeopleUserListStateProvider : PreviewParameterProvider<UserListSta
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aUserListState(),
|
aUserListState(),
|
||||||
aUserListState().copy(
|
aUserListState().copy(
|
||||||
searchResults = aMatrixUserList().toImmutableList(),
|
searchResults = UserSearchResultState.Results(aMatrixUserList().toImmutableList()),
|
||||||
selectedUsers = aListOfSelectedUsers(),
|
selectedUsers = aListOfSelectedUsers(),
|
||||||
isSearchActive = false,
|
isSearchActive = false,
|
||||||
selectionMode = SelectionMode.Multiple,
|
selectionMode = SelectionMode.Multiple,
|
||||||
),
|
),
|
||||||
aUserListState().copy(
|
aUserListState().copy(
|
||||||
searchResults = aMatrixUserList().toImmutableList(),
|
searchResults = UserSearchResultState.Results(aMatrixUserList().toImmutableList()),
|
||||||
selectedUsers = aListOfSelectedUsers(),
|
selectedUsers = aListOfSelectedUsers(),
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
selectionMode = SelectionMode.Multiple,
|
selectionMode = SelectionMode.Multiple,
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,11 @@ import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import io.element.android.features.createroom.impl.AllMatrixUsersDataSource
|
import io.element.android.features.createroom.impl.AllMatrixUsersDataSource
|
||||||
import io.element.android.features.userlist.api.UserListDataSource
|
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
|
import javax.inject.Named
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@ContributesTo(AppScope::class)
|
@ContributesTo(SessionScope::class)
|
||||||
interface CreateRoomModule {
|
interface CreateRoomModule {
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,11 @@ class CreateRoomRootPresenter @Inject constructor(
|
||||||
|
|
||||||
private val presenter by lazy {
|
private val presenter by lazy {
|
||||||
presenterFactory.create(
|
presenterFactory.create(
|
||||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
UserListPresenterArgs(
|
||||||
|
selectionMode = SelectionMode.Single,
|
||||||
|
minimumSearchLength = 3,
|
||||||
|
searchDebouncePeriodMillis = 500,
|
||||||
|
),
|
||||||
userListDataSource,
|
userListDataSource,
|
||||||
userListDataStore,
|
userListDataStore,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.features.createroom.impl.root
|
package io.element.android.features.createroom.impl.root
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
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.features.userlist.api.aUserListState
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
|
|
@ -31,7 +32,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRoot
|
||||||
userListState = aMatrixUser().let {
|
userListState = aMatrixUser().let {
|
||||||
aUserListState().copy(
|
aUserListState().copy(
|
||||||
searchQuery = it.id.value,
|
searchQuery = it.id.value,
|
||||||
searchResults = persistentListOf(it),
|
searchResults = UserSearchResultState.Results(persistentListOf(it)),
|
||||||
selectedUsers = persistentListOf(it),
|
selectedUsers = persistentListOf(it),
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
)
|
)
|
||||||
|
|
@ -42,7 +43,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRoot
|
||||||
userListState = aMatrixUser().let {
|
userListState = aMatrixUser().let {
|
||||||
aUserListState().copy(
|
aUserListState().copy(
|
||||||
searchQuery = it.id.value,
|
searchQuery = it.id.value,
|
||||||
searchResults = persistentListOf(it),
|
searchResults = UserSearchResultState.Results(persistentListOf(it)),
|
||||||
selectedUsers = persistentListOf(it),
|
selectedUsers = persistentListOf(it),
|
||||||
isSearchActive = true,
|
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
|
package io.element.android.features.roomdetails.impl.members
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
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.features.userlist.api.aUserListState
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
|
|
@ -33,7 +34,7 @@ internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMember
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun aRoomMemberListState(
|
internal fun aRoomMemberListState(
|
||||||
searchResults: ImmutableList<MatrixUser> = persistentListOf(),
|
searchResults: UserSearchResultState = UserSearchResultState.NotSearching,
|
||||||
allUsers: Async<ImmutableList<MatrixUser>> = Async.Uninitialized,
|
allUsers: Async<ImmutableList<MatrixUser>> = Async.Uninitialized,
|
||||||
) =
|
) =
|
||||||
RoomMemberListState(
|
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.UserListDataStore
|
||||||
import io.element.android.features.userlist.api.UserListPresenter
|
import io.element.android.features.userlist.api.UserListPresenter
|
||||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
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.impl.DefaultUserListPresenter
|
||||||
import io.element.android.features.userlist.test.FakeUserListDataSource
|
import io.element.android.features.userlist.test.FakeUserListDataSource
|
||||||
import io.element.android.libraries.architecture.Async
|
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.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import okhttp3.internal.toImmutableList
|
import okhttp3.internal.toImmutableList
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -72,7 +71,7 @@ class RoomMemberListPresenterTests {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
Truth.assertThat(initialState.allUsers).isInstanceOf(Async.Loading::class.java)
|
Truth.assertThat(initialState.allUsers).isInstanceOf(Async.Loading::class.java)
|
||||||
Truth.assertThat(initialState.userListState.isSearchActive).isFalse()
|
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)
|
Truth.assertThat(initialState.userListState.selectionMode).isEqualTo(SelectionMode.Single)
|
||||||
|
|
||||||
val loadedState = awaitItem()
|
val loadedState = awaitItem()
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
plugins {
|
plugins {
|
||||||
id("io.element.android-compose-library")
|
id("io.element.android-compose-library")
|
||||||
|
alias(libs.plugins.ksp)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
@ -27,4 +28,5 @@ dependencies {
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.matrixui)
|
implementation(projects.libraries.matrixui)
|
||||||
|
ksp(libs.showkase.processor)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ package io.element.android.features.userlist.api
|
||||||
|
|
||||||
data class UserListPresenterArgs(
|
data class UserListPresenterArgs(
|
||||||
val selectionMode: SelectionMode,
|
val selectionMode: SelectionMode,
|
||||||
|
val minimumSearchLength: Int = 1,
|
||||||
|
val searchDebouncePeriodMillis: Long = 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class SelectionMode {
|
enum class SelectionMode {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class UserListState(
|
data class UserListState(
|
||||||
val searchQuery: String,
|
val searchQuery: String,
|
||||||
val searchResults: ImmutableList<MatrixUser>,
|
val searchResults: UserSearchResultState,
|
||||||
val selectedUsers: ImmutableList<MatrixUser>,
|
val selectedUsers: ImmutableList<MatrixUser>,
|
||||||
val isSearchActive: Boolean,
|
val isSearchActive: Boolean,
|
||||||
val selectionMode: SelectionMode,
|
val selectionMode: SelectionMode,
|
||||||
|
|
@ -29,3 +29,14 @@ data class UserListState(
|
||||||
) {
|
) {
|
||||||
val isMultiSelectionEnabled = selectionMode == SelectionMode.Multiple
|
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,
|
isSearchActive = true,
|
||||||
searchQuery = "@someone:matrix.org",
|
searchQuery = "@someone:matrix.org",
|
||||||
selectedUsers = aListOfSelectedUsers(),
|
selectedUsers = aListOfSelectedUsers(),
|
||||||
searchResults = aMatrixUserList().toImmutableList(),
|
searchResults = UserSearchResultState.Results(aMatrixUserList().toImmutableList()),
|
||||||
),
|
),
|
||||||
aUserListState().copy(
|
aUserListState().copy(
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
searchQuery = "@someone:matrix.org",
|
searchQuery = "@someone:matrix.org",
|
||||||
selectionMode = SelectionMode.Multiple,
|
selectionMode = SelectionMode.Multiple,
|
||||||
selectedUsers = aListOfSelectedUsers(),
|
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(
|
fun aUserListState() = UserListState(
|
||||||
isSearchActive = false,
|
isSearchActive = false,
|
||||||
searchQuery = "",
|
searchQuery = "",
|
||||||
searchResults = persistentListOf(),
|
searchResults = UserSearchResultState.NotSearching,
|
||||||
selectedUsers = persistentListOf(),
|
selectedUsers = persistentListOf(),
|
||||||
selectionMode = SelectionMode.Single,
|
selectionMode = SelectionMode.Single,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
|
|
|
||||||
|
|
@ -17,14 +17,17 @@
|
||||||
package io.element.android.features.userlist.api.components
|
package io.element.android.features.userlist.api.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.Search
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SearchBarDefaults
|
import androidx.compose.material3.SearchBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
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.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
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.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||||
|
|
@ -46,7 +51,7 @@ import kotlinx.collections.immutable.ImmutableList
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchUserBar(
|
fun SearchUserBar(
|
||||||
query: String,
|
query: String,
|
||||||
results: ImmutableList<MatrixUser>,
|
state: UserSearchResultState,
|
||||||
selectedUsers: ImmutableList<MatrixUser>,
|
selectedUsers: ImmutableList<MatrixUser>,
|
||||||
active: Boolean,
|
active: Boolean,
|
||||||
isMultiSelectionEnabled: Boolean,
|
isMultiSelectionEnabled: Boolean,
|
||||||
|
|
@ -91,6 +96,7 @@ fun SearchUserBar(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
!active -> {
|
!active -> {
|
||||||
{
|
{
|
||||||
Icon(
|
Icon(
|
||||||
|
|
@ -100,6 +106,7 @@ fun SearchUserBar(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> null
|
else -> null
|
||||||
},
|
},
|
||||||
colors = if (!active) SearchBarDefaults.colors() else SearchBarDefaults.colors(containerColor = Color.Transparent),
|
colors = if (!active) SearchBarDefaults.colors() else SearchBarDefaults.colors(containerColor = Color.Transparent),
|
||||||
|
|
@ -113,31 +120,43 @@ fun SearchUserBar(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
LazyColumn {
|
|
||||||
if (isMultiSelectionEnabled) {
|
if (state is UserSearchResultState.Results) {
|
||||||
items(results) { matrixUser ->
|
LazyColumn {
|
||||||
SearchMultipleUsersResultItem(
|
if (isMultiSelectionEnabled) {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
items(state.results) { matrixUser ->
|
||||||
matrixUser = matrixUser,
|
SearchMultipleUsersResultItem(
|
||||||
isUserSelected = selectedUsers.find { it.id == matrixUser.id } != null,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
onCheckedChange = { checked ->
|
matrixUser = matrixUser,
|
||||||
if (checked) {
|
isUserSelected = selectedUsers.find { it.id == matrixUser.id } != null,
|
||||||
onUserSelected(matrixUser)
|
onCheckedChange = { checked ->
|
||||||
} else {
|
if (checked) {
|
||||||
onUserDeselected(matrixUser)
|
onUserSelected(matrixUser)
|
||||||
|
} else {
|
||||||
|
onUserDeselected(matrixUser)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
}
|
} else {
|
||||||
} else {
|
items(state.results) { matrixUser ->
|
||||||
items(results) { matrixUser ->
|
SearchSingleUserResultItem(
|
||||||
SearchSingleUserResultItem(
|
modifier = Modifier.fillMaxWidth(),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
matrixUser = matrixUser,
|
||||||
matrixUser = matrixUser,
|
onClick = { onUserSelected(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(
|
SearchUserBar(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
query = state.searchQuery,
|
query = state.searchQuery,
|
||||||
results = state.searchResults,
|
state = state.searchResults,
|
||||||
selectedUsers = state.selectedUsers,
|
selectedUsers = state.selectedUsers,
|
||||||
active = state.isSearchActive,
|
active = state.isSearchActive,
|
||||||
isMultiSelectionEnabled = state.isMultiSelectionEnabled,
|
isMultiSelectionEnabled = state.isMultiSelectionEnabled,
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ package io.element.android.features.userlist.impl
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
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.UserListPresenter
|
||||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
import io.element.android.features.userlist.api.UserListPresenterArgs
|
||||||
import io.element.android.features.userlist.api.UserListState
|
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.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.core.MatrixPatterns
|
import io.element.android.libraries.matrix.api.core.MatrixPatterns
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
class DefaultUserListPresenter @AssistedInject constructor(
|
class DefaultUserListPresenter @AssistedInject constructor(
|
||||||
@Assisted val args: UserListPresenterArgs,
|
@Assisted val args: UserListPresenterArgs,
|
||||||
|
|
@ -62,45 +62,47 @@ class DefaultUserListPresenter @AssistedInject constructor(
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): UserListState {
|
override fun present(): UserListState {
|
||||||
var isSearchActive by rememberSaveable { mutableStateOf(false) }
|
var isSearchActive by rememberSaveable { mutableStateOf(false) }
|
||||||
val selectedUsers = userListDataStore.selectedUsers().collectAsState(emptyList())
|
val selectedUsers by userListDataStore.selectedUsers().collectAsState(emptyList())
|
||||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||||
val searchResults: MutableState<ImmutableList<MatrixUser>> = remember {
|
var searchResults: UserSearchResultState by remember {
|
||||||
mutableStateOf(persistentListOf())
|
mutableStateOf(UserSearchResultState.NotSearching)
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(searchQuery) {
|
LaunchedEffect(searchQuery) {
|
||||||
// Clear the search results before performing the search, manually add a fake result with the matrixId, if any
|
// Clear the search results before performing the search, manually add a fake result with the matrixId, if any
|
||||||
searchResults.value = if (MatrixPatterns.isUserId(searchQuery)) {
|
searchResults = if (MatrixPatterns.isUserId(searchQuery)) {
|
||||||
persistentListOf(MatrixUser(UserId(searchQuery)))
|
UserSearchResultState.Results(persistentListOf(MatrixUser(UserId(searchQuery))))
|
||||||
} else {
|
} else {
|
||||||
persistentListOf()
|
UserSearchResultState.NotSearching
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debounce
|
||||||
|
delay(args.searchDebouncePeriodMillis)
|
||||||
|
|
||||||
// Perform the search asynchronously
|
// Perform the search asynchronously
|
||||||
if (searchQuery.isNotEmpty()) {
|
if (searchQuery.length >= args.minimumSearchLength) {
|
||||||
searchResults.value = performSearch(searchQuery)
|
searchResults = performSearch(searchQuery)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return UserListState(
|
return UserListState(
|
||||||
searchQuery = searchQuery,
|
searchQuery = searchQuery,
|
||||||
searchResults = searchResults.value,
|
searchResults = searchResults,
|
||||||
selectedUsers = selectedUsers.value.toImmutableList(),
|
selectedUsers = selectedUsers.toImmutableList(),
|
||||||
isSearchActive = isSearchActive,
|
isSearchActive = isSearchActive,
|
||||||
selectionMode = args.selectionMode,
|
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 isMatrixId = MatrixPatterns.isUserId(query)
|
||||||
val results = userListDataSource.search(query).toMutableList()
|
val results = userListDataSource.search(query).toMutableList()
|
||||||
if (isMatrixId && results.none { it.id.value == query }) {
|
if (isMatrixId && results.none { it.id.value == query }) {
|
||||||
|
|
@ -108,6 +110,6 @@ class DefaultUserListPresenter @AssistedInject constructor(
|
||||||
val profile = getProfileResult ?: MatrixUser(UserId(query))
|
val profile = getProfileResult ?: MatrixUser(UserId(query))
|
||||||
results.add(0, profile)
|
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.UserListDataStore
|
||||||
import io.element.android.features.userlist.api.UserListEvents
|
import io.element.android.features.userlist.api.UserListEvents
|
||||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
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.features.userlist.test.FakeUserListDataSource
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
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.components.aMatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
import io.mockk.coJustRun
|
import io.mockk.coJustRun
|
||||||
import io.mockk.mockkConstructor
|
import io.mockk.mockkConstructor
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -55,7 +57,7 @@ class DefaultUserListPresenterTests {
|
||||||
assertThat(initialState.isMultiSelectionEnabled).isFalse()
|
assertThat(initialState.isMultiSelectionEnabled).isFalse()
|
||||||
assertThat(initialState.isSearchActive).isFalse()
|
assertThat(initialState.isSearchActive).isFalse()
|
||||||
assertThat(initialState.selectedUsers).isEmpty()
|
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.isMultiSelectionEnabled).isTrue()
|
||||||
assertThat(initialState.isSearchActive).isFalse()
|
assertThat(initialState.isSearchActive).isFalse()
|
||||||
assertThat(initialState.selectedUsers).isEmpty()
|
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"
|
val matrixIdQuery = "@name:matrix.org"
|
||||||
initialState.eventSink(UserListEvents.UpdateSearchQuery(matrixIdQuery))
|
initialState.eventSink(UserListEvents.UpdateSearchQuery(matrixIdQuery))
|
||||||
assertThat(awaitItem().searchQuery).isEqualTo(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"
|
val notMatrixIdQuery = "name"
|
||||||
initialState.eventSink(UserListEvents.UpdateSearchQuery(notMatrixIdQuery))
|
initialState.eventSink(UserListEvents.UpdateSearchQuery(notMatrixIdQuery))
|
||||||
assertThat(awaitItem().searchQuery).isEqualTo(notMatrixIdQuery)
|
assertThat(awaitItem().searchQuery).isEqualTo(notMatrixIdQuery)
|
||||||
assertThat(awaitItem().searchResults).isEmpty()
|
assertThat(awaitItem().searchResults).isEqualTo(UserSearchResultState.NoResults)
|
||||||
|
|
||||||
initialState.eventSink(UserListEvents.OnSearchActiveChanged(false))
|
initialState.eventSink(UserListEvents.OnSearchActiveChanged(false))
|
||||||
assertThat(awaitItem().isSearchActive).isFalse()
|
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
|
@Test
|
||||||
fun `present - select a user`() = runTest {
|
fun `present - select a user`() = runTest {
|
||||||
mockkConstructor(LazyListState::class)
|
mockkConstructor(LazyListState::class)
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import io.element.android.libraries.matrix.api.pusher.PushersService
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||||
import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource
|
import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource
|
||||||
|
import io.element.android.libraries.matrix.api.usersearch.MatrixSearchUserResults
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
|
|
||||||
|
|
@ -58,4 +59,6 @@ interface MatrixClient : Closeable {
|
||||||
fun onSlidingSyncUpdate()
|
fun onSlidingSyncUpdate()
|
||||||
|
|
||||||
fun roomMembershipObserver(): RoomMembershipObserver
|
fun roomMembershipObserver(): RoomMembershipObserver
|
||||||
|
|
||||||
|
suspend fun searchUsers(searchTerm: String, limit: Long): Result<MatrixSearchUserResults>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* 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.libraries.matrix.api.usersearch
|
||||||
|
|
||||||
|
data class MatrixSearchUserResults(
|
||||||
|
val results: List<MatrixUserProfile>,
|
||||||
|
val limited: Boolean,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* 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.libraries.matrix.api.usersearch
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
||||||
|
data class MatrixUserProfile(
|
||||||
|
val userId: UserId,
|
||||||
|
val displayName: String?,
|
||||||
|
val avatarUrl: String?
|
||||||
|
)
|
||||||
|
|
@ -29,6 +29,7 @@ import io.element.android.libraries.matrix.api.pusher.PushersService
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||||
import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource
|
import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource
|
||||||
|
import io.element.android.libraries.matrix.api.usersearch.MatrixSearchUserResults
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import io.element.android.libraries.matrix.impl.media.RustMediaResolver
|
import io.element.android.libraries.matrix.impl.media.RustMediaResolver
|
||||||
import io.element.android.libraries.matrix.impl.notification.RustNotificationService
|
import io.element.android.libraries.matrix.impl.notification.RustNotificationService
|
||||||
|
|
@ -36,6 +37,7 @@ import io.element.android.libraries.matrix.impl.pushers.RustPushersService
|
||||||
import io.element.android.libraries.matrix.impl.room.RustMatrixRoom
|
import io.element.android.libraries.matrix.impl.room.RustMatrixRoom
|
||||||
import io.element.android.libraries.matrix.impl.room.RustRoomSummaryDataSource
|
import io.element.android.libraries.matrix.impl.room.RustRoomSummaryDataSource
|
||||||
import io.element.android.libraries.matrix.impl.sync.SlidingSyncObserverProxy
|
import io.element.android.libraries.matrix.impl.sync.SlidingSyncObserverProxy
|
||||||
|
import io.element.android.libraries.matrix.impl.usersearch.UserSearchResultMapper
|
||||||
import io.element.android.libraries.matrix.impl.verification.RustSessionVerificationService
|
import io.element.android.libraries.matrix.impl.verification.RustSessionVerificationService
|
||||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -362,6 +364,13 @@ class RustMatrixClient constructor(
|
||||||
|
|
||||||
override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver
|
override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver
|
||||||
|
|
||||||
|
override suspend fun searchUsers(searchTerm: String, limit: Long): Result<MatrixSearchUserResults> =
|
||||||
|
withContext(dispatchers.io) {
|
||||||
|
runCatching {
|
||||||
|
UserSearchResultMapper.map(client.searchUsers(searchTerm, limit.toULong()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun File.deleteSessionDirectory(userID: String): Boolean {
|
private fun File.deleteSessionDirectory(userID: String): Boolean {
|
||||||
// Rust sanitises the user ID replacing invalid characters with an _
|
// Rust sanitises the user ID replacing invalid characters with an _
|
||||||
val sanitisedUserID = userID.replace(":", "_")
|
val sanitisedUserID = userID.replace(":", "_")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* 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.libraries.matrix.impl.usersearch
|
||||||
|
|
||||||
|
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 org.matrix.rustcomponents.sdk.SearchUsersResults
|
||||||
|
import org.matrix.rustcomponents.sdk.UserProfile
|
||||||
|
|
||||||
|
object UserSearchResultMapper {
|
||||||
|
|
||||||
|
fun map(result: SearchUsersResults): MatrixSearchUserResults {
|
||||||
|
return MatrixSearchUserResults(
|
||||||
|
results = result.results.map(::mapUserProfile),
|
||||||
|
limited = result.limited,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapUserProfile(userProfile: UserProfile): MatrixUserProfile {
|
||||||
|
return MatrixUserProfile(
|
||||||
|
userId = UserId(userProfile.userId),
|
||||||
|
displayName = userProfile.displayName,
|
||||||
|
avatarUrl = userProfile.avatarUrl,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.api.pusher.PushersService
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||||
import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource
|
import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource
|
||||||
|
import io.element.android.libraries.matrix.api.usersearch.MatrixSearchUserResults
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import io.element.android.libraries.matrix.test.media.FakeMediaResolver
|
import io.element.android.libraries.matrix.test.media.FakeMediaResolver
|
||||||
import io.element.android.libraries.matrix.test.notification.FakeNotificationService
|
import io.element.android.libraries.matrix.test.notification.FakeNotificationService
|
||||||
|
|
@ -55,6 +56,7 @@ class FakeMatrixClient(
|
||||||
private var findDmResult: MatrixRoom? = FakeMatrixRoom()
|
private var findDmResult: MatrixRoom? = FakeMatrixRoom()
|
||||||
private var logoutFailure: Throwable? = null
|
private var logoutFailure: Throwable? = null
|
||||||
private val getRoomResults = mutableMapOf<RoomId, MatrixRoom>()
|
private val getRoomResults = mutableMapOf<RoomId, MatrixRoom>()
|
||||||
|
private val searchUserResults = mutableMapOf<String, Result<MatrixSearchUserResults>>()
|
||||||
|
|
||||||
override fun getRoom(roomId: RoomId): MatrixRoom? {
|
override fun getRoom(roomId: RoomId): MatrixRoom? {
|
||||||
return getRoomResults[roomId]
|
return getRoomResults[roomId]
|
||||||
|
|
@ -126,6 +128,10 @@ class FakeMatrixClient(
|
||||||
return RoomMembershipObserver()
|
return RoomMembershipObserver()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun searchUsers(searchTerm: String, limit: Long): Result<MatrixSearchUserResults> {
|
||||||
|
return searchUserResults[searchTerm] ?: Result.failure(IllegalStateException("No response defined for $searchTerm"))
|
||||||
|
}
|
||||||
|
|
||||||
// Mocks
|
// Mocks
|
||||||
|
|
||||||
fun givenLogoutError(failure: Throwable?) {
|
fun givenLogoutError(failure: Throwable?) {
|
||||||
|
|
@ -159,4 +165,8 @@ class FakeMatrixClient(
|
||||||
fun givenGetRoomResult(roomId: RoomId, result: MatrixRoom) {
|
fun givenGetRoomResult(roomId: RoomId, result: MatrixRoom) {
|
||||||
getRoomResults[roomId] = result
|
getRoomResults[roomId] = result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun givenSearchUsersResult(searchTerm: String, result: Result<MatrixSearchUserResults>) {
|
||||||
|
searchUserResults[searchTerm] = result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:4bd925ebe0257b400ed0b1cb956848622d1b42d8afcc71f915522c4878aaf94b
|
||||||
|
size 23447
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:12b569ad80bb4a4df89d70d9ae4a92da0d9e96deb477e1e437efbef5ec4a1fdd
|
||||||
|
size 22302
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:91de9d54216aac2bf2c4721e5e8ff5e74be43105846bdac16230cb7bea7427af
|
||||||
|
size 13777
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:38fbef57e6fb2860d05ab2b864bab16a30ebc98199eb18b19e422f946c52e163
|
||||||
|
size 13096
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:3b357bc650466f045ff7a3d601b0a716f81af9fc55cd4f082a231747905e6860
|
||||||
|
size 13146
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:de04bcf3f3d053024f7f5e9e11da95b7d609cf3c1982db70d1ef734b0f21d729
|
||||||
|
size 12577
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:ffcc7edc0898f3afccf77679a477e76f9f9aef5c4f0309518b3025ed6ce17f96
|
||||||
|
size 32696
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:cc5d0c10593401fbb674c2a27333e546db9b0c11aca9ae280a721aa0e2d16765
|
||||||
|
size 31793
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:02d8edfee13ad4ca97dde238939b6166cccc28550d77c7b924c6aebd6e8d6a07
|
||||||
|
size 9916
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:6a212fb83b307da9ebd0681492286d7f19520c8140629f0a21bad0c43e3954d6
|
||||||
|
size 36910
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:89ea65099fb4981bbeb24e49afb7400b0e8da79e3b783e198519ba1e970404a8
|
||||||
|
size 8317
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:9a8ba207cf61c56b64c6855d04c8a30def0b3daf325adf57edd45b40981ed745
|
||||||
|
size 7733
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:9a8ba207cf61c56b64c6855d04c8a30def0b3daf325adf57edd45b40981ed745
|
||||||
|
size 7733
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a57daa1da9d07e6c7d497c63eb0c9fda36f48ea28f4592eef62774034b86b5e3
|
||||||
|
size 87856
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:0071d13454a9e8d75fc693d2f13a65c4d61910425217757804d92c086d56792f
|
||||||
|
size 102885
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:d5b879b74654fdad0638f432a71adfc9186fbe00dafd5d963a3affb33ec8c5c8
|
||||||
|
size 12841
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:2fbafefd9aa66ac411836715ef57f39e05e4eeeb0f4601b8a1706d9a3a487527
|
||||||
|
size 9692
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:18d25ee10a5deb2e7a81aadd65dad542ed1a40889d77e375412c66c30a834492
|
||||||
|
size 34260
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:b8de2f28cf9918a7eddcc1311c34ba0376242a4708724b57689df000b7480524
|
||||||
|
size 8197
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:687133e4729ea42382ac0b76af6ceaf8b20cbc65923412fb97b077d2475c70f7
|
||||||
|
size 7514
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:687133e4729ea42382ac0b76af6ceaf8b20cbc65923412fb97b077d2475c70f7
|
||||||
|
size 7514
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a708d195ffce2d542c494c6949456a1d341dff5c51f742e04212b72863c91988
|
||||||
|
size 84136
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:ff83f67a9fbcc5066547224c2f26d94c33bd0b60563acc062fde399ef9d681e1
|
||||||
|
size 97631
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:534cba0c13aa52b3557ab8df854c521dd13b82e5a1550d73abcef12925e389da
|
||||||
|
size 11878
|
||||||
Loading…
Add table
Add a link
Reference in a new issue