diff --git a/features/selectusers/api/build.gradle.kts b/features/selectusers/api/build.gradle.kts index 8454780482..d46ed2fbf1 100644 --- a/features/selectusers/api/build.gradle.kts +++ b/features/selectusers/api/build.gradle.kts @@ -26,5 +26,6 @@ dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.designsystem) implementation(projects.libraries.uiStrings) + implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) } diff --git a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersEvents.kt b/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersEvents.kt index 2d13a3475e..e0ee6ddf68 100644 --- a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersEvents.kt +++ b/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersEvents.kt @@ -22,4 +22,5 @@ sealed interface SelectUsersEvents { data class UpdateSearchQuery(val query: String) : SelectUsersEvents data class AddToSelection(val matrixUser: MatrixUser) : SelectUsersEvents data class RemoveFromSelection(val matrixUser: MatrixUser) : SelectUsersEvents + data class OnSearchActiveChanged(val active: Boolean) : SelectUsersEvents } diff --git a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersState.kt b/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersState.kt index 5d89010399..82951b5c09 100644 --- a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersState.kt +++ b/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersState.kt @@ -23,5 +23,6 @@ data class SelectUsersState( val searchQuery: String, val searchResults: ImmutableList, val selectedUsers: ImmutableList, + val isSearchActive: Boolean, val eventSink: (SelectUsersEvents) -> Unit, ) diff --git a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersStateProvider.kt b/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersStateProvider.kt index 6f0c71a0d3..38b52ec117 100644 --- a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersStateProvider.kt +++ b/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersStateProvider.kt @@ -17,7 +17,6 @@ package io.element.android.features.selectusers.api import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.ui.model.MatrixUser import kotlinx.collections.immutable.persistentListOf @@ -25,24 +24,38 @@ import kotlinx.collections.immutable.persistentListOf open class SelectUsersStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( + // TODO add states with selectedUsers aSelectUsersState(), + aSelectUsersState().copy(isSearchActive = true), + aSelectUsersState().copy(isSearchActive = true, searchQuery = "someone"), aSelectUsersState().copy( - selectedUsers = persistentListOf( - aMatrixUser(userName = ""), - aMatrixUser(userName = "User"), - aMatrixUser(userName = "User with long name"), + isSearchActive = true, + searchQuery = "@someone:matrix.org", + searchResults = persistentListOf( + MatrixUser(id = UserId("@someone:matrix.org")), + MatrixUser(id = UserId("@someone:matrix.org"), username = "someone"), + MatrixUser( + id = UserId("@someone_with_a_very_long_matrix_identifier:a_very_long_domain.org"), + username = "hey, I am someone with a very long display name" + ), + MatrixUser(id = UserId("@someone_2:matrix.org"), username = "someone 2"), + MatrixUser(id = UserId("@someone_3:matrix.org"), username = "someone 3"), + MatrixUser(id = UserId("@someone_4:matrix.org"), username = "someone 4"), + MatrixUser(id = UserId("@someone_5:matrix.org"), username = "someone 5"), + MatrixUser(id = UserId("@someone_6:matrix.org"), username = "someone 6"), + MatrixUser(id = UserId("@someone_7:matrix.org"), username = "someone 7"), + MatrixUser(id = UserId("@someone_8:matrix.org"), username = "someone 8"), + MatrixUser(id = UserId("@someone_9:matrix.org"), username = "someone 9"), + MatrixUser(id = UserId("@someone_10:matrix.org"), username = "someone 10"), ) - ) + ), ) } fun aSelectUsersState() = SelectUsersState( + isSearchActive = false, searchQuery = "", searchResults = persistentListOf(), selectedUsers = persistentListOf(), eventSink = {} ) - -fun aMatrixUser(userName: String): MatrixUser { - return MatrixUser(id = UserId("@id"), username = userName, avatarData = AvatarData("@id", "U")) -} diff --git a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersView.kt b/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersView.kt index f49b7984f8..26e647dc3a 100644 --- a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersView.kt +++ b/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersView.kt @@ -22,11 +22,12 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close @@ -35,10 +36,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SearchBarDefaults import androidx.compose.runtime.Composable -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.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -55,9 +52,9 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight -import io.element.android.libraries.designsystem.theme.components.DockedSearchBar 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.SearchBar import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.ui.components.MatrixUserRow import io.element.android.libraries.matrix.ui.model.MatrixUser @@ -70,14 +67,9 @@ import io.element.android.libraries.ui.strings.R as StringR fun SelectUsersView( state: SelectUsersState, modifier: Modifier = Modifier, - onSearchActiveChanged: (Boolean) -> Unit = {}, - onSelectionChanged: (ImmutableList) -> Unit = {}, ) { - var isSearchActive by rememberSaveable { mutableStateOf(false) } val eventSink = state.eventSink - // TODO how to pass back the selection list? - Column( modifier = modifier .fillMaxSize() @@ -86,11 +78,8 @@ fun SelectUsersView( modifier = Modifier.fillMaxWidth(), query = state.searchQuery, results = state.searchResults, - active = isSearchActive, - onActiveChanged = { - isSearchActive = it - onSearchActiveChanged(it) - }, + active = state.isSearchActive, + onActiveChanged = { eventSink.invoke(SelectUsersEvents.OnSearchActiveChanged(it)) }, onTextChanged = { state.eventSink(SelectUsersEvents.UpdateSearchQuery(it)) }, onResultSelected = { state.eventSink(SelectUsersEvents.AddToSelection(it)) } ) @@ -133,7 +122,7 @@ fun SelectedUser( Column( horizontalAlignment = Alignment.CenterHorizontally, ) { - Avatar(matrixUser.avatarData.copy(size = AvatarSize.Custom(56))) + Avatar(matrixUser.avatarData.copy(size = AvatarSize.Custom(56.dp))) Text( text = matrixUser.getBestName(), overflow = TextOverflow.Ellipsis, @@ -177,7 +166,7 @@ fun SearchUserBar( focusManager.clearFocus() } - DockedSearchBar( + SearchBar( query = query, onQueryChange = onTextChanged, onSearch = { focusManager.clearFocus() }, @@ -193,7 +182,9 @@ fun SearchUserBar( }, leadingIcon = if (active) { { BackButton(onClick = { onActiveChanged(false) }) } - } else null, + } else { + null + }, trailingIcon = when { active && query.isNotEmpty() -> { { @@ -213,14 +204,15 @@ fun SearchUserBar( } else -> null }, - shape = if (!active) SearchBarDefaults.dockedShape else SearchBarDefaults.fullScreenShape, colors = if (!active) SearchBarDefaults.colors() else SearchBarDefaults.colors(containerColor = Color.Transparent), content = { - results.forEach { - SearchUserResultItem( - matrixUser = it, - onClick = { onResultSelected(it) } - ) + LazyColumn { + items(results) { + SearchUserResultItem( + matrixUser = it, + onClick = { onResultSelected(it) } + ) + } } }, ) @@ -233,9 +225,9 @@ fun SearchUserResultItem( onClick: () -> Unit = {}, ) { MatrixUserRow( - modifier = modifier.heightIn(min = 56.dp), + modifier = modifier, matrixUser = matrixUser, - avatarSize = AvatarSize.Custom(36), + avatarSize = AvatarSize.Custom(36.dp), onClick = onClick, ) } diff --git a/features/selectusers/impl/src/main/kotlin/io/element/android/features/selectusers/impl/DefaultSelectUsersPresenter.kt b/features/selectusers/impl/src/main/kotlin/io/element/android/features/selectusers/impl/DefaultSelectUsersPresenter.kt index 12bc44c7e9..84f1d7a950 100644 --- a/features/selectusers/impl/src/main/kotlin/io/element/android/features/selectusers/impl/DefaultSelectUsersPresenter.kt +++ b/features/selectusers/impl/src/main/kotlin/io/element/android/features/selectusers/impl/DefaultSelectUsersPresenter.kt @@ -58,11 +58,13 @@ class DefaultSelectUsersPresenter @Inject constructor() : SelectUsersPresenter { } LaunchedEffect(searchQuery) { + // Clear the search results before performing the search, manually add a fake result with the matrixId, if any searchResults.value = if (MatrixPatterns.isUserId(searchQuery)) { persistentListOf(MatrixUser(UserId(searchQuery))) } else { persistentListOf() } + // Perform the search asynchronously if (searchQuery.isNotEmpty()) { searchResults.value = performSearch(searchQuery) } diff --git a/features/selectusers/impl/src/test/kotlin/io/element/android/features/selectusers/impl/DefaultSelectUsersPresenterTests.kt b/features/selectusers/impl/src/test/kotlin/io/element/android/features/selectusers/impl/DefaultSelectUsersPresenterTests.kt new file mode 100644 index 0000000000..f00fe56d1c --- /dev/null +++ b/features/selectusers/impl/src/test/kotlin/io/element/android/features/selectusers/impl/DefaultSelectUsersPresenterTests.kt @@ -0,0 +1,62 @@ +/* + * 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.selectusers.impl + +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.selectusers.api.SelectUsersEvents +import io.element.android.libraries.matrix.api.core.UserId +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) +class DefaultSelectUsersPresenterTests { + + @Test + fun `present - initial state`() = runTest { + val presenter = DefaultSelectUsersPresenter() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState) + } + } + + @Test + fun `present - update search query`() = runTest { + val presenter = DefaultSelectUsersPresenter() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val matrixIdQuery = "@name:matrix.org" + initialState.eventSink(SelectUsersEvents.UpdateSearchQuery(matrixIdQuery)) + assertThat(awaitItem().searchQuery).isEqualTo(matrixIdQuery) + assertThat(awaitItem().searchResults).containsExactly(MatrixUser(UserId(matrixIdQuery))) + + val notMatrixIdQuery = "name" + initialState.eventSink(SelectUsersEvents.UpdateSearchQuery(notMatrixIdQuery)) + assertThat(awaitItem().searchQuery).isEqualTo(notMatrixIdQuery) + assertThat(awaitItem().searchResults).isEmpty() + } + } +}