diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt index f03decf561..13c3ff52be 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt @@ -28,8 +28,8 @@ import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.createroom.impl.addpeople.AddPeopleNode import io.element.android.features.createroom.impl.root.CreateRoomRootNode -import io.element.android.features.createroom.impl.selectusers.SelectUsersNode import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode @@ -67,7 +67,7 @@ class CreateRoomFlowNode @AssistedInject constructor( } createNode(buildContext, plugins = listOf(callback)) } - NavTarget.NewRoom -> createNode(buildContext) + NavTarget.NewRoom -> createNode(buildContext) } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleEvents.kt new file mode 100644 index 0000000000..c7ce6308e1 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleEvents.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.createroom.impl.addpeople + +import io.element.android.libraries.matrix.ui.model.MatrixUser +import kotlinx.collections.immutable.ImmutableList + +sealed interface AddPeopleEvents { + data class UpdateSelection(val users: ImmutableList) : AddPeopleEvents +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/selectusers/SelectUsersNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt similarity index 88% rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/selectusers/SelectUsersNode.kt rename to features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt index 7ca9747c5c..751ab7c53f 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/selectusers/SelectUsersNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.createroom.impl.selectusers +package io.element.android.features.createroom.impl.addpeople import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -27,16 +27,16 @@ import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.di.AppScope @ContributesNode(AppScope::class) -class SelectUsersNode @AssistedInject constructor( +class AddPeopleNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val presenter: SelectUsersPresenter, + private val presenter: AddPeoplePresenter, ) : Node(buildContext, plugins = plugins) { @Composable override fun View(modifier: Modifier) { val state = presenter.present() - SelectUsersView( + AddPeopleView( state = state, modifier = modifier, onBackPressed = { navigateUp() }, diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/selectusers/SelectUsersPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt similarity index 66% rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/selectusers/SelectUsersPresenter.kt rename to features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt index bd040f91a0..4cca64b4dd 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/selectusers/SelectUsersPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.createroom.impl.selectusers +package io.element.android.features.createroom.impl.addpeople import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState @@ -24,24 +24,22 @@ import io.element.android.libraries.architecture.Presenter 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 // TODO add unit tests -class SelectUsersPresenter @Inject constructor() : Presenter { +class AddPeoplePresenter @Inject constructor() : Presenter { @Composable - override fun present(): SelectUsersState { - val selectedUsers: MutableState> = remember { mutableStateOf(persistentListOf()) } + override fun present(): AddPeopleState { + val selectedUsers: MutableState> = remember { mutableStateOf(persistentListOf(aMatrixUser("test"))) } - fun handleEvents(event: SelectUsersEvents) { + fun handleEvents(event: AddPeopleEvents) { when (event) { - is SelectUsersEvents.AddToSelection -> selectedUsers.value = selectedUsers.value.plus(event.matrixUser).toImmutableList() - is SelectUsersEvents.RemoveFromSelection -> selectedUsers.value = selectedUsers.value.minus(event.matrixUser).toImmutableList() + is AddPeopleEvents.UpdateSelection -> selectedUsers.value = event.users } } - return SelectUsersState( + return AddPeopleState( selectedUsers = selectedUsers.value, eventSink = ::handleEvents, ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleState.kt new file mode 100644 index 0000000000..6214e2259a --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleState.kt @@ -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.features.createroom.impl.addpeople + +import io.element.android.libraries.matrix.ui.model.MatrixUser +import kotlinx.collections.immutable.ImmutableList + +data class AddPeopleState( + val selectedUsers: ImmutableList, + val eventSink: (AddPeopleEvents) -> Unit, +) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleStateProvider.kt new file mode 100644 index 0000000000..7d7c1851ff --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleStateProvider.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.createroom.impl.addpeople + +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 + +open class AddPeopleStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aAddPeopleState(), + aAddPeopleState().copy( + selectedUsers = persistentListOf( + aMatrixUser(userName = ""), + aMatrixUser(userName = "User"), + aMatrixUser(userName = "User with long name"), + ) + ) + ) +} + +fun aAddPeopleState() = AddPeopleState( + selectedUsers = persistentListOf(), + eventSink = {} +) + +fun aMatrixUser(userName: String): MatrixUser { + return MatrixUser(id = UserId("@id"), username = userName, avatarData = AvatarData("@id", "U")) +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt new file mode 100644 index 0000000000..62e935079a --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.createroom.impl.addpeople + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +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.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +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.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.CenterAlignedTopAppBar +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.TextButton +import io.element.android.libraries.ui.strings.R as StringR + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AddPeopleView( + state: AddPeopleState, + modifier: Modifier = Modifier, + onBackPressed: () -> Unit = {}, + onNextPressed: () -> Unit = {}, +) { + var isSearchActive by rememberSaveable { mutableStateOf(false) } + val eventSink = state.eventSink + + Scaffold( + topBar = { + AddPeopleViewTopBar( + hasSelectedUsers = state.selectedUsers.isNotEmpty(), + onBackPressed = onBackPressed, + onNextPressed = onNextPressed, + ) + } + ) { padding -> + Column( + modifier = modifier + .fillMaxSize() + .padding(padding), + ) { + // TODO use reusable searchUser bar with multi selection + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AddPeopleViewTopBar( + hasSelectedUsers: Boolean, + modifier: Modifier = Modifier, + onBackPressed: () -> Unit = {}, + onNextPressed: () -> Unit = {}, +) { + CenterAlignedTopAppBar( + modifier = modifier, + title = { + Text( + text = stringResource(id = StringR.string.add_people), + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + ) + }, + navigationIcon = { BackButton(onClick = onBackPressed) }, + actions = { + TextButton( + modifier = Modifier.padding(horizontal = 8.dp), + onClick = onNextPressed, + ) { + val textActionResId = if (hasSelectedUsers) StringR.string.action_next else StringR.string.action_skip + Text( + text = stringResource(id = textActionResId), + fontSize = 16.sp, + ) + } + } + ) +} + +@Preview +@Composable +internal fun ChangeServerViewLightPreview(@PreviewParameter(AddPeopleStateProvider::class) state: AddPeopleState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +internal fun ChangeServerViewDarkPreview(@PreviewParameter(AddPeopleStateProvider::class) state: AddPeopleState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: AddPeopleState) { + AddPeopleView(state = state) +} diff --git a/features/selectusers/api/build.gradle.kts b/features/selectusers/api/build.gradle.kts new file mode 100644 index 0000000000..8454780482 --- /dev/null +++ b/features/selectusers/api/build.gradle.kts @@ -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-compose-library") +} + +android { + namespace = "io.element.android.features.selectusers.api" +} + +dependencies { + implementation(projects.libraries.architecture) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.uiStrings) + implementation(projects.libraries.matrixui) +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/selectusers/SelectUsersEvents.kt b/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersEvents.kt similarity index 87% rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/selectusers/SelectUsersEvents.kt rename to features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersEvents.kt index 97e6af4d7e..2d13a3475e 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/selectusers/SelectUsersEvents.kt +++ b/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersEvents.kt @@ -14,11 +14,12 @@ * limitations under the License. */ -package io.element.android.features.createroom.impl.selectusers +package io.element.android.features.selectusers.api import io.element.android.libraries.matrix.ui.model.MatrixUser sealed interface SelectUsersEvents { + data class UpdateSearchQuery(val query: String) : SelectUsersEvents data class AddToSelection(val matrixUser: MatrixUser) : SelectUsersEvents data class RemoveFromSelection(val matrixUser: MatrixUser) : SelectUsersEvents } 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/SelectUsersPresenter.kt new file mode 100644 index 0000000000..de889c80b3 --- /dev/null +++ b/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersPresenter.kt @@ -0,0 +1,21 @@ +/* + * 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.api + +import io.element.android.libraries.architecture.Presenter + +interface SelectUsersPresenter : Presenter diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/selectusers/SelectUsersState.kt b/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersState.kt similarity index 86% rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/selectusers/SelectUsersState.kt rename to features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersState.kt index 84c0da9466..5d89010399 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/selectusers/SelectUsersState.kt +++ b/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersState.kt @@ -14,12 +14,14 @@ * limitations under the License. */ -package io.element.android.features.createroom.impl.selectusers +package io.element.android.features.selectusers.api import io.element.android.libraries.matrix.ui.model.MatrixUser import kotlinx.collections.immutable.ImmutableList data class SelectUsersState( + val searchQuery: String, + val searchResults: ImmutableList, val selectedUsers: ImmutableList, val eventSink: (SelectUsersEvents) -> Unit, ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/selectusers/SelectUsersStateProvider.kt b/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersStateProvider.kt similarity index 93% rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/selectusers/SelectUsersStateProvider.kt rename to features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersStateProvider.kt index c7b5956c99..6f0c71a0d3 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/selectusers/SelectUsersStateProvider.kt +++ b/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersStateProvider.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.createroom.impl.selectusers +package io.element.android.features.selectusers.api import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.designsystem.components.avatar.AvatarData @@ -37,6 +37,8 @@ open class SelectUsersStateProvider : PreviewParameterProvider } fun aSelectUsersState() = SelectUsersState( + searchQuery = "", + searchResults = persistentListOf(), selectedUsers = persistentListOf(), eventSink = {} ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/selectusers/SelectUsersView.kt b/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersView.kt similarity index 53% rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/selectusers/SelectUsersView.kt rename to features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersView.kt index 020b9c8d7b..f49b7984f8 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/selectusers/SelectUsersView.kt +++ b/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersView.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * 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. @@ -14,47 +14,55 @@ * limitations under the License. */ -package io.element.android.features.createroom.impl.selectusers +package io.element.android.features.selectusers.api 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.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width 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 +import androidx.compose.material.icons.filled.Search 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 import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview 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.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.CenterAlignedTopAppBar +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.Scaffold import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.matrix.ui.components.MatrixUserRow import io.element.android.libraries.matrix.ui.model.MatrixUser 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) @@ -62,66 +70,38 @@ import io.element.android.libraries.ui.strings.R as StringR fun SelectUsersView( state: SelectUsersState, modifier: Modifier = Modifier, - onBackPressed: () -> Unit = {}, - onNextPressed: () -> Unit = {}, + onSearchActiveChanged: (Boolean) -> Unit = {}, + onSelectionChanged: (ImmutableList) -> Unit = {}, ) { + var isSearchActive by rememberSaveable { mutableStateOf(false) } val eventSink = state.eventSink - Scaffold( - topBar = { - SelectUsersViewTopBar( - hasSelectedUsers = state.selectedUsers.isNotEmpty(), - onBackPressed = onBackPressed, - onNextPressed = onNextPressed, - ) - } - ) { padding -> - Column( - modifier = modifier - .fillMaxSize() - .padding(padding) - ) { - // TODO create a SearchUserView with multi selection option + callbacks - SelectedUsersList( - modifier = Modifier.padding(horizontal = 16.dp), - selectedUsers = state.selectedUsers, - onUserRemoved = { eventSink(SelectUsersEvents.RemoveFromSelection(it)) } - ) - } - } -} + // TODO how to pass back the selection list? -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun SelectUsersViewTopBar( - hasSelectedUsers: Boolean, - modifier: Modifier = Modifier, - onBackPressed: () -> Unit = {}, - onNextPressed: () -> Unit = {}, -) { - CenterAlignedTopAppBar( - modifier = modifier, - title = { - Text( - text = stringResource(id = StringR.string.add_people), - fontSize = 16.sp, - fontWeight = FontWeight.SemiBold, - ) - }, - navigationIcon = { BackButton(onClick = onBackPressed) }, - actions = { - TextButton( - modifier = Modifier.padding(horizontal = 8.dp), - onClick = onNextPressed, - ) { - val textActionResId = if (hasSelectedUsers) StringR.string.action_next else StringR.string.action_skip - Text( - text = stringResource(id = textActionResId), - fontSize = 16.sp, - ) - } - } - ) + Column( + modifier = modifier + .fillMaxSize() + ) { + SearchUserBar( + modifier = Modifier.fillMaxWidth(), + query = state.searchQuery, + results = state.searchResults, + active = isSearchActive, + onActiveChanged = { + isSearchActive = it + 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)) } + ) + } } @Composable @@ -178,6 +158,88 @@ 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() + } + + DockedSearchBar( + 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 + }, + 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) } + ) + } + }, + ) +} + +@Composable +fun SearchUserResultItem( + matrixUser: MatrixUser, + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, +) { + MatrixUserRow( + modifier = modifier.heightIn(min = 56.dp), + matrixUser = matrixUser, + avatarSize = AvatarSize.Custom(36), + onClick = onClick, + ) +} + @Preview @Composable internal fun ChangeServerViewLightPreview(@PreviewParameter(SelectUsersStateProvider::class) state: SelectUsersState) = diff --git a/features/selectusers/impl/build.gradle.kts b/features/selectusers/impl/build.gradle.kts new file mode 100644 index 0000000000..e74996cc5c --- /dev/null +++ b/features/selectusers/impl/build.gradle.kts @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2022 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. + */ + +// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.anvil) + alias(libs.plugins.ksp) +} + +android { + namespace = "io.element.android.features.selectusers.impl" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(projects.anvilannotations) + anvil(projects.anvilcodegen) + implementation(projects.libraries.core) + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixui) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.elementresources) + implementation(projects.libraries.testtags) + implementation(projects.libraries.uiStrings) + api(projects.features.selectusers.api) + ksp(libs.showkase.processor) + + 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) + + androidTestImplementation(libs.test.junitext) +} diff --git a/features/selectusers/impl/src/main/AndroidManifest.xml b/features/selectusers/impl/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..e9c0841b6b --- /dev/null +++ b/features/selectusers/impl/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + 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 new file mode 100644 index 0000000000..12bc44c7e9 --- /dev/null +++ b/features/selectusers/impl/src/main/kotlin/io/element/android/features/selectusers/impl/DefaultSelectUsersPresenter.kt @@ -0,0 +1,89 @@ +/* + * 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 androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +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.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 + +// TODO add unit tests +@ContributesBinding(SessionScope::class) +class DefaultSelectUsersPresenter @Inject constructor() : SelectUsersPresenter { + + @Composable + override fun present(): SelectUsersState { + val selectedUsers: MutableState> = remember { mutableStateOf(persistentListOf()) } + var searchQuery by rememberSaveable { mutableStateOf("") } + val searchResults: MutableState> = remember { + mutableStateOf(persistentListOf()) + } + + fun handleEvents(event: SelectUsersEvents) { + when (event) { + is SelectUsersEvents.UpdateSearchQuery -> searchQuery = event.query + is SelectUsersEvents.AddToSelection -> selectedUsers.value = selectedUsers.value.plus(event.matrixUser).toImmutableList() + is SelectUsersEvents.RemoveFromSelection -> selectedUsers.value = selectedUsers.value.minus(event.matrixUser).toImmutableList() + } + } + + LaunchedEffect(searchQuery) { + searchResults.value = if (MatrixPatterns.isUserId(searchQuery)) { + persistentListOf(MatrixUser(UserId(searchQuery))) + } else { + persistentListOf() + } + if (searchQuery.isNotEmpty()) { + searchResults.value = performSearch(searchQuery) + } + } + + return SelectUsersState( + searchQuery = searchQuery, + searchResults = searchResults.value, + selectedUsers = selectedUsers.value, + eventSink = ::handleEvents, + ) + } + + private fun performSearch(query: String): ImmutableList { + val isMatrixId = MatrixPatterns.isUserId(query) + val results = mutableListOf()// TODO trigger /search request + if (isMatrixId && results.none { it.id.value == query }) { + val getProfileResult: MatrixUser? = null // TODO trigger /profile request + val profile = getProfileResult ?: MatrixUser(UserId(query)) + results.add(0, profile) + } + return results.toImmutableList() + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 0fd8bc2938..04636090ec 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -92,3 +92,5 @@ include(":features:createroom:api") include(":features:createroom:impl") include(":features:verifysession:api") include(":features:verifysession:impl") +include(":features:selectusers:api") +include(":features:selectusers:impl")