Merge branch 'develop' into feature/fga/image_loading
This commit is contained in:
commit
63513ae2da
212 changed files with 1616 additions and 916 deletions
|
|
@ -18,8 +18,10 @@ package io.element.android.libraries.designsystem.components.preferences
|
|||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
|
@ -45,7 +47,7 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold
|
|||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun PreferenceView(
|
||||
title: String,
|
||||
|
|
@ -70,6 +72,7 @@ fun PreferenceView(
|
|||
Column(
|
||||
modifier = Modifier
|
||||
.padding(it)
|
||||
.consumeWindowInsets(it)
|
||||
.verticalScroll(
|
||||
state = scrollState,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
package io.element.android.libraries.designsystem.theme.components
|
||||
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
package io.element.android.libraries.designsystem.theme.components
|
||||
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
|
|
@ -57,6 +55,7 @@ fun MediumTopAppBar(
|
|||
internal fun MediumTopAppBarPreview() =
|
||||
ElementThemedPreview { ContentToPreview() }
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
MediumTopAppBar(title = { Text(text = "Title") })
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalMaterialApi::class, ExperimentalMaterialApi::class)
|
||||
|
||||
package io.element.android.libraries.designsystem.theme.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
|
|
@ -107,6 +105,7 @@ internal fun ModalBottomSheetLayoutLightPreview() =
|
|||
internal fun ModalBottomSheetLayoutDarkPreview() =
|
||||
ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
ModalBottomSheetLayout(
|
||||
|
|
|
|||
|
|
@ -23,10 +23,9 @@ import androidx.compose.foundation.layout.height
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.TextFieldColors
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
|
|
@ -49,7 +48,6 @@ import io.element.android.libraries.designsystem.preview.PreviewGroup
|
|||
import io.element.android.libraries.designsystem.utils.allBooleans
|
||||
import io.element.android.libraries.designsystem.utils.asInt
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun OutlinedTextField(
|
||||
value: String,
|
||||
|
|
@ -70,8 +68,8 @@ fun OutlinedTextField(
|
|||
singleLine: Boolean = false,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
shape: Shape = TextFieldDefaults.outlinedShape,
|
||||
colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors()
|
||||
shape: Shape = OutlinedTextFieldDefaults.shape,
|
||||
colors: TextFieldColors = OutlinedTextFieldDefaults.colors()
|
||||
) {
|
||||
androidx.compose.material3.OutlinedTextField(
|
||||
value = value,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ package io.element.android.libraries.designsystem.theme.components
|
|||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ScaffoldDefaults
|
||||
|
|
@ -27,7 +26,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun Scaffold(
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
|
|||
|
|
@ -14,77 +14,218 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
package io.element.android.libraries.designsystem.theme.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
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.SearchBarColors
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SearchBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||
import io.element.android.libraries.ui.strings.R
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SearchBar(
|
||||
fun <T> SearchBar(
|
||||
query: String,
|
||||
onQueryChange: (String) -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
active: Boolean,
|
||||
onActiveChange: (Boolean) -> Unit,
|
||||
placeHolderTitle: String,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
placeholder: @Composable (() -> Unit)? = null,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
resultState: SearchBarResultState<T> = SearchBarResultState.NotSearching(),
|
||||
shape: Shape = SearchBarDefaults.inputFieldShape,
|
||||
colors: SearchBarColors = SearchBarDefaults.colors(),
|
||||
tonalElevation: Dp = SearchBarDefaults.Elevation,
|
||||
windowInsets: WindowInsets = SearchBarDefaults.windowInsets,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
contentPrefix: @Composable ColumnScope.() -> Unit = {},
|
||||
contentSuffix: @Composable ColumnScope.() -> Unit = {},
|
||||
resultHandler: @Composable ColumnScope.(T) -> Unit = {},
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
if (!active) {
|
||||
onQueryChange("")
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
|
||||
androidx.compose.material3.SearchBar(
|
||||
query = query,
|
||||
onQueryChange = onQueryChange,
|
||||
onSearch = onSearch,
|
||||
onSearch = { focusManager.clearFocus() },
|
||||
active = active,
|
||||
onActiveChange = onActiveChange,
|
||||
modifier = modifier,
|
||||
modifier = modifier.padding(horizontal = if (!active) 16.dp else 0.dp),
|
||||
enabled = enabled,
|
||||
placeholder = placeholder,
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = trailingIcon,
|
||||
placeholder = {
|
||||
Text(
|
||||
text = placeHolderTitle,
|
||||
modifier = Modifier.alpha(0.4f), // FIXME align on Design system theme (removing alpha should be fine)
|
||||
)
|
||||
},
|
||||
leadingIcon = if (active) {
|
||||
{ BackButton(onClick = { onActiveChange(false) }) }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
trailingIcon = when {
|
||||
active && query.isNotEmpty() -> {
|
||||
{
|
||||
IconButton(onClick = { onQueryChange("") }) {
|
||||
Icon(Icons.Default.Close, stringResource(R.string.action_clear))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
!active -> {
|
||||
{
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = stringResource(R.string.action_search),
|
||||
modifier = Modifier.alpha(0.4f), // FIXME align on Design system theme (removing alpha should be fine)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> null
|
||||
},
|
||||
shape = shape,
|
||||
colors = colors,
|
||||
colors = if (!active) SearchBarDefaults.colors() else SearchBarDefaults.colors(containerColor = Color.Transparent),
|
||||
tonalElevation = tonalElevation,
|
||||
windowInsets = windowInsets,
|
||||
interactionSource = interactionSource,
|
||||
content = content,
|
||||
content = {
|
||||
contentPrefix()
|
||||
when (resultState) {
|
||||
is SearchBarResultState.Results<T> -> {
|
||||
resultHandler(resultState.results)
|
||||
}
|
||||
|
||||
is SearchBarResultState.NoResults<T> -> {
|
||||
// No results found, show a message
|
||||
Spacer(Modifier.size(80.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.common_no_results),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
// Not searching - nothing to show.
|
||||
}
|
||||
}
|
||||
contentSuffix()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
sealed interface SearchBarResultState<in T> {
|
||||
/** No search results are available yet (e.g. because the user hasn't entered a search term). */
|
||||
class NotSearching<T> : SearchBarResultState<T>
|
||||
|
||||
/** The search has completed, but no results were found. */
|
||||
class NoResults<T> : SearchBarResultState<T>
|
||||
|
||||
/** The search has completed, and some matching users were found. */
|
||||
data class Results<T>(val results: T) : SearchBarResultState<T>
|
||||
}
|
||||
|
||||
@Preview(group = PreviewGroup.Search)
|
||||
@Composable
|
||||
internal fun SearchBarPreviewInactive() = ElementThemedPreview { ContentToPreview() }
|
||||
|
||||
@Preview(group = PreviewGroup.Search)
|
||||
@Composable
|
||||
internal fun SearchBarPreviewActiveEmptyQuery() = ElementThemedPreview {
|
||||
ContentToPreview(
|
||||
query = "",
|
||||
active = true,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(group = PreviewGroup.Search)
|
||||
@Composable
|
||||
internal fun DockedSearchBarPreview() = ElementThemedPreview { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
SearchBar(
|
||||
query = "Some text",
|
||||
onQueryChange = {},
|
||||
onSearch = {},
|
||||
active = false,
|
||||
onActiveChange = {},
|
||||
content = {},
|
||||
internal fun SearchBarPreviewActiveWithQuery() = ElementThemedPreview {
|
||||
ContentToPreview(
|
||||
query = "search term",
|
||||
active = true,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(group = PreviewGroup.Search)
|
||||
@Composable
|
||||
internal fun SearchBarPreviewActiveWithNoResults() = ElementThemedPreview {
|
||||
ContentToPreview(
|
||||
query = "search term",
|
||||
active = true,
|
||||
resultState = SearchBarResultState.NoResults(),
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(group = PreviewGroup.Search)
|
||||
@Composable
|
||||
internal fun SearchBarPreviewActiveWithContent() = ElementThemedPreview {
|
||||
ContentToPreview(
|
||||
query = "search term",
|
||||
active = true,
|
||||
resultState = SearchBarResultState.Results("result!"),
|
||||
contentPrefix = {
|
||||
Text(text = "Content that goes before the search results", modifier = Modifier.background(color = Color.Red).fillMaxWidth())
|
||||
},
|
||||
contentSuffix = {
|
||||
Text(text = "Content that goes after the search results", modifier = Modifier.background(color = Color.Blue).fillMaxWidth())
|
||||
},
|
||||
resultHandler = {
|
||||
Text(text = "Results go here", modifier = Modifier.background(color = Color.Green).fillMaxWidth())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview(
|
||||
query: String = "",
|
||||
active: Boolean = false,
|
||||
resultState: SearchBarResultState<String> = SearchBarResultState.NotSearching(),
|
||||
contentPrefix: @Composable ColumnScope.() -> Unit = {},
|
||||
contentSuffix: @Composable ColumnScope.() -> Unit = {},
|
||||
resultHandler: @Composable ColumnScope.(String) -> Unit = {},
|
||||
) {
|
||||
SearchBar(
|
||||
query = query,
|
||||
active = active,
|
||||
resultState = resultState,
|
||||
onQueryChange = {},
|
||||
onActiveChange = {},
|
||||
placeHolderTitle = "Search for things",
|
||||
contentPrefix = contentPrefix,
|
||||
contentSuffix = contentSuffix,
|
||||
resultHandler = resultHandler,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import androidx.compose.foundation.layout.height
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.TextFieldColors
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
|
|
@ -50,7 +49,6 @@ import io.element.android.libraries.designsystem.preview.PreviewGroup
|
|||
import io.element.android.libraries.designsystem.utils.allBooleans
|
||||
import io.element.android.libraries.designsystem.utils.asInt
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TextField(
|
||||
value: String,
|
||||
|
|
@ -71,8 +69,8 @@ fun TextField(
|
|||
singleLine: Boolean = false,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
shape: Shape = TextFieldDefaults.filledShape,
|
||||
colors: TextFieldColors = TextFieldDefaults.textFieldColors()
|
||||
shape: Shape = TextFieldDefaults.shape,
|
||||
colors: TextFieldColors = TextFieldDefaults.colors()
|
||||
) {
|
||||
androidx.compose.material3.TextField(
|
||||
value = value,
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
package io.element.android.libraries.designsystem.theme.components
|
||||
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ interface MatrixClient : Closeable {
|
|||
suspend fun createRoom(createRoomParams: CreateRoomParameters): Result<RoomId>
|
||||
suspend fun createDM(userId: UserId): Result<RoomId>
|
||||
suspend fun getProfile(userId: UserId): Result<MatrixUser>
|
||||
suspend fun searchUsers(searchTerm: String, limit: Long): Result<MatrixSearchUserResults>
|
||||
fun startSync()
|
||||
fun stopSync()
|
||||
fun sessionVerificationService(): SessionVerificationService
|
||||
|
|
@ -51,10 +52,7 @@ interface MatrixClient : Closeable {
|
|||
suspend fun logout()
|
||||
suspend fun loadUserDisplayName(): Result<String>
|
||||
suspend fun loadUserAvatarURLString(): Result<String?>
|
||||
|
||||
suspend fun uploadMedia(mimeType: String, data: ByteArray): Result<String>
|
||||
fun onSlidingSyncUpdate()
|
||||
|
||||
fun roomMembershipObserver(): RoomMembershipObserver
|
||||
|
||||
suspend fun searchUsers(searchTerm: String, limit: Long): Result<MatrixSearchUserResults>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.libraries.matrix.impl
|
||||
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
|
|
@ -46,7 +44,6 @@ import io.element.android.libraries.matrix.impl.verification.RustSessionVerifica
|
|||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
|
|
@ -284,6 +281,13 @@ class RustMatrixClient constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun searchUsers(searchTerm: String, limit: Long): Result<MatrixSearchUserResults> =
|
||||
withContext(dispatchers.io) {
|
||||
runCatching {
|
||||
client.searchUsers(searchTerm, limit.toULong()).let(UserSearchResultMapper::map)
|
||||
}
|
||||
}
|
||||
|
||||
override fun sessionVerificationService(): SessionVerificationService = verificationService
|
||||
|
||||
override fun pushersService(): PushersService = pushersService
|
||||
|
|
@ -341,6 +345,13 @@ class RustMatrixClient constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalUnsignedTypes::class)
|
||||
override suspend fun uploadMedia(mimeType: String, data: ByteArray): Result<String> = withContext(dispatchers.io) {
|
||||
runCatching {
|
||||
client.uploadMedia(mimeType, data.toUByteArray().toList())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSlidingSyncUpdate() {
|
||||
if (!verificationService.isReady.value) {
|
||||
try {
|
||||
|
|
@ -353,13 +364,6 @@ class RustMatrixClient constructor(
|
|||
|
||||
override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver
|
||||
|
||||
override suspend fun searchUsers(searchTerm: String, limit: Long): Result<MatrixSearchUserResults> =
|
||||
withContext(dispatchers.io) {
|
||||
runCatching {
|
||||
client.searchUsers(searchTerm, limit.toULong()).let(UserSearchResultMapper::map)
|
||||
}
|
||||
}
|
||||
|
||||
private fun File.deleteSessionDirectory(userID: String): Boolean {
|
||||
// Rust sanitises the user ID replacing invalid characters with an _
|
||||
val sanitisedUserID = userID.replace(":", "_")
|
||||
|
|
|
|||
|
|
@ -91,7 +91,6 @@ internal class RustRoomSummaryDataSource(
|
|||
coroutineScope.cancel()
|
||||
}
|
||||
|
||||
//@OptIn(FlowPreview::class)
|
||||
override fun roomSummaries(): StateFlow<List<RoomSummary>> {
|
||||
return roomSummaries
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ class FakeMatrixClient(
|
|||
private val getRoomResults = mutableMapOf<RoomId, MatrixRoom>()
|
||||
private val searchUserResults = mutableMapOf<String, Result<MatrixSearchUserResults>>()
|
||||
private val getProfileResults = mutableMapOf<UserId, Result<MatrixUser>>()
|
||||
private var uploadMediaResult: Result<String> = Result.success(AN_AVATAR_URL)
|
||||
|
||||
override fun getRoom(roomId: RoomId): MatrixRoom? {
|
||||
return getRoomResults[roomId]
|
||||
|
|
@ -92,6 +93,10 @@ class FakeMatrixClient(
|
|||
return getProfileResults[userId] ?: Result.failure(IllegalStateException("No profile found for $userId"))
|
||||
}
|
||||
|
||||
override suspend fun searchUsers(searchTerm: String, limit: Long): Result<MatrixSearchUserResults> {
|
||||
return searchUserResults[searchTerm] ?: Result.failure(IllegalStateException("No response defined for $searchTerm"))
|
||||
}
|
||||
|
||||
override fun startSync() = Unit
|
||||
|
||||
override fun stopSync() = Unit
|
||||
|
|
@ -111,6 +116,10 @@ class FakeMatrixClient(
|
|||
return userAvatarURLString
|
||||
}
|
||||
|
||||
override suspend fun uploadMedia(mimeType: String, data: ByteArray): Result<String> {
|
||||
return uploadMediaResult
|
||||
}
|
||||
|
||||
override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService
|
||||
|
||||
override fun pushersService(): PushersService = pushersService
|
||||
|
|
@ -123,10 +132,6 @@ class FakeMatrixClient(
|
|||
return RoomMembershipObserver()
|
||||
}
|
||||
|
||||
override suspend fun searchUsers(searchTerm: String, limit: Long): Result<MatrixSearchUserResults> {
|
||||
return searchUserResults[searchTerm] ?: Result.failure(IllegalStateException("No response defined for $searchTerm"))
|
||||
}
|
||||
|
||||
// Mocks
|
||||
|
||||
fun givenLogoutError(failure: Throwable?) {
|
||||
|
|
@ -168,4 +173,8 @@ class FakeMatrixClient(
|
|||
fun givenGetProfileResult(userId: UserId, result: Result<MatrixUser>) {
|
||||
getProfileResults[userId] = result
|
||||
}
|
||||
|
||||
fun givenUploadMediaResult(result: Result<String>) {
|
||||
uploadMediaResult = result
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ dependencies {
|
|||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(libs.coil.compose)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
|
|
|
|||
|
|
@ -26,11 +26,14 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Checkbox
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.matrix.ui.model.getBestName
|
||||
|
||||
@Composable
|
||||
fun CheckableMatrixUserRow(
|
||||
|
|
@ -40,18 +43,39 @@ fun CheckableMatrixUserRow(
|
|||
avatarSize: AvatarSize = AvatarSize.MEDIUM,
|
||||
onCheckedChange: (Boolean) -> Unit = {},
|
||||
enabled: Boolean = true,
|
||||
) {
|
||||
) = CheckableUserRow(
|
||||
checked = checked,
|
||||
avatarData = matrixUser.getAvatarData(avatarSize),
|
||||
name = matrixUser.getBestName(),
|
||||
subtext = if (matrixUser.displayName.isNullOrEmpty()) null else matrixUser.userId.value,
|
||||
modifier = modifier,
|
||||
onCheckedChange = onCheckedChange,
|
||||
enabled = enabled,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun CheckableUserRow(
|
||||
checked: Boolean,
|
||||
avatarData: AvatarData,
|
||||
name: String,
|
||||
subtext: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
onCheckedChange: (Boolean) -> Unit = {},
|
||||
enabled: Boolean = true,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(role = Role.Checkbox) { onCheckedChange(!checked) },
|
||||
.clickable(role = Role.Checkbox, enabled = enabled) {
|
||||
onCheckedChange(!checked)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
MatrixUserRow(
|
||||
UserRow(
|
||||
modifier = Modifier.weight(1f),
|
||||
matrixUser = matrixUser,
|
||||
avatarSize = avatarSize,
|
||||
avatarData = avatarData,
|
||||
name = name,
|
||||
subtext = subtext,
|
||||
)
|
||||
|
||||
Checkbox(
|
||||
|
|
@ -77,5 +101,7 @@ private fun ContentToPreview(matrixUser: MatrixUser) {
|
|||
Column {
|
||||
CheckableMatrixUserRow(checked = true, matrixUser)
|
||||
CheckableMatrixUserRow(checked = false, matrixUser)
|
||||
CheckableMatrixUserRow(checked = true, matrixUser, enabled = false)
|
||||
CheckableMatrixUserRow(checked = false, matrixUser, enabled = false)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
|
|
@ -46,6 +47,19 @@ fun MatrixUserRow(
|
|||
matrixUser: MatrixUser,
|
||||
modifier: Modifier = Modifier,
|
||||
avatarSize: AvatarSize = AvatarSize.MEDIUM,
|
||||
) = UserRow(
|
||||
avatarData = matrixUser.getAvatarData(avatarSize),
|
||||
name = matrixUser.getBestName(),
|
||||
subtext = if (matrixUser.displayName.isNullOrEmpty()) null else matrixUser.userId.value,
|
||||
modifier = modifier,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun UserRow(
|
||||
avatarData: AvatarData,
|
||||
name: String,
|
||||
subtext: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
|
|
@ -54,9 +68,7 @@ fun MatrixUserRow(
|
|||
.height(IntrinsicSize.Min),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Avatar(
|
||||
matrixUser.getAvatarData(size = avatarSize),
|
||||
)
|
||||
Avatar(avatarData)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp),
|
||||
|
|
@ -65,15 +77,15 @@ fun MatrixUserRow(
|
|||
Text(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
text = matrixUser.getBestName(),
|
||||
text = name,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
// Id
|
||||
if (matrixUser.displayName.isNullOrEmpty().not()) {
|
||||
subtext?.let {
|
||||
Text(
|
||||
text = matrixUser.userId.value,
|
||||
text = subtext,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
fontSize = 14.sp,
|
||||
maxLines = 1,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* 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.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
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.Text
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.matrix.ui.model.getBestName
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@Composable
|
||||
fun SelectedUser(
|
||||
matrixUser: MatrixUser,
|
||||
modifier: Modifier = Modifier,
|
||||
onUserRemoved: (MatrixUser) -> Unit = {},
|
||||
) {
|
||||
Box(modifier = modifier.width(56.dp)) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Avatar(matrixUser.getAvatarData(size = AvatarSize.Custom(56.dp)))
|
||||
Text(
|
||||
text = matrixUser.getBestName(),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primary)
|
||||
.size(20.dp)
|
||||
.align(Alignment.TopEnd),
|
||||
onClick = { onUserRemoved(matrixUser) }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = stringResource(id = StringR.string.action_remove),
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun SelectedUserLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun SelectedUserDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
SelectedUser(aMatrixUser())
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
fun SelectedUsersList(
|
||||
selectedUsers: ImmutableList<MatrixUser>,
|
||||
modifier: Modifier = Modifier,
|
||||
autoScroll: Boolean = false,
|
||||
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||
onUserRemoved: (MatrixUser) -> Unit = {},
|
||||
) {
|
||||
val lazyListState = rememberLazyListState()
|
||||
if (autoScroll) {
|
||||
var currentSize by rememberSaveable { mutableStateOf(selectedUsers.size) }
|
||||
LaunchedEffect(selectedUsers.size) {
|
||||
val isItemAdded = selectedUsers.size > currentSize
|
||||
if (isItemAdded) {
|
||||
lazyListState.animateScrollToItem(selectedUsers.lastIndex)
|
||||
}
|
||||
currentSize = selectedUsers.size
|
||||
}
|
||||
}
|
||||
|
||||
LazyRow(
|
||||
state = lazyListState,
|
||||
modifier = modifier,
|
||||
contentPadding = contentPadding,
|
||||
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) {
|
||||
items(selectedUsers.toList()) { matrixUser ->
|
||||
SelectedUser(
|
||||
matrixUser = matrixUser,
|
||||
onUserRemoved = onUserRemoved,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun SelectedUsersListLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun SelectedUsersListDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
SelectedUsersList(
|
||||
selectedUsers = aMatrixUserList().take(6).toImmutableList(),
|
||||
)
|
||||
}
|
||||
|
|
@ -27,6 +27,11 @@ interface PickerProvider {
|
|||
onResult: (uri: Uri?, mimeType: String?) -> Unit
|
||||
): PickerLauncher<PickVisualMediaRequest, Uri?>
|
||||
|
||||
@Composable
|
||||
fun registerGalleryImagePicker(
|
||||
onResult: (Uri?) -> Unit
|
||||
): PickerLauncher<PickVisualMediaRequest, Uri?>
|
||||
|
||||
@Composable
|
||||
fun registerFilePicker(
|
||||
mimeType: String,
|
||||
|
|
@ -38,5 +43,4 @@ interface PickerProvider {
|
|||
|
||||
@Composable
|
||||
fun registerCameraVideoPicker(onResult: (Uri?) -> Unit): PickerLauncher<Uri, Boolean>
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,13 @@ sealed interface PickerType<Input, Output> {
|
|||
fun getContract(): ActivityResultContract<Input, Output>
|
||||
fun getDefaultRequest(): Input
|
||||
|
||||
object Image : PickerType<PickVisualMediaRequest, Uri?> {
|
||||
override fun getContract() = ActivityResultContracts.PickVisualMedia()
|
||||
override fun getDefaultRequest(): PickVisualMediaRequest {
|
||||
return PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
|
||||
}
|
||||
}
|
||||
|
||||
object ImageAndVideo : PickerType<PickVisualMediaRequest, Uri?> {
|
||||
override fun getContract() = ActivityResultContracts.PickVisualMedia()
|
||||
override fun getDefaultRequest(): PickVisualMediaRequest {
|
||||
|
|
|
|||
|
|
@ -59,6 +59,20 @@ class PickerProviderImpl constructor(private val isInTest: Boolean) : PickerProv
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remembers and returns a [PickerLauncher] for a gallery picture.
|
||||
* [onResult] will be called with either the selected file's [Uri] or `null` if nothing was selected.
|
||||
*/
|
||||
@Composable
|
||||
override fun registerGalleryImagePicker(onResult: (Uri?) -> Unit): PickerLauncher<PickVisualMediaRequest, Uri?> {
|
||||
// Tests and UI preview can't handle Contexts, so we might as well disable the whole picker
|
||||
return if (LocalInspectionMode.current || isInTest) {
|
||||
NoOpPickerLauncher { onResult(null) }
|
||||
} else {
|
||||
rememberPickerLauncher(type = PickerType.Image) { uri -> onResult(uri) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remembers and returns a [PickerLauncher] for a gallery item, either a picture or a video.
|
||||
* [onResult] will be called with either the selected file's [Uri] or `null` if nothing was selected.
|
||||
|
|
|
|||
|
|
@ -33,6 +33,11 @@ class FakePickerProvider : PickerProvider {
|
|||
return NoOpPickerLauncher { onResult(result, mimeType) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun registerGalleryImagePicker(onResult: (uri: Uri?) -> Unit): PickerLauncher<PickVisualMediaRequest, Uri?> {
|
||||
return NoOpPickerLauncher { onResult(result) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun registerFilePicker(mimeType: String, onResult: (Uri?) -> Unit): PickerLauncher<String, Uri?> {
|
||||
return NoOpPickerLauncher { onResult(result) }
|
||||
|
|
|
|||
|
|
@ -24,10 +24,13 @@ import io.element.android.libraries.matrix.api.media.VideoInfo
|
|||
import java.io.File
|
||||
|
||||
sealed interface MediaUploadInfo {
|
||||
data class Image(val file: File, val info: ImageInfo, val thumbnailInfo: ThumbnailProcessingInfo) : MediaUploadInfo
|
||||
data class Video(val file: File, val info: VideoInfo, val thumbnailInfo: ThumbnailProcessingInfo) : MediaUploadInfo
|
||||
data class Audio(val file: File, val info: AudioInfo) : MediaUploadInfo
|
||||
data class AnyFile(val file: File, val info: FileInfo) : MediaUploadInfo
|
||||
|
||||
val file: File
|
||||
|
||||
data class Image(override val file: File, val info: ImageInfo, val thumbnailInfo: ThumbnailProcessingInfo) : MediaUploadInfo
|
||||
data class Video(override val file: File, val info: VideoInfo, val thumbnailInfo: ThumbnailProcessingInfo) : MediaUploadInfo
|
||||
data class Audio(override val file: File, val info: AudioInfo) : MediaUploadInfo
|
||||
data class AnyFile(override val file: File, val info: FileInfo) : MediaUploadInfo
|
||||
}
|
||||
|
||||
data class ThumbnailProcessingInfo(
|
||||
|
|
|
|||
|
|
@ -130,19 +130,15 @@ class AndroidMediaPreProcessor @Inject constructor(
|
|||
}.mapFailure { MediaPreProcessor.Failure(it) }
|
||||
|
||||
private fun MediaUploadInfo.postProcess(uri: Uri): MediaUploadInfo {
|
||||
|
||||
fun File.rename(name: String): File {
|
||||
return File(context.cacheDir, name).also {
|
||||
renameTo(it)
|
||||
}
|
||||
}
|
||||
|
||||
val name = context.getFileName(uri) ?: return this
|
||||
val renamedFile = File(context.cacheDir, name).also {
|
||||
file.renameTo(it)
|
||||
}
|
||||
return when (this) {
|
||||
is MediaUploadInfo.AnyFile -> copy(file = file.rename(name))
|
||||
is MediaUploadInfo.Audio -> copy(file = file.rename(name))
|
||||
is MediaUploadInfo.Image -> copy(file = file.rename(name))
|
||||
is MediaUploadInfo.Video -> copy(file = file.rename(name))
|
||||
is MediaUploadInfo.AnyFile -> copy(file = renamedFile)
|
||||
is MediaUploadInfo.Audio -> copy(file = renamedFile)
|
||||
is MediaUploadInfo.Image -> copy(file = renamedFile)
|
||||
is MediaUploadInfo.Video -> copy(file = renamedFile)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@
|
|||
<string name="common_developer_options">"Developer options"</string>
|
||||
<string name="common_edited_suffix">"(edited)"</string>
|
||||
<string name="common_editing">"Editing"</string>
|
||||
<string name="common_emote">"* %1$s %2$s"</string>
|
||||
<string name="common_encryption_enabled">"Encryption enabled"</string>
|
||||
<string name="common_error">"Error"</string>
|
||||
<string name="common_file">"File"</string>
|
||||
|
|
@ -84,12 +85,14 @@
|
|||
<string name="common_report_a_bug">"Report a bug"</string>
|
||||
<string name="common_report_submitted">"Report submitted"</string>
|
||||
<string name="common_search_for_someone">"Search for someone"</string>
|
||||
<string name="common_search_results">"Search results"</string>
|
||||
<string name="common_security">"Security"</string>
|
||||
<string name="common_select_your_server">"Select your server"</string>
|
||||
<string name="common_sending">"Sending…"</string>
|
||||
<string name="common_server_not_supported">"Server not supported"</string>
|
||||
<string name="common_server_url">"Server URL"</string>
|
||||
<string name="common_settings">"Settings"</string>
|
||||
<string name="common_starting_chat">"Starting chat…"</string>
|
||||
<string name="common_sticker">"Sticker"</string>
|
||||
<string name="common_success">"Success"</string>
|
||||
<string name="common_suggestions">"Suggestions"</string>
|
||||
|
|
@ -159,4 +162,4 @@
|
|||
<string name="screen_analytics_settings_read_terms">"You can read all our terms %1$s."</string>
|
||||
<string name="screen_analytics_settings_read_terms_content_link">"here"</string>
|
||||
<string name="screen_report_content_block_user">"Block user"</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
|||
28
libraries/usersearch/api/build.gradle.kts
Normal file
28
libraries/usersearch/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.usersearch.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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.usersearch.api
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
||||
interface UserListDataSource {
|
||||
//TODO should probably have a flow
|
||||
suspend fun search(query: String): List<MatrixUser>
|
||||
suspend fun getProfile(userId: UserId): MatrixUser?
|
||||
}
|
||||
|
|
@ -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.usersearch.api
|
||||
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface UserRepository {
|
||||
|
||||
suspend fun search(query: String): Flow<List<MatrixUser>>
|
||||
}
|
||||
45
libraries/usersearch/impl/build.gradle.kts
Normal file
45
libraries/usersearch/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
alias(libs.plugins.anvil)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.usersearch.impl"
|
||||
}
|
||||
|
||||
anvil {
|
||||
generateDaggerFactories.set(true)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.usersearch.test)
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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.usersearch.impl
|
||||
|
||||
import com.squareup.anvil.annotations.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
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class MatrixUserListDataSource @Inject constructor(
|
||||
private val client: MatrixClient
|
||||
) : UserListDataSource {
|
||||
override suspend fun search(query: String): List<MatrixUser> {
|
||||
val res = client.searchUsers(query, MAX_SEARCH_RESULTS)
|
||||
return res.getOrNull()?.results.orEmpty()
|
||||
}
|
||||
|
||||
override suspend fun getProfile(userId: UserId): MatrixUser? {
|
||||
return client.getProfile(userId).getOrNull()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_SEARCH_RESULTS = 5L
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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.usersearch.impl
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
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.api.user.MatrixUser
|
||||
import io.element.android.libraries.usersearch.api.UserListDataSource
|
||||
import io.element.android.libraries.usersearch.api.UserRepository
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class MatrixUserRepository @Inject constructor(
|
||||
private val dataSource: UserListDataSource
|
||||
) : UserRepository {
|
||||
|
||||
override suspend fun search(query: String): Flow<List<MatrixUser>> = flow {
|
||||
// Manually add a fake result with the matrixId, if any
|
||||
val isUserId = MatrixPatterns.isUserId(query)
|
||||
if (isUserId) {
|
||||
emit(listOf(MatrixUser(UserId(query))))
|
||||
}
|
||||
|
||||
if (query.length >= MINIMUM_SEARCH_LENGTH) {
|
||||
// Debounce
|
||||
delay(DEBOUNCE_TIME_MILLIS)
|
||||
|
||||
val results = dataSource.search(query).toMutableList()
|
||||
|
||||
// If the query is a user ID and the result doesn't contain that user ID, query the profile information explicitly
|
||||
if (isUserId && results.none { it.userId.value == query }) {
|
||||
val getProfileResult: MatrixUser? = dataSource.getProfile(UserId(query))
|
||||
val profile = getProfileResult ?: MatrixUser(UserId(query))
|
||||
results.add(0, profile)
|
||||
}
|
||||
|
||||
emit(results)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEBOUNCE_TIME_MILLIS = 500L
|
||||
private const val MINIMUM_SEARCH_LENGTH = 3
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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.usersearch.impl
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
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.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 = listOf(
|
||||
aMatrixUserProfile(),
|
||||
aMatrixUserProfile(userId = A_USER_ID_2)
|
||||
),
|
||||
limited = false
|
||||
)
|
||||
)
|
||||
)
|
||||
val dataSource = MatrixUserListDataSource(matrixClient)
|
||||
|
||||
val results = dataSource.search("test")
|
||||
Truth.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(Throwable("Ruhroh"))
|
||||
)
|
||||
val dataSource = MatrixUserListDataSource(matrixClient)
|
||||
|
||||
val results = dataSource.search("test")
|
||||
Truth.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)
|
||||
Truth.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(Throwable("Ruhroh"))
|
||||
)
|
||||
val dataSource = MatrixUserListDataSource(matrixClient)
|
||||
|
||||
val result = dataSource.getProfile(A_USER_ID)
|
||||
Truth.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)
|
||||
}
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* 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.usersearch.impl
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
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.ui.components.aMatrixUserList
|
||||
import io.element.android.libraries.usersearch.test.FakeUserListDataSource
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
internal class MatrixUserRepositoryTest {
|
||||
|
||||
@Test
|
||||
fun `search - emits nothing if the search query is too short`() = runTest {
|
||||
val dataSource = FakeUserListDataSource()
|
||||
val repository = MatrixUserRepository(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(dataSource)
|
||||
|
||||
val result = repository.search("some query")
|
||||
|
||||
result.test {
|
||||
assertThat(awaitItem()).isEmpty()
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `search - returns users if results are found`() = runTest {
|
||||
val dataSource = FakeUserListDataSource()
|
||||
dataSource.givenSearchResult(aMatrixUserList())
|
||||
val repository = MatrixUserRepository(dataSource)
|
||||
|
||||
val result = repository.search("some query")
|
||||
|
||||
result.test {
|
||||
assertThat(awaitItem()).isEqualTo(aMatrixUserList())
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `search - immediately returns placeholder if search is mxid`() = runTest {
|
||||
val dataSource = FakeUserListDataSource()
|
||||
val repository = MatrixUserRepository(dataSource)
|
||||
|
||||
val result = repository.search(A_USER_ID.value)
|
||||
|
||||
result.test {
|
||||
assertThat(awaitItem()).isEqualTo(listOf(MatrixUser(userId = A_USER_ID)))
|
||||
skipItems(1)
|
||||
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(dataSource)
|
||||
|
||||
val result = repository.search(A_USER_ID.value)
|
||||
|
||||
result.test {
|
||||
skipItems(1)
|
||||
assertThat(awaitItem()).isEqualTo(searchResults)
|
||||
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(dataSource)
|
||||
|
||||
val result = repository.search(A_USER_ID.value)
|
||||
|
||||
result.test {
|
||||
skipItems(1)
|
||||
assertThat(awaitItem()).isEqualTo(listOf(userProfile) + searchResults)
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `search - just shows id 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(dataSource)
|
||||
|
||||
val result = repository.search(A_USER_ID.value)
|
||||
|
||||
result.test {
|
||||
skipItems(1)
|
||||
assertThat(awaitItem()).isEqualTo(listOf(MatrixUser(userId = A_USER_ID)) + searchResults)
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
private fun aMatrixUserListWithoutUserId(userId: UserId) = aMatrixUserList().filterNot { it.userId == userId }
|
||||
|
||||
}
|
||||
30
libraries/usersearch/test/build.gradle.kts
Normal file
30
libraries/usersearch/test/build.gradle.kts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.usersearch"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
api(projects.libraries.usersearch.api)
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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.usersearch.test
|
||||
|
||||
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
|
||||
|
||||
class FakeUserListDataSource : UserListDataSource {
|
||||
|
||||
private var searchResult: List<MatrixUser> = emptyList()
|
||||
private var profile: MatrixUser? = null
|
||||
|
||||
override suspend fun search(query: String): List<MatrixUser> = searchResult
|
||||
|
||||
override suspend fun getProfile(userId: UserId): MatrixUser? = profile
|
||||
|
||||
fun givenSearchResult(users: List<MatrixUser>) {
|
||||
this.searchResult = users
|
||||
}
|
||||
|
||||
fun givenUserProfile(matrixUser: MatrixUser?) {
|
||||
this.profile = matrixUser
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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.usersearch.test
|
||||
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.usersearch.api.UserRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
||||
class FakeUserRepository : UserRepository {
|
||||
|
||||
var providedQuery: String? = null
|
||||
private set
|
||||
|
||||
private val flow = MutableSharedFlow<List<MatrixUser>>()
|
||||
|
||||
override suspend fun search(query: String): Flow<List<MatrixUser>> {
|
||||
providedQuery = query
|
||||
return flow
|
||||
}
|
||||
|
||||
suspend fun emitResult(result: List<MatrixUser>) {
|
||||
flow.emit(result)
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue