forked from dsutanto/bChot-android
First Commit
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.usersearch.impl"
|
||||
}
|
||||
|
||||
setupDependencyInjection()
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.di)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
api(projects.libraries.usersearch.api)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.usersearch.test)
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.usersearch.impl
|
||||
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.usersearch.api.UserListDataSource
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class MatrixUserListDataSource(
|
||||
private val client: MatrixClient
|
||||
) : UserListDataSource {
|
||||
override suspend fun search(query: String, count: Long): List<MatrixUser> {
|
||||
val res = client.searchUsers(query, count)
|
||||
return res.getOrNull()?.results.orEmpty()
|
||||
}
|
||||
|
||||
override suspend fun getProfile(userId: UserId): MatrixUser? {
|
||||
return client.getProfile(userId).getOrNull()
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.usersearch.impl
|
||||
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
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.user.MatrixUser
|
||||
import io.element.android.libraries.usersearch.api.UserListDataSource
|
||||
import io.element.android.libraries.usersearch.api.UserRepository
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResultState
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class MatrixUserRepository(
|
||||
private val client: MatrixClient,
|
||||
private val dataSource: UserListDataSource
|
||||
) : UserRepository {
|
||||
override fun search(query: String): Flow<UserSearchResultState> = flow {
|
||||
val shouldQueryProfile = MatrixPatterns.isUserId(query) && !client.isMe(UserId(query))
|
||||
val shouldFetchSearchResults = query.length >= MINIMUM_SEARCH_LENGTH
|
||||
// If the search term is a MXID that's not ours, we'll show a 'fake' result for that user, then update it when we get search results.
|
||||
val fakeSearchResult = if (shouldQueryProfile) {
|
||||
UserSearchResult(MatrixUser(UserId(query)))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (shouldQueryProfile || shouldFetchSearchResults) {
|
||||
emit(UserSearchResultState(isSearching = shouldFetchSearchResults, results = listOfNotNull(fakeSearchResult)))
|
||||
}
|
||||
if (shouldFetchSearchResults) {
|
||||
val results = fetchSearchResults(query, shouldQueryProfile)
|
||||
emit(results)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchSearchResults(query: String, shouldQueryProfile: Boolean): UserSearchResultState {
|
||||
// Debounce
|
||||
delay(DEBOUNCE_TIME_MILLIS)
|
||||
val results = dataSource
|
||||
.search(query, MAXIMUM_SEARCH_RESULTS)
|
||||
.filter { !client.isMe(it.userId) }
|
||||
.map { UserSearchResult(it) }
|
||||
.toMutableList()
|
||||
|
||||
// If the query is another user's MXID and the result doesn't contain that user ID, query the profile information explicitly
|
||||
if (shouldQueryProfile && results.none { it.matrixUser.userId.value == query }) {
|
||||
results.add(
|
||||
0,
|
||||
dataSource.getProfile(UserId(query))
|
||||
?.let { UserSearchResult(it) }
|
||||
?: UserSearchResult(MatrixUser(UserId(query)), isUnresolved = true)
|
||||
)
|
||||
}
|
||||
|
||||
return UserSearchResultState(results = results, isSearching = false)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEBOUNCE_TIME_MILLIS = 250L
|
||||
private const val MINIMUM_SEARCH_LENGTH = 3
|
||||
private const val MAXIMUM_SEARCH_RESULTS = 10L
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.usersearch.impl
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
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 kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
internal class MatrixUserListDataSourceTest {
|
||||
@Test
|
||||
fun `search - returns users on success`() = runTest {
|
||||
val matrixClient = FakeMatrixClient()
|
||||
matrixClient.givenSearchUsersResult(
|
||||
searchTerm = "test",
|
||||
result = Result.success(
|
||||
MatrixSearchUserResults(
|
||||
results = persistentListOf(
|
||||
aMatrixUserProfile(),
|
||||
aMatrixUserProfile(userId = A_USER_ID_2)
|
||||
),
|
||||
limited = false
|
||||
)
|
||||
)
|
||||
)
|
||||
val dataSource = MatrixUserListDataSource(matrixClient)
|
||||
|
||||
val results = dataSource.search("test", 2)
|
||||
assertThat(results).containsExactly(
|
||||
aMatrixUserProfile(),
|
||||
aMatrixUserProfile(userId = A_USER_ID_2)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `search - returns empty list on error`() = runTest {
|
||||
val matrixClient = FakeMatrixClient()
|
||||
matrixClient.givenSearchUsersResult(
|
||||
searchTerm = "test",
|
||||
result = Result.failure(RuntimeException("Ruhroh"))
|
||||
)
|
||||
val dataSource = MatrixUserListDataSource(matrixClient)
|
||||
|
||||
val results = dataSource.search("test", 2)
|
||||
assertThat(results).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get profile - returns user on success`() = runTest {
|
||||
val matrixClient = FakeMatrixClient()
|
||||
matrixClient.givenGetProfileResult(
|
||||
userId = A_USER_ID,
|
||||
result = Result.success(aMatrixUserProfile())
|
||||
)
|
||||
val dataSource = MatrixUserListDataSource(matrixClient)
|
||||
|
||||
val result = dataSource.getProfile(A_USER_ID)
|
||||
assertThat(result).isEqualTo(aMatrixUserProfile())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get profile - returns null on error`() = runTest {
|
||||
val matrixClient = FakeMatrixClient()
|
||||
matrixClient.givenGetProfileResult(
|
||||
userId = A_USER_ID,
|
||||
result = Result.failure(RuntimeException("Ruhroh"))
|
||||
)
|
||||
val dataSource = MatrixUserListDataSource(matrixClient)
|
||||
|
||||
val result = dataSource.getProfile(A_USER_ID)
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
private fun aMatrixUserProfile(
|
||||
userId: UserId = A_USER_ID,
|
||||
displayName: String = A_USER_NAME,
|
||||
avatarUrl: String = AN_AVATAR_URL
|
||||
) = MatrixUser(userId, displayName, avatarUrl)
|
||||
}
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.usersearch.impl
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
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.components.aMatrixUserList
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||
import io.element.android.libraries.usersearch.test.FakeUserListDataSource
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
private val SESSION_ID = SessionId("@current-user:example.com")
|
||||
|
||||
internal class MatrixUserRepositoryTest {
|
||||
@Test
|
||||
fun `search - emits nothing if the search query is too short`() = runTest {
|
||||
val dataSource = FakeUserListDataSource()
|
||||
val repository = MatrixUserRepository(FakeMatrixClient(SESSION_ID), dataSource)
|
||||
|
||||
val result = repository.search("x")
|
||||
|
||||
result.test {
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `search - returns empty list if no results are found`() = runTest {
|
||||
val dataSource = FakeUserListDataSource()
|
||||
val repository = MatrixUserRepository(FakeMatrixClient(SESSION_ID), dataSource)
|
||||
|
||||
val result = repository.search("some query")
|
||||
|
||||
result.test {
|
||||
awaitItem().also {
|
||||
assertThat(it.isSearching).isTrue()
|
||||
assertThat(it.results).isEmpty()
|
||||
}
|
||||
awaitItem().also {
|
||||
assertThat(it.isSearching).isFalse()
|
||||
assertThat(it.results).isEmpty()
|
||||
}
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `search - returns users if results are found`() = runTest {
|
||||
val dataSource = FakeUserListDataSource()
|
||||
dataSource.givenSearchResult(aMatrixUserList())
|
||||
val repository = MatrixUserRepository(FakeMatrixClient(SESSION_ID), dataSource)
|
||||
|
||||
val result = repository.search("some query")
|
||||
|
||||
result.test {
|
||||
awaitItem().also {
|
||||
assertThat(it.isSearching).isTrue()
|
||||
assertThat(it.results).isEmpty()
|
||||
}
|
||||
awaitItem().also {
|
||||
assertThat(it.isSearching).isFalse()
|
||||
assertThat(it.results).isEqualTo(aMatrixUserList().toUserSearchResults())
|
||||
}
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `search - immediately returns placeholder if search is mxid`() = runTest {
|
||||
val dataSource = FakeUserListDataSource()
|
||||
val repository = MatrixUserRepository(FakeMatrixClient(SESSION_ID), dataSource)
|
||||
|
||||
val result = repository.search(A_USER_ID.value)
|
||||
|
||||
result.test {
|
||||
awaitItem().also {
|
||||
assertThat(it.isSearching).isTrue()
|
||||
assertThat(it.results).isEqualTo(listOf(placeholderResult()))
|
||||
}
|
||||
cancelAndConsumeRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `search - doesn't return placeholder if search is the local user's mxid`() = runTest {
|
||||
val dataSource = FakeUserListDataSource()
|
||||
val repository = MatrixUserRepository(FakeMatrixClient(SESSION_ID), dataSource)
|
||||
|
||||
val result = repository.search(SESSION_ID.value)
|
||||
|
||||
result.test {
|
||||
awaitItem().also {
|
||||
assertThat(it.isSearching).isTrue()
|
||||
assertThat(it.results).isEmpty()
|
||||
}
|
||||
cancelAndConsumeRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `search - filters out results with the local user's mxid`() = runTest {
|
||||
val searchResults = aMatrixUserList() + MatrixUser(userId = SESSION_ID, displayName = A_USER_NAME)
|
||||
val dataSource = FakeUserListDataSource()
|
||||
dataSource.givenSearchResult(searchResults)
|
||||
val repository = MatrixUserRepository(FakeMatrixClient(SESSION_ID), dataSource)
|
||||
|
||||
val result = repository.search("some text")
|
||||
|
||||
result.test {
|
||||
skipItems(1)
|
||||
assertThat(awaitItem().results).isEqualTo(aMatrixUserList().toUserSearchResults())
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `search - does not change results if they contain searched mxid`() = runTest {
|
||||
val searchResults = aMatrixUserListWithoutUserId(A_USER_ID) + MatrixUser(userId = A_USER_ID, displayName = A_USER_NAME)
|
||||
val dataSource = FakeUserListDataSource()
|
||||
dataSource.givenSearchResult(searchResults)
|
||||
val repository = MatrixUserRepository(FakeMatrixClient(SESSION_ID), dataSource)
|
||||
|
||||
val result = repository.search(A_USER_ID.value)
|
||||
|
||||
result.test {
|
||||
skipItems(1)
|
||||
assertThat(awaitItem().results).isEqualTo(searchResults.toUserSearchResults())
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `search - gets profile results if searched mxid not in results`() = runTest {
|
||||
val userProfile = MatrixUser(userId = A_USER_ID, displayName = A_USER_NAME)
|
||||
val searchResults = aMatrixUserListWithoutUserId(A_USER_ID)
|
||||
|
||||
val dataSource = FakeUserListDataSource()
|
||||
dataSource.givenSearchResult(searchResults)
|
||||
dataSource.givenUserProfile(userProfile)
|
||||
val repository = MatrixUserRepository(FakeMatrixClient(SESSION_ID), dataSource)
|
||||
|
||||
val result = repository.search(A_USER_ID.value)
|
||||
|
||||
result.test {
|
||||
skipItems(1)
|
||||
assertThat(awaitItem().results).isEqualTo((listOf(userProfile) + searchResults).toUserSearchResults())
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `search - doesn't add profile results if searched mxid is local user and not in results`() = runTest {
|
||||
val userProfile = MatrixUser(userId = A_USER_ID, displayName = A_USER_NAME)
|
||||
val searchResults = aMatrixUserListWithoutUserId(SESSION_ID)
|
||||
|
||||
val dataSource = FakeUserListDataSource()
|
||||
dataSource.givenSearchResult(searchResults)
|
||||
dataSource.givenUserProfile(userProfile)
|
||||
val repository = MatrixUserRepository(FakeMatrixClient(SESSION_ID), dataSource)
|
||||
|
||||
val result = repository.search(SESSION_ID.value)
|
||||
|
||||
result.test {
|
||||
skipItems(1)
|
||||
assertThat(awaitItem().results).isEqualTo(searchResults.toUserSearchResults())
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `search - returns unresolved user if profile can't be loaded`() = runTest {
|
||||
val searchResults = aMatrixUserListWithoutUserId(A_USER_ID)
|
||||
|
||||
val dataSource = FakeUserListDataSource()
|
||||
dataSource.givenSearchResult(searchResults)
|
||||
dataSource.givenUserProfile(null)
|
||||
val repository = MatrixUserRepository(FakeMatrixClient(SESSION_ID), dataSource)
|
||||
|
||||
val result = repository.search(A_USER_ID.value)
|
||||
|
||||
result.test {
|
||||
skipItems(1)
|
||||
assertThat(awaitItem().results).isEqualTo(listOf(placeholderResult(isUnresolved = true)) + searchResults.toUserSearchResults())
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
private fun aMatrixUserListWithoutUserId(userId: UserId) = aMatrixUserList().filterNot { it.userId == userId }
|
||||
|
||||
private fun List<MatrixUser>.toUserSearchResults() = map { UserSearchResult(it) }
|
||||
|
||||
private fun placeholderResult(id: UserId = A_USER_ID, isUnresolved: Boolean = false) = UserSearchResult(MatrixUser(id), isUnresolved = isUnresolved)
|
||||
}
|
||||
Reference in New Issue
Block a user