diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt index c9fd95339c..9c29bbf576 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt @@ -17,19 +17,19 @@ package io.element.android.features.createroom.impl.root import androidx.compose.runtime.Composable -import io.element.android.features.selectusers.api.SelectUsersPresenter +import io.element.android.features.selectusers.api.SelectSingleUserPresenter import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.ui.model.MatrixUser import timber.log.Timber import javax.inject.Inject class CreateRoomRootPresenter @Inject constructor( - private val selectUsersPresenter: SelectUsersPresenter, + private val selectSingleUserPresenter: SelectSingleUserPresenter, ) : Presenter { @Composable override fun present(): CreateRoomRootState { - val selectUsersState = selectUsersPresenter.present() + val selectUsersState = selectSingleUserPresenter.present() fun handleEvents(event: CreateRoomRootEvents) { when (event) { diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt index e129c98fd4..678f02476c 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt @@ -17,10 +17,7 @@ package io.element.android.features.createroom.impl.root import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.selectusers.api.SelectUsersState -import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.ui.model.MatrixUser -import kotlinx.collections.immutable.persistentListOf +import io.element.android.features.selectusers.api.aSelectUsersState open class CreateRoomRootStateProvider : PreviewParameterProvider { override val values: Sequence @@ -31,11 +28,5 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider diff --git a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersPresenter.kt b/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectSingleUserPresenter.kt similarity index 91% rename from features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersPresenter.kt rename to features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectSingleUserPresenter.kt index de889c80b3..a0755e4375 100644 --- a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersPresenter.kt +++ b/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectSingleUserPresenter.kt @@ -18,4 +18,4 @@ package io.element.android.features.selectusers.api import io.element.android.libraries.architecture.Presenter -interface SelectUsersPresenter : Presenter +interface SelectSingleUserPresenter : Presenter 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 82951b5c09..4be92a0003 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 @@ -24,5 +24,6 @@ data class SelectUsersState( val searchResults: ImmutableList, val selectedUsers: ImmutableList, val isSearchActive: Boolean, + val isMultiSelectionEnabled: 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 38b52ec117..0b57957afa 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 @@ -24,31 +24,23 @@ 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(isSearchActive = true, searchQuery = "someone", isMultiSelectionEnabled = true), aSelectUsersState().copy( 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"), - ) + selectedUsers = aListOfSelectedUsers(), + searchResults = aListOfResults(), ), + aSelectUsersState().copy( + isSearchActive = true, + searchQuery = "@someone:matrix.org", + isMultiSelectionEnabled = true, + selectedUsers = aListOfSelectedUsers(), + searchResults = aListOfResults(), + ) ) } @@ -57,5 +49,29 @@ fun aSelectUsersState() = SelectUsersState( searchQuery = "", searchResults = persistentListOf(), selectedUsers = persistentListOf(), + isMultiSelectionEnabled = false, eventSink = {} ) + +fun aListOfSelectedUsers() = persistentListOf( + MatrixUser(id = UserId("@someone:matrix.org")), + MatrixUser(id = UserId("@someone:matrix.org"), username = "someone"), +) + +fun aListOfResults() = 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"), +) 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 26e647dc3a..53c1c21e02 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 @@ -20,7 +20,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement 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.padding import androidx.compose.foundation.layout.size @@ -62,7 +61,6 @@ import io.element.android.libraries.matrix.ui.model.getBestName import kotlinx.collections.immutable.ImmutableList import io.element.android.libraries.ui.strings.R as StringR -@OptIn(ExperimentalMaterial3Api::class) @Composable fun SelectUsersView( state: SelectUsersState, @@ -71,28 +69,119 @@ fun SelectUsersView( val eventSink = state.eventSink Column( - modifier = modifier - .fillMaxSize() + modifier = modifier, ) { SearchUserBar( modifier = Modifier.fillMaxWidth(), query = state.searchQuery, results = state.searchResults, + selectedUsers = state.selectedUsers, active = state.isSearchActive, + isMultiSelectionEnabled = state.isMultiSelectionEnabled, onActiveChanged = { eventSink.invoke(SelectUsersEvents.OnSearchActiveChanged(it)) }, onTextChanged = { state.eventSink(SelectUsersEvents.UpdateSearchQuery(it)) }, - onResultSelected = { state.eventSink(SelectUsersEvents.AddToSelection(it)) } - ) - - // TODO move into search content - SelectedUsersList( - modifier = Modifier.padding(16.dp), - selectedUsers = state.selectedUsers, - onUserRemoved = { eventSink(SelectUsersEvents.RemoveFromSelection(it)) } + onResultSelected = { state.eventSink(SelectUsersEvents.AddToSelection(it)) }, + onUserRemoved = { eventSink(SelectUsersEvents.RemoveFromSelection(it)) }, ) } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SearchUserBar( + query: String, + results: ImmutableList, + selectedUsers: ImmutableList, + active: Boolean, + isMultiSelectionEnabled: Boolean, + modifier: Modifier = Modifier, + placeHolderTitle: String = stringResource(StringR.string.search_for_someone), + onActiveChanged: (Boolean) -> Unit = {}, + onTextChanged: (String) -> Unit = {}, + onResultSelected: (MatrixUser) -> Unit = {}, + onUserRemoved: (MatrixUser) -> Unit = {}, +) { + val focusManager = LocalFocusManager.current + + if (!active) { + onTextChanged("") + focusManager.clearFocus() + } + + SearchBar( + query = query, + onQueryChange = onTextChanged, + onSearch = { focusManager.clearFocus() }, + active = active, + onActiveChange = onActiveChanged, + modifier = modifier + .padding(horizontal = if (!active) 16.dp else 0.dp), + 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 = { onActiveChanged(false) }) } + } else { + null + }, + trailingIcon = when { + active && query.isNotEmpty() -> { + { + IconButton(onClick = { onTextChanged("") }) { + Icon(Icons.Default.Close, stringResource(StringR.string.a11y_clear)) + } + } + } + !active -> { + { + Icon( + imageVector = Icons.Default.Search, + contentDescription = stringResource(StringR.string.search), + modifier = Modifier.alpha(0.4f), // FIXME align on Design system theme (removing alpha should be fine) + ) + } + } + else -> null + }, + colors = if (!active) SearchBarDefaults.colors() else SearchBarDefaults.colors(containerColor = Color.Transparent), + content = { + if (isMultiSelectionEnabled && selectedUsers.isNotEmpty()) { + SelectedUsersList( + modifier = Modifier.padding(16.dp), + selectedUsers = selectedUsers, + onUserRemoved = onUserRemoved, + ) + } + + LazyColumn { + items(results) { + SearchUserResultItem( + matrixUser = it, + onClick = { onResultSelected(it) } + ) + } + } + }, + ) +} + +@Composable +fun SearchUserResultItem( + matrixUser: MatrixUser, + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, +) { + MatrixUserRow( + modifier = modifier, + matrixUser = matrixUser, + avatarSize = AvatarSize.Custom(36.dp), + onClick = onClick, + ) +} + @Composable fun SelectedUsersList( selectedUsers: List, @@ -147,99 +236,14 @@ fun SelectedUser( } } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun SearchUserBar( - query: String, - results: ImmutableList, - active: Boolean, - modifier: Modifier = Modifier, - placeHolderTitle: String = stringResource(StringR.string.search_for_someone), - onActiveChanged: (Boolean) -> Unit = {}, - onTextChanged: (String) -> Unit = {}, - onResultSelected: (MatrixUser) -> Unit = {}, -) { - val focusManager = LocalFocusManager.current - - if (!active) { - onTextChanged("") - focusManager.clearFocus() - } - - SearchBar( - query = query, - onQueryChange = onTextChanged, - onSearch = { focusManager.clearFocus() }, - active = active, - onActiveChange = onActiveChanged, - modifier = modifier - .padding(horizontal = if (!active) 16.dp else 0.dp), - 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 = { onActiveChanged(false) }) } - } else { - null - }, - trailingIcon = when { - active && query.isNotEmpty() -> { - { - IconButton(onClick = { onTextChanged("") }) { - Icon(Icons.Default.Close, stringResource(StringR.string.a11y_clear)) - } - } - } - !active -> { - { - Icon( - imageVector = Icons.Default.Search, - contentDescription = stringResource(StringR.string.search), - modifier = Modifier.alpha(0.4f), // FIXME align on Design system theme (removing alpha should be fine) - ) - } - } - else -> null - }, - colors = if (!active) SearchBarDefaults.colors() else SearchBarDefaults.colors(containerColor = Color.Transparent), - content = { - LazyColumn { - items(results) { - SearchUserResultItem( - matrixUser = it, - onClick = { onResultSelected(it) } - ) - } - } - }, - ) -} - -@Composable -fun SearchUserResultItem( - matrixUser: MatrixUser, - modifier: Modifier = Modifier, - onClick: () -> Unit = {}, -) { - MatrixUserRow( - modifier = modifier, - matrixUser = matrixUser, - avatarSize = AvatarSize.Custom(36.dp), - onClick = onClick, - ) -} - @Preview @Composable -internal fun ChangeServerViewLightPreview(@PreviewParameter(SelectUsersStateProvider::class) state: SelectUsersState) = +internal fun SelectUsersViewLightPreview(@PreviewParameter(SelectUsersStateProvider::class) state: SelectUsersState) = ElementPreviewLight { ContentToPreview(state) } @Preview @Composable -internal fun ChangeServerViewDarkPreview(@PreviewParameter(SelectUsersStateProvider::class) state: SelectUsersState) = +internal fun SelectUsersViewDarkPreview(@PreviewParameter(SelectUsersStateProvider::class) state: SelectUsersState) = ElementPreviewDark { ContentToPreview(state) } @Composable diff --git a/features/selectusers/impl/src/main/kotlin/io/element/android/features/selectusers/impl/DefaultSelectMultipleUsersPresenter.kt b/features/selectusers/impl/src/main/kotlin/io/element/android/features/selectusers/impl/DefaultSelectMultipleUsersPresenter.kt new file mode 100644 index 0000000000..7e6628e878 --- /dev/null +++ b/features/selectusers/impl/src/main/kotlin/io/element/android/features/selectusers/impl/DefaultSelectMultipleUsersPresenter.kt @@ -0,0 +1,33 @@ +/* + * 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 com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.selectusers.api.SelectMultipleUsersPresenter +import io.element.android.libraries.di.SessionScope +import javax.inject.Inject + +// TODO add unit tests +@ContributesBinding( + scope = SessionScope::class, + boundType = SelectMultipleUsersPresenter::class, +) +class DefaultSelectMultipleUsersPresenter @Inject constructor() : + DefaultSelectUsersPresenter, + SelectMultipleUsersPresenter { + override val isMultiSelectionEnabled: Boolean = true +} diff --git a/features/selectusers/impl/src/main/kotlin/io/element/android/features/selectusers/impl/DefaultSelectSingleUserPresenter.kt b/features/selectusers/impl/src/main/kotlin/io/element/android/features/selectusers/impl/DefaultSelectSingleUserPresenter.kt new file mode 100644 index 0000000000..e4fb82acb4 --- /dev/null +++ b/features/selectusers/impl/src/main/kotlin/io/element/android/features/selectusers/impl/DefaultSelectSingleUserPresenter.kt @@ -0,0 +1,32 @@ +/* + * 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 com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.selectusers.api.SelectSingleUserPresenter +import io.element.android.libraries.di.SessionScope +import javax.inject.Inject + +@ContributesBinding( + scope = SessionScope::class, + boundType = SelectSingleUserPresenter::class, +) +class DefaultSelectSingleUserPresenter @Inject constructor() : + DefaultSelectUsersPresenter, + SelectSingleUserPresenter { + override val isMultiSelectionEnabled: Boolean = false +} 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 a0837df242..69cec08433 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 @@ -24,26 +24,26 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.selectusers.api.SelectUsersEvents -import io.element.android.features.selectusers.api.SelectUsersPresenter import io.element.android.features.selectusers.api.SelectUsersState -import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.core.MatrixPatterns import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.ui.model.MatrixUser import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -import javax.inject.Inject -@ContributesBinding(SessionScope::class) -class DefaultSelectUsersPresenter @Inject constructor() : SelectUsersPresenter { +interface DefaultSelectUsersPresenter : Presenter { + + val isMultiSelectionEnabled: Boolean @Composable override fun present(): SelectUsersState { var isSearchActive by rememberSaveable { mutableStateOf(false) } - val selectedUsers: MutableState> = remember { mutableStateOf(persistentListOf()) } + val selectedUsers: MutableState> = remember { + mutableStateOf(persistentListOf()) + } var searchQuery by rememberSaveable { mutableStateOf("") } val searchResults: MutableState> = remember { mutableStateOf(persistentListOf()) @@ -76,6 +76,7 @@ class DefaultSelectUsersPresenter @Inject constructor() : SelectUsersPresenter { searchResults = searchResults.value, selectedUsers = selectedUsers.value, isSearchActive = isSearchActive, + isMultiSelectionEnabled = isMultiSelectionEnabled, eventSink = ::handleEvents, ) } 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/DefaultSelectSingleUserPresenterTests.kt similarity index 93% rename from features/selectusers/impl/src/test/kotlin/io/element/android/features/selectusers/impl/DefaultSelectUsersPresenterTests.kt rename to features/selectusers/impl/src/test/kotlin/io/element/android/features/selectusers/impl/DefaultSelectSingleUserPresenterTests.kt index 7632f1695a..61c4a386ec 100644 --- 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/DefaultSelectSingleUserPresenterTests.kt @@ -28,11 +28,11 @@ import kotlinx.coroutines.test.runTest import org.junit.Test @OptIn(ExperimentalCoroutinesApi::class) -class DefaultSelectUsersPresenterTests { +class DefaultSelectSingleUserPresenterTests { @Test fun `present - initial state`() = runTest { - val presenter = DefaultSelectUsersPresenter() + val presenter = DefaultSelectSingleUserPresenter() moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -43,7 +43,7 @@ class DefaultSelectUsersPresenterTests { @Test fun `present - update search query`() = runTest { - val presenter = DefaultSelectUsersPresenter() + val presenter = DefaultSelectSingleUserPresenter() moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test {