From 0a926bd05ae0c470e5f21a6aa924128410e223df Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 3 Apr 2023 09:28:24 +0200 Subject: [PATCH 01/23] Navigate from people view to configuration view --- .../createroom/impl/CreateRoomFlowNode.kt | 21 +++++-- .../impl/addpeople/AddPeopleNode.kt | 12 +++- .../impl/addpeople/AddPeopleView.kt | 5 +- .../impl/configureroom/ConfigureRoomEvents.kt | 22 +++++++ .../impl/configureroom/ConfigureRoomNode.kt | 56 ++++++++++++++++++ .../configureroom/ConfigureRoomPresenter.kt | 48 +++++++++++++++ .../ConfigureRoomPresenterArgs.kt | 23 ++++++++ .../impl/configureroom/ConfigureRoomState.kt | 24 ++++++++ .../ConfigureRoomStateProvider.kt | 32 ++++++++++ .../impl/configureroom/ConfigureRoomView.kt | 58 +++++++++++++++++++ libraries/designsystem/build.gradle.kts | 1 + .../components/avatar/AvatarData.kt | 7 ++- libraries/matrixui/build.gradle.kts | 1 + .../libraries/matrix/ui/model/MatrixUser.kt | 5 +- 14 files changed, 306 insertions(+), 9 deletions(-) create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterArgs.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt 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 22851904c4..137017a2ec 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 @@ -31,12 +31,14 @@ import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.features.createroom.impl.addpeople.AddPeopleNode +import io.element.android.features.createroom.impl.configureroom.ConfigureRoomNode import io.element.android.features.createroom.impl.root.CreateRoomRootNode import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.ui.model.MatrixUser import kotlinx.parcelize.Parcelize @ContributesNode(SessionScope::class) @@ -58,12 +60,15 @@ class CreateRoomFlowNode @AssistedInject constructor( @Parcelize object NewRoom : NavTarget + + @Parcelize + data class ConfigureRoom(val users: List) : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { NavTarget.Root -> { - val callback = object : CreateRoomRootNode.Callback { + createNode(context = buildContext, plugins = listOf(object : CreateRoomRootNode.Callback { override fun onCreateNewRoom() { backstack.push(NavTarget.NewRoom) } @@ -71,10 +76,18 @@ class CreateRoomFlowNode @AssistedInject constructor( override fun onOpenRoom(roomId: RoomId) { plugins().forEach { it.onOpenRoom(roomId) } } - } - createNode(buildContext, plugins = listOf(callback)) + })) + } + NavTarget.NewRoom -> { + createNode(context = buildContext, plugins = listOf(object : AddPeopleNode.Callback { + override fun onContinue(selectedUsers: List) { + backstack.push(NavTarget.ConfigureRoom(selectedUsers)) + } + })) + } + is NavTarget.ConfigureRoom -> { + createNode(context = buildContext, plugins = listOf(ConfigureRoomNode.Inputs(navTarget.users))) } - NavTarget.NewRoom -> createNode(buildContext) } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt index 5393075d18..91b1d5a721 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt @@ -21,10 +21,12 @@ import androidx.compose.ui.Modifier import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.ui.model.MatrixUser @ContributesNode(SessionScope::class) class AddPeopleNode @AssistedInject constructor( @@ -33,6 +35,14 @@ class AddPeopleNode @AssistedInject constructor( private val presenter: AddPeoplePresenter, ) : Node(buildContext, plugins = plugins) { + interface Callback : Plugin { + fun onContinue(selectedUsers: List) + } + + private fun onContinue(selectedUsers: List) { + plugins().forEach { it.onContinue(selectedUsers) } + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() @@ -40,7 +50,7 @@ class AddPeopleNode @AssistedInject constructor( state = state, modifier = modifier, onBackPressed = { navigateUp() }, - onNextPressed = { }, + onNextPressed = this::onContinue, ) } } 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 index 56a16b24f9..e74a1477d2 100644 --- 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 @@ -38,6 +38,7 @@ import io.element.android.libraries.designsystem.theme.components.CenterAlignedT 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.model.MatrixUser import io.element.android.libraries.ui.strings.R as StringR @OptIn(ExperimentalMaterial3Api::class) @@ -46,7 +47,7 @@ fun AddPeopleView( state: AddPeopleState, modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, - onNextPressed: () -> Unit = {}, + onNextPressed: (List) -> Unit = {}, ) { val eventSink = state.eventSink @@ -56,7 +57,7 @@ fun AddPeopleView( AddPeopleViewTopBar( hasSelectedUsers = state.userListState.selectedUsers.isNotEmpty(), onBackPressed = onBackPressed, - onNextPressed = onNextPressed, + onNextPressed = { onNextPressed(state.selectUsersState.selectedUsers) }, ) } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt new file mode 100644 index 0000000000..d1ba2c8ebe --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt @@ -0,0 +1,22 @@ +/* + * 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.configureroom + +// TODO Add your events or remove the file completely if no events +sealed interface ConfigureRoomEvents { + object MyEvent : ConfigureRoomEvents +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt new file mode 100644 index 0000000000..6e11e492b8 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt @@ -0,0 +1,56 @@ +/* + * 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.configureroom + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.ui.model.MatrixUser + +@ContributesNode(SessionScope::class) +class ConfigureRoomNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenterFactory: ConfigureRoomPresenter.Factory, +) : Node(buildContext, plugins = plugins) { + + data class Inputs( + val selectedUsers: List + ) : NodeInputs + + private val inputs: Inputs = inputs() + private val presenter by lazy { + presenterFactory.create(ConfigureRoomPresenterArgs(inputs.selectedUsers)) + } + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + ConfigureRoomView( + state = state, + modifier = modifier, + ) + } +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt new file mode 100644 index 0000000000..243769e90d --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -0,0 +1,48 @@ +/* + * 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.configureroom + +import androidx.compose.runtime.Composable +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.libraries.architecture.Presenter + +class ConfigureRoomPresenter @AssistedInject constructor( + @Assisted val args: ConfigureRoomPresenterArgs, +) : Presenter { + + @AssistedFactory + interface Factory { + fun create(args: ConfigureRoomPresenterArgs): ConfigureRoomPresenter + } + + @Composable + override fun present(): ConfigureRoomState { + + fun handleEvents(event: ConfigureRoomEvents) { + when (event) { + ConfigureRoomEvents.MyEvent -> Unit + } + } + + return ConfigureRoomState( + selectedUsers = args.selectedUsers, + eventSink = ::handleEvents, + ) + } +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterArgs.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterArgs.kt new file mode 100644 index 0000000000..8969a2a5fa --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterArgs.kt @@ -0,0 +1,23 @@ +/* + * 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.configureroom + +import io.element.android.libraries.matrix.ui.model.MatrixUser + +data class ConfigureRoomPresenterArgs( + val selectedUsers: List, +) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt new file mode 100644 index 0000000000..b06f4e29b9 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.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.configureroom + +import io.element.android.libraries.matrix.ui.model.MatrixUser + +data class ConfigureRoomState( + val selectedUsers: List, + val eventSink: (ConfigureRoomEvents) -> Unit +) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt new file mode 100644 index 0000000000..f9701fdaa6 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.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.createroom.impl.configureroom + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +open class ConfigureRoomStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aConfigureRoomState(), + // Add other state here + ) +} + +fun aConfigureRoomState() = ConfigureRoomState( + selectedUsers = emptyList(), + eventSink = {} +) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt new file mode 100644 index 0000000000..43c4a84f03 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -0,0 +1,58 @@ +/* + * 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.configureroom + +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Text + +@Composable +fun ConfigureRoomView( + state: ConfigureRoomState, + modifier: Modifier = Modifier, +) { + Box(modifier, contentAlignment = Alignment.Center) { + Text( + "ConfigureRoom feature view", + color = MaterialTheme.colorScheme.primary, + ) + } +} + +@Preview +@Composable +fun ConfigureRoomViewLightPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +fun ConfigureRoomViewDarkPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: ConfigureRoomState) { + ConfigureRoomView( + state = state, + ) +} diff --git a/libraries/designsystem/build.gradle.kts b/libraries/designsystem/build.gradle.kts index 45430e5d82..4533c950b1 100644 --- a/libraries/designsystem/build.gradle.kts +++ b/libraries/designsystem/build.gradle.kts @@ -19,6 +19,7 @@ plugins { id("io.element.android-compose-library") alias(libs.plugins.ksp) + id("kotlin-parcelize") } android { diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt index 7f2cddea09..3bf4f7d0b4 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt @@ -16,15 +16,20 @@ package io.element.android.libraries.designsystem.components.avatar +import android.os.Parcelable import androidx.compose.runtime.Immutable +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize @Immutable +@Parcelize data class AvatarData( val id: String, val name: String?, val url: String? = null, + @IgnoredOnParcel val size: AvatarSize = AvatarSize.MEDIUM -) { +) : Parcelable { fun getInitial(): String { val firstChar = name?.firstOrNull() ?: id.getOrNull(1) ?: '?' return firstChar.uppercase() diff --git a/libraries/matrixui/build.gradle.kts b/libraries/matrixui/build.gradle.kts index c9dead5eb4..6d2109d3f8 100644 --- a/libraries/matrixui/build.gradle.kts +++ b/libraries/matrixui/build.gradle.kts @@ -20,6 +20,7 @@ plugins { id("io.element.android-compose-library") alias(libs.plugins.anvil) alias(libs.plugins.ksp) + id("kotlin-parcelize") } android { diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUser.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUser.kt index c524cbb733..f0ddb30a93 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUser.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUser.kt @@ -16,16 +16,19 @@ package io.element.android.libraries.matrix.ui.model +import android.os.Parcelable import androidx.compose.runtime.Immutable import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.api.core.UserId +import kotlinx.parcelize.Parcelize +@Parcelize @Immutable data class MatrixUser( val id: UserId, val username: String? = null, val avatarData: AvatarData = AvatarData(id.value, username), -) +) : Parcelable fun MatrixUser.getBestName(): String { return username?.takeIf { it.isNotEmpty() } ?: id.value From ac5f50d264a8899c52043cb7b45dd7075103ea38 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 3 Apr 2023 15:02:33 +0200 Subject: [PATCH 02/23] WIP create room screen --- features/createroom/impl/build.gradle.kts | 1 + .../configureroom/ConfigureRoomPresenter.kt | 4 +- .../impl/configureroom/ConfigureRoomState.kt | 5 +- .../ConfigureRoomStateProvider.kt | 5 +- .../impl/configureroom/ConfigureRoomView.kt | 195 +++++++++++++++++- .../features/userlist/api/UserListView.kt | 3 + 6 files changed, 207 insertions(+), 6 deletions(-) diff --git a/features/createroom/impl/build.gradle.kts b/features/createroom/impl/build.gradle.kts index 6f2544822c..77a5ed26be 100644 --- a/features/createroom/impl/build.gradle.kts +++ b/features/createroom/impl/build.gradle.kts @@ -49,6 +49,7 @@ dependencies { implementation(projects.libraries.uiStrings) implementation(projects.features.userlist.api) api(projects.features.createroom.api) + implementation(libs.coil.compose) // FIXME temp testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index 243769e90d..25851e41af 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -21,6 +21,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.element.android.libraries.architecture.Presenter +import kotlinx.collections.immutable.toImmutableList class ConfigureRoomPresenter @AssistedInject constructor( @Assisted val args: ConfigureRoomPresenterArgs, @@ -41,7 +42,8 @@ class ConfigureRoomPresenter @AssistedInject constructor( } return ConfigureRoomState( - selectedUsers = args.selectedUsers, + selectedUsers = args.selectedUsers.toImmutableList(), + avatarUri = null, eventSink = ::handleEvents, ) } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt index b06f4e29b9..b8eee9e7bf 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt @@ -16,9 +16,12 @@ package io.element.android.features.createroom.impl.configureroom +import android.net.Uri import io.element.android.libraries.matrix.ui.model.MatrixUser +import kotlinx.collections.immutable.ImmutableList data class ConfigureRoomState( - val selectedUsers: List, + val selectedUsers: ImmutableList, + val avatarUri: Uri?, val eventSink: (ConfigureRoomEvents) -> Unit ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt index f9701fdaa6..30100c720b 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt @@ -17,6 +17,8 @@ package io.element.android.features.createroom.impl.configureroom import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.matrix.ui.components.aMatrixUser +import kotlinx.collections.immutable.persistentListOf open class ConfigureRoomStateProvider : PreviewParameterProvider { override val values: Sequence @@ -27,6 +29,7 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider Unit = {}, + onCreatePressed: () -> Unit = {}, ) { - Box(modifier, contentAlignment = Alignment.Center) { + Scaffold( + modifier = modifier, + topBar = { + ConfigureRoomToolbar( + isNextActionEnabled = false, + onBackPressed = onBackPressed, + onNextPressed = onCreatePressed, + ) + } + ) { padding -> + Column( + modifier = Modifier.padding(padding), + verticalArrangement = Arrangement.spacedBy(24.dp), + ) { + RoomNameWithAvatar( + modifier = Modifier.padding(horizontal = 16.dp), + ) + RoomTopic( + modifier = Modifier.padding(horizontal = 16.dp), + ) + SelectedUsersList( + listState = LazyListState(), // FIXME + contentPadding = PaddingValues(horizontal = 24.dp), + selectedUsers = state.selectedUsers, + onUserRemoved = { + // TODO + }, + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ConfigureRoomToolbar( + isNextActionEnabled: Boolean, + modifier: Modifier = Modifier, + onBackPressed: () -> Unit = {}, + onNextPressed: () -> Unit = {}, +) { + CenterAlignedTopAppBar( + modifier = modifier, + title = { + Text( + text = "Create a room", + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + ) + }, + navigationIcon = { BackButton(onClick = onBackPressed) }, + actions = { + TextButton( + modifier = Modifier.padding(horizontal = 8.dp), + enabled = isNextActionEnabled, + onClick = onNextPressed, + ) { + Text( + text = "Create", + fontSize = 16.sp, + ) + } + } + ) +} + +@Composable +fun RoomNameWithAvatar( + modifier: Modifier = Modifier, + avatarUri: Uri? = null, + roomName: String = "", + onAvatarClick: () -> Unit = {}, + onRoomNameChanged: (String) -> Unit = {}, +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Avatar( + avatarUri = avatarUri, + onClick = onAvatarClick, + ) + + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = "Room name" + ) + + TextField( + modifier = Modifier.fillMaxWidth(), + value = roomName, + placeholder = { Text("e.g. Product Sprint") }, + onValueChange = onRoomNameChanged, + maxLines = 1, + ) + } + } +} + +@Composable +fun Avatar( + modifier: Modifier = Modifier, + avatarUri: Uri? = null, + onClick: () -> Unit = {}, +) { + val commonModifier = modifier + .size(70.dp) + .clip(CircleShape) + .clickable(onClick = onClick) + + if (avatarUri != null) { + val context = LocalContext.current + val model = ImageRequest.Builder(context) + .data(avatarUri) + .build() + AsyncImage( + model = model, + contentDescription = null, + modifier = commonModifier, + ) + } else { + Box( + modifier = commonModifier + .background(LocalColors.current.quinary) + ) { + Icon( + imageVector = Icons.Outlined.AddAPhoto, + contentDescription = "", + modifier = modifier + .align(Alignment.Center) + .size(40.dp), + tint = MaterialTheme.colorScheme.secondary, + ) + } + } +} + +@Composable +fun RoomTopic( + modifier: Modifier = Modifier, + topic: String = "", + onTopicChanged: (String) -> Unit = {}, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { Text( - "ConfigureRoom feature view", - color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(horizontal = 16.dp), + text = "Topic (optional)", + ) + TextField( + modifier = Modifier.fillMaxWidth(), + value = topic, + placeholder = { Text("What is this room about?") }, + onValueChange = onTopicChanged, + maxLines = 3, ) } } diff --git a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt index bc355a0a26..5605ed6028 100644 --- a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -244,11 +245,13 @@ fun SelectedUsersList( listState: LazyListState, selectedUsers: ImmutableList, modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(0.dp), onUserRemoved: (MatrixUser) -> Unit = {}, ) { LazyRow( state = listState, modifier = modifier, + contentPadding = contentPadding, horizontalArrangement = Arrangement.spacedBy(24.dp), ) { items(selectedUsers.toList()) { matrixUser -> From 6c4cc71d3f6164ff70695fb9288c314942cc8e90 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 3 Apr 2023 15:11:22 +0200 Subject: [PATCH 03/23] Use content padding --- .../io/element/android/features/userlist/api/UserListView.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt index 5605ed6028..5f587b471e 100644 --- a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt @@ -98,7 +98,7 @@ fun UserListView( if (state.isMultiSelectionEnabled && !state.isSearchActive && state.selectedUsers.isNotEmpty()) { SelectedUsersList( listState = state.selectedUsersListState, - modifier = Modifier.padding(16.dp), + contentPadding = PaddingValues(16.dp), selectedUsers = state.selectedUsers, onUserRemoved = { state.eventSink(UserListEvents.RemoveFromSelection(it)) @@ -175,7 +175,7 @@ fun SearchUserBar( if (isMultiSelectionEnabled && active && selectedUsers.isNotEmpty()) { SelectedUsersList( listState = selectedUsersListState, - modifier = Modifier.padding(16.dp), + contentPadding = PaddingValues(16.dp), selectedUsers = selectedUsers, onUserRemoved = onUserDeselected, ) From 66b672e6554a339412fb57a94c1ea64f6010a109 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 4 Apr 2023 16:42:06 +0200 Subject: [PATCH 04/23] Change wording of create a room button --- .../features/createroom/impl/root/CreateRoomRootView.kt | 2 +- ...CreateRoomRootViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...reateRoomRootViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt index e488645b78..4e12dcaab0 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt @@ -156,7 +156,7 @@ fun CreateRoomActionButtonsList( Column(modifier = modifier) { CreateRoomActionButton( iconRes = DrawableR.drawable.ic_groups, - text = stringResource(id = R.string.screen_create_room_action_create_room), + text = stringResource(id = StringR.string.action_create_a_room), onClick = onNewRoomClicked, ) CreateRoomActionButton( diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index 1c22798a9a..de4377ecc9 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:365161fefa9ea3c82bf3ed2527b4847df27860266e1d5f0e770962e95154b4a6 -size 19178 +oid sha256:6f36c2b4f4266048d5295df08f380e4128630d11cce7ac11cf3a0eaaa5594d61 +size 19924 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index d404485dee..0bdbed7968 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f4413c59f47c45c06433f53c3f1fb7e9095bb47e3d8c3e7fabefcb19cdd5146 -size 18346 +oid sha256:23b9e996b8f0cc2efcb3adb6294bfa7b4a53fefbb7b6ee07add4105da9b9d40e +size 18984 From 97ade693f54eae2881941ac933a2bbcbd126e50a Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 4 Apr 2023 17:26:00 +0200 Subject: [PATCH 05/23] Add fake list of matrix users --- .../configureroom/ConfigureRoomStateProvider.kt | 6 +++--- .../matrix/ui/components/MatrixUserProvider.kt | 13 +++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt index 30100c720b..2bc7f62ee8 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt @@ -17,8 +17,8 @@ package io.element.android.features.createroom.impl.configureroom import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.libraries.matrix.ui.components.aMatrixUser -import kotlinx.collections.immutable.persistentListOf +import io.element.android.libraries.matrix.ui.components.aMatrixUserList +import kotlinx.collections.immutable.toImmutableList open class ConfigureRoomStateProvider : PreviewParameterProvider { override val values: Sequence @@ -29,7 +29,7 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( From 470afea802c9d1a4583213aff5a92053fe516fcb Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 4 Apr 2023 17:41:31 +0200 Subject: [PATCH 06/23] Add topic and room name to the state --- .../impl/configureroom/ConfigureRoomEvents.kt | 7 +++++-- .../impl/configureroom/ConfigureRoomPresenter.kt | 16 ++++++++++++++-- .../impl/configureroom/ConfigureRoomState.kt | 2 ++ .../configureroom/ConfigureRoomStateProvider.kt | 2 ++ .../impl/configureroom/ConfigureRoomView.kt | 13 +++++++++---- 5 files changed, 32 insertions(+), 8 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt index d1ba2c8ebe..db90c58834 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt @@ -16,7 +16,10 @@ package io.element.android.features.createroom.impl.configureroom -// TODO Add your events or remove the file completely if no events +import android.net.Uri + sealed interface ConfigureRoomEvents { - object MyEvent : ConfigureRoomEvents + data class RoomNameChanged(val name: String) : ConfigureRoomEvents + data class TopicChanged(val topic: String) : ConfigureRoomEvents + data class AvatarUriChanged(val uri: Uri?) : ConfigureRoomEvents } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index 25851e41af..56962647ad 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -16,7 +16,12 @@ package io.element.android.features.createroom.impl.configureroom +import android.net.Uri 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 dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -34,16 +39,23 @@ class ConfigureRoomPresenter @AssistedInject constructor( @Composable override fun present(): ConfigureRoomState { + var roomName by rememberSaveable { mutableStateOf("") } + var topic by rememberSaveable { mutableStateOf("") } + var avatarUri by rememberSaveable { mutableStateOf(null) } fun handleEvents(event: ConfigureRoomEvents) { when (event) { - ConfigureRoomEvents.MyEvent -> Unit + is ConfigureRoomEvents.AvatarUriChanged -> avatarUri = event.uri + is ConfigureRoomEvents.RoomNameChanged -> roomName = event.name + is ConfigureRoomEvents.TopicChanged -> topic = event.topic } } return ConfigureRoomState( selectedUsers = args.selectedUsers.toImmutableList(), - avatarUri = null, + roomName = roomName, + topic = topic, + avatarUri = avatarUri, eventSink = ::handleEvents, ) } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt index b8eee9e7bf..d8a3635d35 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt @@ -22,6 +22,8 @@ import kotlinx.collections.immutable.ImmutableList data class ConfigureRoomState( val selectedUsers: ImmutableList, + val roomName: String, + val topic: String, val avatarUri: Uri?, val eventSink: (ConfigureRoomEvents) -> Unit ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt index 2bc7f62ee8..2838d77f23 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt @@ -30,6 +30,8 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider Unit = {}, onRoomNameChanged: (String) -> Unit = {}, ) { @@ -170,7 +175,7 @@ fun RoomNameWithAvatar( @Composable fun Avatar( modifier: Modifier = Modifier, - avatarUri: Uri? = null, + avatarUri: Uri?, onClick: () -> Unit = {}, ) { val commonModifier = modifier @@ -208,7 +213,7 @@ fun Avatar( @Composable fun RoomTopic( modifier: Modifier = Modifier, - topic: String = "", + topic: String, onTopicChanged: (String) -> Unit = {}, ) { Column( From 5e88b2337280a3d55e6575644737742dcf67d5f0 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 5 Apr 2023 16:28:55 +0200 Subject: [PATCH 07/23] Fix build --- .../features/createroom/impl/addpeople/AddPeopleView.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index e74a1477d2..eed59923a8 100644 --- 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 @@ -29,8 +29,8 @@ 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.features.userlist.api.UserListView import io.element.android.features.createroom.impl.R +import io.element.android.features.userlist.api.UserListView 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 @@ -57,7 +57,7 @@ fun AddPeopleView( AddPeopleViewTopBar( hasSelectedUsers = state.userListState.selectedUsers.isNotEmpty(), onBackPressed = onBackPressed, - onNextPressed = { onNextPressed(state.selectUsersState.selectedUsers) }, + onNextPressed = { onNextPressed(state.userListState.selectedUsers) }, ) } } From bb48f5f378c1d24d172366e2b21332711dbbffec Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 5 Apr 2023 16:28:09 +0200 Subject: [PATCH 08/23] Room visibility --- .../impl/configureroom/ConfigureRoomEvents.kt | 1 + .../configureroom/ConfigureRoomPresenter.kt | 3 + .../impl/configureroom/ConfigureRoomState.kt | 1 + .../ConfigureRoomStateProvider.kt | 1 + .../impl/configureroom/ConfigureRoomView.kt | 125 +++++++++++++++++- .../impl/configureroom/RoomPrivacy.kt | 22 +++ .../theme/components/RadioButton.kt | 63 +++++++++ 7 files changed, 209 insertions(+), 7 deletions(-) create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacy.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/RadioButton.kt diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt index db90c58834..7322dbd961 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt @@ -22,4 +22,5 @@ sealed interface ConfigureRoomEvents { data class RoomNameChanged(val name: String) : ConfigureRoomEvents data class TopicChanged(val topic: String) : ConfigureRoomEvents data class AvatarUriChanged(val uri: Uri?) : ConfigureRoomEvents + data class RoomPrivacyChanged(val privacy: RoomPrivacy?) : ConfigureRoomEvents } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index 56962647ad..97871fb99a 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -42,12 +42,14 @@ class ConfigureRoomPresenter @AssistedInject constructor( var roomName by rememberSaveable { mutableStateOf("") } var topic by rememberSaveable { mutableStateOf("") } var avatarUri by rememberSaveable { mutableStateOf(null) } + var privacy by rememberSaveable { mutableStateOf(null) } fun handleEvents(event: ConfigureRoomEvents) { when (event) { is ConfigureRoomEvents.AvatarUriChanged -> avatarUri = event.uri is ConfigureRoomEvents.RoomNameChanged -> roomName = event.name is ConfigureRoomEvents.TopicChanged -> topic = event.topic + is ConfigureRoomEvents.RoomPrivacyChanged -> privacy = event.privacy } } @@ -56,6 +58,7 @@ class ConfigureRoomPresenter @AssistedInject constructor( roomName = roomName, topic = topic, avatarUri = avatarUri, + privacy = privacy, eventSink = ::handleEvents, ) } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt index d8a3635d35..643f01ef3e 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt @@ -25,5 +25,6 @@ data class ConfigureRoomState( val roomName: String, val topic: String, val avatarUri: Uri?, + val privacy: RoomPrivacy?, val eventSink: (ConfigureRoomEvents) -> Unit ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt index 2838d77f23..d097b0e599 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt @@ -33,5 +33,6 @@ fun aConfigureRoomState() = ConfigureRoomState( roomName = "", topic = "", avatarUri = null, + privacy = null, eventSink = {} ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index 360ad2e0e0..d492d5d81c 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -24,20 +24,27 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.AddAPhoto +import androidx.compose.material.icons.outlined.Lock +import androidx.compose.material.icons.outlined.Public import androidx.compose.material3.ExperimentalMaterial3Api 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.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -45,13 +52,14 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.AsyncImage import coil.request.ImageRequest -import io.element.android.features.selectusers.api.SelectedUsersList +import io.element.android.features.userlist.api.SelectedUsersList 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.LocalColors import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.RadioButton 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 @@ -98,6 +106,12 @@ fun ConfigureRoomView( // TODO }, ) + Spacer(Modifier.weight(1f)) + RoomPrivacyOptions( + modifier = Modifier.padding(bottom = 40.dp), + selected = state.privacy, + onOptionSelected = { state.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(it)) }, + ) } } } @@ -189,15 +203,12 @@ fun Avatar( .data(avatarUri) .build() AsyncImage( + modifier = commonModifier, model = model, contentDescription = null, - modifier = commonModifier, ) } else { - Box( - modifier = commonModifier - .background(LocalColors.current.quinary) - ) { + Box(modifier = commonModifier.background(LocalColors.current.quinary)) { Icon( imageVector = Icons.Outlined.AddAPhoto, contentDescription = "", @@ -212,8 +223,8 @@ fun Avatar( @Composable fun RoomTopic( - modifier: Modifier = Modifier, topic: String, + modifier: Modifier = Modifier, onTopicChanged: (String) -> Unit = {}, ) { Column( @@ -234,6 +245,106 @@ fun RoomTopic( } } +@Composable +fun RoomPrivacyOptions( + selected: RoomPrivacy?, + modifier: Modifier = Modifier, + onOptionSelected: (RoomPrivacy) -> Unit = {}, +) { + + data class RoomPrivacyItem( + val privacy: RoomPrivacy, + val icon: ImageVector, + val title: String, + val description: String, + ) + + val items = RoomPrivacy.values().map { + when (it) { + RoomPrivacy.Public -> RoomPrivacyItem( + privacy = it, + icon = Icons.Outlined.Lock, + title = "Private room (invite only)", + description = "Messages in this room are encrypted. Encryption can’t be disabled afterwards.", + ) + RoomPrivacy.Private -> RoomPrivacyItem( + privacy = it, + icon = Icons.Outlined.Public, + title = "Public room (anyone)", + description = "Messages are not encrypted and anyone can read them. You can enable encryption at a later date.", + ) + } + } + Column(modifier = modifier.selectableGroup()) { + items.forEach { item -> + RoomPrivacyOption( + privacy = RoomPrivacy.Private, + icon = item.icon, + title = item.title, + description = item.description, + isSelected = selected == item.privacy, + onOptionSelected = { onOptionSelected(item.privacy) } + ) + } + } +} + +@Composable +fun RoomPrivacyOption( + privacy: RoomPrivacy, + icon: ImageVector, + title: String, + description: String, + modifier: Modifier = Modifier, + isSelected: Boolean = false, + onOptionSelected: (RoomPrivacy) -> Unit = {}, +) { + Row( + modifier + .fillMaxWidth() + .selectable( + selected = isSelected, + onClick = { onOptionSelected(privacy) }, + role = Role.RadioButton, + ) + .padding(8.dp), + ) { + Icon( + modifier = Modifier.padding(horizontal = 8.dp), + imageVector = icon, + contentDescription = "", + tint = MaterialTheme.colorScheme.secondary, + ) + + Column( + Modifier + .weight(1f) + .padding(horizontal = 8.dp) + ) { + Text( + text = title, + fontSize = 16.sp, + color = MaterialTheme.colorScheme.primary, + ) + Spacer(Modifier.size(3.dp)) + Text( + text = description, + fontSize = 12.sp, + lineHeight = 17.sp, + color = MaterialTheme.colorScheme.tertiary, + ) + } + + RadioButton( + modifier = Modifier + .align(Alignment.CenterVertically) + .size(48.dp), + selected = isSelected, + onClick = null // null recommended for accessibility with screenreaders + ) + } +} + @Preview @Composable fun ConfigureRoomViewLightPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacy.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacy.kt new file mode 100644 index 0000000000..e0b7411680 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacy.kt @@ -0,0 +1,22 @@ +/* + * 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.configureroom + +enum class RoomPrivacy { + Public, + Private, +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/RadioButton.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/RadioButton.kt new file mode 100644 index 0000000000..3e703962a0 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/RadioButton.kt @@ -0,0 +1,63 @@ +/* + * 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.designsystem.theme.components + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.RadioButtonColors +import androidx.compose.material3.RadioButtonDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight + +@Composable +fun RadioButton( + selected: Boolean, + onClick: (() -> Unit)?, + modifier: Modifier = Modifier, + enabled: Boolean = true, + colors: RadioButtonColors = RadioButtonDefaults.colors(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } +) { + androidx.compose.material3.RadioButton( + selected = selected, + onClick = onClick, + modifier = modifier, + enabled = enabled, + colors = colors, + interactionSource = interactionSource, + ) +} + +@Preview +@Composable +internal fun RadioButtonLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +internal fun RadioButtonDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + Column { + RadioButton(selected = false, onClick = {}) + RadioButton(selected = true, onClick = {}) + } +} From 11994ec629386a288093beb130076db19d8b4255 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 7 Apr 2023 14:29:36 +0200 Subject: [PATCH 09/23] Extract room name and topic to dedicated composable --- .../impl/configureroom/ConfigureRoomView.kt | 72 +++++++++++-------- 1 file changed, 41 insertions(+), 31 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index d492d5d81c..50514b663a 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -167,22 +167,12 @@ fun RoomNameWithAvatar( onClick = onAvatarClick, ) - Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - modifier = Modifier.padding(horizontal = 16.dp), - text = "Room name" - ) - - TextField( - modifier = Modifier.fillMaxWidth(), - value = roomName, - placeholder = { Text("e.g. Product Sprint") }, - onValueChange = onRoomNameChanged, - maxLines = 1, - ) - } + LabelledTextField( + label = "Room name", + value = roomName, + placeholder = "e.g. Product Sprint", + onValueChange = onRoomNameChanged + ) } } @@ -227,22 +217,14 @@ fun RoomTopic( modifier: Modifier = Modifier, onTopicChanged: (String) -> Unit = {}, ) { - Column( + LabelledTextField( modifier = modifier, - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - modifier = Modifier.padding(horizontal = 16.dp), - text = "Topic (optional)", - ) - TextField( - modifier = Modifier.fillMaxWidth(), - value = topic, - placeholder = { Text("What is this room about?") }, - onValueChange = onTopicChanged, - maxLines = 3, - ) - } + label = "Topic (optional)", + value = topic, + placeholder = "What is this room about?", + onValueChange = onTopicChanged, + maxLines = 3, + ) } @Composable @@ -345,6 +327,34 @@ fun RoomPrivacyOption( } } +@Composable +fun LabelledTextField( + label: String, + value: String, + modifier: Modifier = Modifier, + placeholder: String = "", + maxLines: Int = 1, + onValueChange: (String) -> Unit, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = label + ) + + TextField( + modifier = Modifier.fillMaxWidth(), + value = value, + placeholder = { Text(placeholder) }, + onValueChange = onValueChange, + maxLines = maxLines, + ) + } +} + @Preview @Composable fun ConfigureRoomViewLightPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = From f1b350c8d4e2e746f4cc9d9a88f2f3421e01aa8d Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 7 Apr 2023 14:34:24 +0200 Subject: [PATCH 10/23] Use rememberLazyListState --- .../createroom/impl/configureroom/ConfigureRoomView.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index 50514b663a..5b9b548ea3 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -28,7 +28,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.shape.CircleShape @@ -73,6 +73,7 @@ fun ConfigureRoomView( onBackPressed: () -> Unit = {}, onCreatePressed: () -> Unit = {}, ) { + val selectedUsersListState = rememberLazyListState() Scaffold( modifier = modifier, topBar = { @@ -99,12 +100,10 @@ fun ConfigureRoomView( onTopicChanged = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) }, ) SelectedUsersList( - listState = LazyListState(), // FIXME + listState = selectedUsersListState, contentPadding = PaddingValues(horizontal = 24.dp), selectedUsers = state.selectedUsers, - onUserRemoved = { - // TODO - }, + onUserRemoved = { }, // TODO ) Spacer(Modifier.weight(1f)) RoomPrivacyOptions( From 5741be3689832b5911c04c6275536e37774b8e66 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 7 Apr 2023 14:51:47 +0200 Subject: [PATCH 11/23] Update enable state of create room button --- .../createroom/impl/configureroom/ConfigureRoomEvents.kt | 1 + .../createroom/impl/configureroom/ConfigureRoomPresenter.kt | 6 ++++++ .../createroom/impl/configureroom/ConfigureRoomState.kt | 1 + .../impl/configureroom/ConfigureRoomStateProvider.kt | 2 +- .../createroom/impl/configureroom/ConfigureRoomView.kt | 6 +++--- 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt index 7322dbd961..644c3d1e03 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt @@ -23,4 +23,5 @@ sealed interface ConfigureRoomEvents { data class TopicChanged(val topic: String) : ConfigureRoomEvents data class AvatarUriChanged(val uri: Uri?) : ConfigureRoomEvents data class RoomPrivacyChanged(val privacy: RoomPrivacy?) : ConfigureRoomEvents + object CreateRoom : ConfigureRoomEvents } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index 97871fb99a..c8bad29f93 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -43,6 +43,10 @@ class ConfigureRoomPresenter @AssistedInject constructor( var topic by rememberSaveable { mutableStateOf("") } var avatarUri by rememberSaveable { mutableStateOf(null) } var privacy by rememberSaveable { mutableStateOf(null) } + val isCreateButtonEnabled by rememberSaveable(roomName, privacy) { + val enabled = roomName.isNotEmpty() && privacy != null + mutableStateOf(enabled) + } fun handleEvents(event: ConfigureRoomEvents) { when (event) { @@ -50,6 +54,7 @@ class ConfigureRoomPresenter @AssistedInject constructor( is ConfigureRoomEvents.RoomNameChanged -> roomName = event.name is ConfigureRoomEvents.TopicChanged -> topic = event.topic is ConfigureRoomEvents.RoomPrivacyChanged -> privacy = event.privacy + ConfigureRoomEvents.CreateRoom -> Unit // TODO } } @@ -59,6 +64,7 @@ class ConfigureRoomPresenter @AssistedInject constructor( topic = topic, avatarUri = avatarUri, privacy = privacy, + isCreateButtonEnabled = isCreateButtonEnabled, eventSink = ::handleEvents, ) } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt index 643f01ef3e..23f5691267 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt @@ -26,5 +26,6 @@ data class ConfigureRoomState( val topic: String, val avatarUri: Uri?, val privacy: RoomPrivacy?, + val isCreateButtonEnabled: Boolean, val eventSink: (ConfigureRoomEvents) -> Unit ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt index d097b0e599..3e4961f56a 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt @@ -24,7 +24,6 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider get() = sequenceOf( aConfigureRoomState(), - // Add other state here ) } @@ -34,5 +33,6 @@ fun aConfigureRoomState() = ConfigureRoomState( topic = "", avatarUri = null, privacy = null, + isCreateButtonEnabled = false, eventSink = {} ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index 5b9b548ea3..6f56ae06ef 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -71,16 +71,15 @@ fun ConfigureRoomView( state: ConfigureRoomState, modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, - onCreatePressed: () -> Unit = {}, ) { val selectedUsersListState = rememberLazyListState() Scaffold( modifier = modifier, topBar = { ConfigureRoomToolbar( - isNextActionEnabled = false, + isNextActionEnabled = state.isCreateButtonEnabled, onBackPressed = onBackPressed, - onNextPressed = onCreatePressed, + onNextPressed = { state.eventSink(ConfigureRoomEvents.CreateRoom) }, ) } ) { padding -> @@ -326,6 +325,7 @@ fun RoomPrivacyOption( } } +// Move this composable to design module if we want to reuse it in other screens @Composable fun LabelledTextField( label: String, From 74be5b121eec1b7de8e3616edf3e027614f2494d Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 7 Apr 2023 14:53:54 +0200 Subject: [PATCH 12/23] plug back button --- .../features/createroom/impl/configureroom/ConfigureRoomNode.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt index 6e11e492b8..ec5bf67787 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt @@ -51,6 +51,7 @@ class ConfigureRoomNode @AssistedInject constructor( ConfigureRoomView( state = state, modifier = modifier, + onBackPressed = { navigateUp() } // TODO we should keep in memory the current view state ) } } From 023c5f4a7edd8d7747436ca16a1af8b9d94af042 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 7 Apr 2023 15:29:55 +0200 Subject: [PATCH 13/23] Use string resources --- .../impl/configureroom/ConfigureRoomView.kt | 23 +++++++++++-------- .../impl/root/CreateRoomRootView.kt | 2 +- .../src/main/res/values-es/translations.xml | 2 ++ .../src/main/res/values-it/translations.xml | 2 ++ .../src/main/res/values-ro/translations.xml | 2 ++ .../impl/src/main/res/values/localazy.xml | 2 ++ .../src/main/res/values-es/translations.xml | 4 ++++ .../src/main/res/values-it/translations.xml | 4 ++++ .../src/main/res/values-ro/translations.xml | 4 ++++ .../src/main/res/values-es/translations.xml | 4 +--- .../src/main/res/values-it/translations.xml | 4 +--- .../src/main/res/values-ro/translations.xml | 4 +--- .../src/main/res/values/localazy.xml | 3 +-- tools/localazy/config.json | 3 ++- 14 files changed, 40 insertions(+), 23 deletions(-) create mode 100644 libraries/androidutils/src/main/res/values-es/translations.xml create mode 100644 libraries/androidutils/src/main/res/values-it/translations.xml create mode 100644 libraries/androidutils/src/main/res/values-ro/translations.xml diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index 6f56ae06ef..255e208532 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -44,6 +44,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview @@ -52,6 +53,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.AsyncImage import coil.request.ImageRequest +import io.element.android.features.createroom.impl.R import io.element.android.features.userlist.api.SelectedUsersList import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreviewDark @@ -64,6 +66,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.TextButton import io.element.android.libraries.designsystem.theme.components.TextField +import io.element.android.libraries.ui.strings.R as StringR @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -126,7 +129,7 @@ fun ConfigureRoomToolbar( modifier = modifier, title = { Text( - text = "Create a room", + text = stringResource(R.string.screen_create_room_title), fontSize = 16.sp, fontWeight = FontWeight.SemiBold, ) @@ -139,7 +142,7 @@ fun ConfigureRoomToolbar( onClick = onNextPressed, ) { Text( - text = "Create", + text = stringResource(StringR.string.action_create), fontSize = 16.sp, ) } @@ -166,9 +169,9 @@ fun RoomNameWithAvatar( ) LabelledTextField( - label = "Room name", + label = stringResource(R.string.screen_create_room_room_name_label), value = roomName, - placeholder = "e.g. Product Sprint", + placeholder = stringResource(R.string.screen_create_room_room_name_placeholder), onValueChange = onRoomNameChanged ) } @@ -217,9 +220,9 @@ fun RoomTopic( ) { LabelledTextField( modifier = modifier, - label = "Topic (optional)", + label = stringResource(R.string.screen_create_room_topic_label), value = topic, - placeholder = "What is this room about?", + placeholder = stringResource(R.string.screen_create_room_topic_placeholder), onValueChange = onTopicChanged, maxLines = 3, ) @@ -244,14 +247,14 @@ fun RoomPrivacyOptions( RoomPrivacy.Public -> RoomPrivacyItem( privacy = it, icon = Icons.Outlined.Lock, - title = "Private room (invite only)", - description = "Messages in this room are encrypted. Encryption can’t be disabled afterwards.", + title = stringResource(R.string.screen_create_room_private_option_title), + description = stringResource(R.string.screen_create_room_private_option_description), ) RoomPrivacy.Private -> RoomPrivacyItem( privacy = it, icon = Icons.Outlined.Public, - title = "Public room (anyone)", - description = "Messages are not encrypted and anyone can read them. You can enable encryption at a later date.", + title = stringResource(R.string.screen_create_room_public_option_title), + description = stringResource(R.string.screen_create_room_public_option_description), ) } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt index 4e12dcaab0..bcf58b7d2d 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt @@ -110,7 +110,7 @@ fun CreateRoomRootView( } is Async.Failure -> { RetryDialog( - content = stringResource(id = StringR.string.screen_start_chat_error_starting_chat), + content = stringResource(id = R.string.screen_start_chat_error_starting_chat), onDismiss = { state.eventSink(CreateRoomRootEvents.CancelStartDM) }, onRetry = { state.userListState.selectedUsers.firstOrNull() diff --git a/features/createroom/impl/src/main/res/values-es/translations.xml b/features/createroom/impl/src/main/res/values-es/translations.xml index f6248df74e..bb3d6fa0b8 100644 --- a/features/createroom/impl/src/main/res/values-es/translations.xml +++ b/features/createroom/impl/src/main/res/values-es/translations.xml @@ -3,4 +3,6 @@ "Nueva sala" "Invitar gente" "Añadir personas" + "Se ha producido un error al intentar iniciar un chat" + "No podemos validar el ID de Matrix de este usuario. Es posible que no reciba la invitación." \ No newline at end of file diff --git a/features/createroom/impl/src/main/res/values-it/translations.xml b/features/createroom/impl/src/main/res/values-it/translations.xml index ea0c0b10e1..1d6ce99b5f 100644 --- a/features/createroom/impl/src/main/res/values-it/translations.xml +++ b/features/createroom/impl/src/main/res/values-it/translations.xml @@ -3,4 +3,6 @@ "Nuova stanza" "Invita persone" "Aggiungi persone" + "Si è verificato un errore durante il tentativo di avviare una chat" + "Non possiamo convalidare l\'ID Matrix di questo utente. L\'invito potrebbe non essere ricevuto." \ No newline at end of file diff --git a/features/createroom/impl/src/main/res/values-ro/translations.xml b/features/createroom/impl/src/main/res/values-ro/translations.xml index 98839a883e..af6e3db1fa 100644 --- a/features/createroom/impl/src/main/res/values-ro/translations.xml +++ b/features/createroom/impl/src/main/res/values-ro/translations.xml @@ -3,4 +3,6 @@ "Cameră nouă" "Invitați persoane" "Adaugați persoane" + "A apărut o eroare la încercarea începerii conversației" + "Nu am putut valida ID-ul Matrix al acestui utilizator. Este posibil ca invitația să nu fi fost primită." \ No newline at end of file diff --git a/features/createroom/impl/src/main/res/values/localazy.xml b/features/createroom/impl/src/main/res/values/localazy.xml index 48f055e082..177d588f7e 100644 --- a/features/createroom/impl/src/main/res/values/localazy.xml +++ b/features/createroom/impl/src/main/res/values/localazy.xml @@ -12,4 +12,6 @@ "Create a room" "Topic (optional)" "What is this room about?" + "An error occurred when trying to start a chat" + "We can’t validate this user’s Matrix ID. The invite might not be received." \ No newline at end of file diff --git a/libraries/androidutils/src/main/res/values-es/translations.xml b/libraries/androidutils/src/main/res/values-es/translations.xml new file mode 100644 index 0000000000..80b2b88347 --- /dev/null +++ b/libraries/androidutils/src/main/res/values-es/translations.xml @@ -0,0 +1,4 @@ + + + "No se encontró ninguna aplicación compatible con esta acción." + \ No newline at end of file diff --git a/libraries/androidutils/src/main/res/values-it/translations.xml b/libraries/androidutils/src/main/res/values-it/translations.xml new file mode 100644 index 0000000000..03aaf3ffd1 --- /dev/null +++ b/libraries/androidutils/src/main/res/values-it/translations.xml @@ -0,0 +1,4 @@ + + + "Non è stata trovata alcuna app compatibile per gestire questa azione." + \ No newline at end of file diff --git a/libraries/androidutils/src/main/res/values-ro/translations.xml b/libraries/androidutils/src/main/res/values-ro/translations.xml new file mode 100644 index 0000000000..d2149227c5 --- /dev/null +++ b/libraries/androidutils/src/main/res/values-ro/translations.xml @@ -0,0 +1,4 @@ + + + "Nu a fost găsită nicio aplicație capabilă să gestioneze această acțiune." + \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-es/translations.xml b/libraries/ui-strings/src/main/res/values-es/translations.xml index 58b25eaf3c..564ede34a8 100644 --- a/libraries/ui-strings/src/main/res/values-es/translations.xml +++ b/libraries/ui-strings/src/main/res/values-es/translations.xml @@ -137,11 +137,9 @@ "Desbloquear" "Al desbloquear al usuario, podrás volver a ver todos sus mensajes." "Desbloquear usuario" - "Se ha producido un error al intentar iniciar un chat" - "No podemos validar el ID de Matrix de este usuario. Es posible que no reciba la invitación." "Agitar con fuerza" "Umbral de detección" "General" "Versión: %1$s (%2$s)" "es" - + \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-it/translations.xml b/libraries/ui-strings/src/main/res/values-it/translations.xml index 2eb58f0d6e..a8ec05115a 100644 --- a/libraries/ui-strings/src/main/res/values-it/translations.xml +++ b/libraries/ui-strings/src/main/res/values-it/translations.xml @@ -137,11 +137,9 @@ "Sblocca" "Dopo aver sbloccato l\'utente, potrai vedere nuovamente tutti i suoi messaggi." "Sblocca utente" - "Si è verificato un errore durante il tentativo di avviare una chat" - "Non possiamo convalidare l\'ID Matrix di questo utente. L\'invito potrebbe non essere ricevuto." "Rageshake" "Soglia di rilevamento" "Generali" "Versione: %1$s (%2$s)" "it" - + \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-ro/translations.xml b/libraries/ui-strings/src/main/res/values-ro/translations.xml index ba066efae9..091dd7f36d 100644 --- a/libraries/ui-strings/src/main/res/values-ro/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ro/translations.xml @@ -139,11 +139,9 @@ "Deblocați" "La deblocarea utilizatorului, veți putea vedea din nou toate mesajele de la acesta." "Deblocați utilizatorul" - "A apărut o eroare la încercarea începerii conversației" - "Nu am putut valida ID-ul Matrix al acestui utilizator. Este posibil ca invitația să nu fi fost primită." "Rageshake" "Prag de detecție" "General" "Versiunea: %1$s (%2$s)" "ro" - + \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index de11a74eac..379dd5508f 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -136,6 +136,7 @@ "This is the beginning of %1$s." "This is the beginning of this conversation." "New" + "No Invites" "%1$s invited you" "Block user" "Check if you want to hide all current and future messages from this user" @@ -145,8 +146,6 @@ "Unblock" "On unblocking the user, you will be able to see all messages by them again." "Unblock user" - "An error occurred when trying to start a chat" - "We can’t validate this user’s Matrix ID. The invite might not be received." "Rageshake" "Detection threshold" "General" diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 37f8c895f1..9cbd6cd620 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -28,7 +28,8 @@ { "name": ":features:createroom:impl", "includeRegex": [ - "screen_create_room_.*" + "screen_create_room_.*", + "screen_start_chat_.*" ] }, { From dde2aad600e2ba5fc9bcec65adc5b68d65d495bf Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 7 Apr 2023 16:12:31 +0200 Subject: [PATCH 14/23] screenshots --- ...p_ConfigureRoomViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ..._ConfigureRoomViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...aultGroup_RadioButtonDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 +++ ...ultGroup_RadioButtonLightPreview_0_null,NEXUS_5,1.0,en].png | 3 +++ 4 files changed, 12 insertions(+) create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_RadioButtonDarkPreview_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_RadioButtonLightPreview_0_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..22e0b97b81 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2327de2be62afdacae32b297347fbcd23cd5e6986f513d018068aa981ecc3941 +size 89757 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..54b8ae3f9e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50129ad0c4d75ff5e7b8ffd43b0ffdc40d77a78f5699f9b7194a9c9ef026af69 +size 82189 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_RadioButtonDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_RadioButtonDarkPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e2b920b8ed --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_RadioButtonDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:247d303776a79f310144487784fb3841a430a6b0fa7fdee28485ed6157e42354 +size 7056 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_RadioButtonLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_RadioButtonLightPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b2c1d53b70 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_RadioButtonLightPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d93540b38b57540142d2e57e4ce2caf7424fcd2aa3017ab16449d60c1e156797 +size 6730 From 25fe88c59d601586f2866928f0d8469956b4cd25 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 7 Apr 2023 16:45:35 +0200 Subject: [PATCH 15/23] Add tests for ConfigureRoomPresenter --- features/createroom/impl/build.gradle.kts | 1 + .../ConfigureRoomPresenterTests.kt | 115 ++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt diff --git a/features/createroom/impl/build.gradle.kts b/features/createroom/impl/build.gradle.kts index 77a5ed26be..edb35428a0 100644 --- a/features/createroom/impl/build.gradle.kts +++ b/features/createroom/impl/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { testImplementation(libs.molecule.runtime) testImplementation(libs.test.truth) testImplementation(libs.test.turbine) + testImplementation(libs.test.robolectric) testImplementation(projects.libraries.matrix.test) testImplementation(projects.features.userlist.impl) testImplementation(projects.features.userlist.test) diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt new file mode 100644 index 0000000000..0934909460 --- /dev/null +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt @@ -0,0 +1,115 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.createroom.impl.configureroom + +import android.net.Uri +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.libraries.matrix.test.AN_AVATAR_URL +import io.element.android.libraries.matrix.test.A_MESSAGE +import io.element.android.libraries.matrix.test.A_ROOM_NAME +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class ConfigureRoomPresenterTests { + + private lateinit var presenter: ConfigureRoomPresenter + + @Before + fun setup() { + presenter = ConfigureRoomPresenter(ConfigureRoomPresenterArgs(emptyList())) + } + + @Test + fun `present - initial state`() = runTest { + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.roomName).isEmpty() + assertThat(initialState.topic).isEmpty() + assertThat(initialState.privacy).isNull() + } + } + + @Test + fun `present - create room button is enabled only if the required fields are completed`() = runTest { + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.isCreateButtonEnabled).isFalse() + + // Room name not empty + initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME)) + var newState: ConfigureRoomState = awaitItem() + assertThat(newState.roomName).isEqualTo(A_ROOM_NAME) + assertThat(newState.isCreateButtonEnabled).isFalse() + + // Select privacy + initialState.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(RoomPrivacy.Private)) + newState = awaitItem() + assertThat(newState.privacy).isEqualTo(RoomPrivacy.Private) + assertThat(newState.isCreateButtonEnabled).isTrue() + + // Clear room name + initialState.eventSink(ConfigureRoomEvents.RoomNameChanged("")) + newState = awaitItem() + assertThat(newState.roomName).isEqualTo("") + assertThat(newState.isCreateButtonEnabled).isFalse() + } + } + + @Test + fun `present - state is updated when fields are changed`() = runTest { + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + // Room name + initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME)) + val stateAfterRoomNameChanged = awaitItem() + assertThat(stateAfterRoomNameChanged.roomName).isEqualTo(A_ROOM_NAME) + + // Room topic + stateAfterRoomNameChanged.eventSink(ConfigureRoomEvents.TopicChanged(A_MESSAGE)) + val stateAfterTopicChanged = awaitItem() + assertThat(stateAfterTopicChanged.topic).isEqualTo(A_MESSAGE) + + // Room avatar + val anUri = Uri.parse(AN_AVATAR_URL) + stateAfterTopicChanged.eventSink(ConfigureRoomEvents.AvatarUriChanged(anUri)) + val stateAfterAvatarUriChanged = awaitItem() + assertThat(stateAfterAvatarUriChanged.avatarUri).isEqualTo(anUri) + + // Room privacy + stateAfterAvatarUriChanged.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(RoomPrivacy.Public)) + val stateAfterPrivacyChanged = awaitItem() + assertThat(stateAfterPrivacyChanged.privacy).isEqualTo(RoomPrivacy.Public) + } + } +} + From c50e199be4ea1d060aaf06a217aba5504477df43 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 7 Apr 2023 16:49:01 +0200 Subject: [PATCH 16/23] Changelog --- changelog.d/110.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/110.feature diff --git a/changelog.d/110.feature b/changelog.d/110.feature new file mode 100644 index 0000000000..d28935b65f --- /dev/null +++ b/changelog.d/110.feature @@ -0,0 +1 @@ +[Create and join rooms] Create a room screen (UI) From 2e013c15d8aa0887100d0f9eb9ac86ab49536afa Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 11 Apr 2023 14:31:05 +0200 Subject: [PATCH 17/23] reorder params --- .../createroom/impl/configureroom/ConfigureRoomView.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index 255e208532..39b47eb60e 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -152,9 +152,9 @@ fun ConfigureRoomToolbar( @Composable fun RoomNameWithAvatar( - modifier: Modifier = Modifier, avatarUri: Uri?, roomName: String, + modifier: Modifier = Modifier, onAvatarClick: () -> Unit = {}, onRoomNameChanged: (String) -> Unit = {}, ) { @@ -179,8 +179,8 @@ fun RoomNameWithAvatar( @Composable fun Avatar( - modifier: Modifier = Modifier, avatarUri: Uri?, + modifier: Modifier = Modifier, onClick: () -> Unit = {}, ) { val commonModifier = modifier From 2e34c8e8eb1439601c3547e03efadc7c7003455a Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 13 Apr 2023 15:54:48 +0200 Subject: [PATCH 18/23] declare node callback in local variable --- .../features/createroom/impl/CreateRoomFlowNode.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 137017a2ec..a50c9ab1d0 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 @@ -68,7 +68,7 @@ class CreateRoomFlowNode @AssistedInject constructor( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { NavTarget.Root -> { - createNode(context = buildContext, plugins = listOf(object : CreateRoomRootNode.Callback { + val callback = object : CreateRoomRootNode.Callback { override fun onCreateNewRoom() { backstack.push(NavTarget.NewRoom) } @@ -76,14 +76,16 @@ class CreateRoomFlowNode @AssistedInject constructor( override fun onOpenRoom(roomId: RoomId) { plugins().forEach { it.onOpenRoom(roomId) } } - })) + } + createNode(context = buildContext, plugins = listOf(callback)) } NavTarget.NewRoom -> { - createNode(context = buildContext, plugins = listOf(object : AddPeopleNode.Callback { + val callback = object : AddPeopleNode.Callback { override fun onContinue(selectedUsers: List) { backstack.push(NavTarget.ConfigureRoom(selectedUsers)) } - })) + } + createNode(context = buildContext, plugins = listOf(callback)) } is NavTarget.ConfigureRoom -> { createNode(context = buildContext, plugins = listOf(ConfigureRoomNode.Inputs(navTarget.users))) From 5699dcf39e8b6d273806563400dc60905f9611e3 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 13 Apr 2023 16:18:33 +0200 Subject: [PATCH 19/23] Fix modifier usage --- .../features/createroom/impl/configureroom/ConfigureRoomView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index 39b47eb60e..1ff41bc8af 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -203,7 +203,7 @@ fun Avatar( Icon( imageVector = Icons.Outlined.AddAPhoto, contentDescription = "", - modifier = modifier + modifier = Modifier .align(Alignment.Center) .size(40.dp), tint = MaterialTheme.colorScheme.secondary, From 4337a95a39470a96060fb8d975a5b5472227af68 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 13 Apr 2023 17:49:14 +0200 Subject: [PATCH 20/23] use derived state --- .../impl/configureroom/ConfigureRoomPresenter.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index c8bad29f93..572d5aa2fb 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -18,8 +18,10 @@ package io.element.android.features.createroom.impl.configureroom import android.net.Uri import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf 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 dagger.assisted.Assisted @@ -43,9 +45,10 @@ class ConfigureRoomPresenter @AssistedInject constructor( var topic by rememberSaveable { mutableStateOf("") } var avatarUri by rememberSaveable { mutableStateOf(null) } var privacy by rememberSaveable { mutableStateOf(null) } - val isCreateButtonEnabled by rememberSaveable(roomName, privacy) { - val enabled = roomName.isNotEmpty() && privacy != null - mutableStateOf(enabled) + val isCreateButtonEnabled by remember { + derivedStateOf { + roomName.isNotEmpty() && privacy != null + } } fun handleEvents(event: ConfigureRoomEvents) { From 878b9ccf331edec448d352dae1e615926fa65459 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 13 Apr 2023 17:56:18 +0200 Subject: [PATCH 21/23] Fix hardcoding privacy option --- .../createroom/impl/configureroom/ConfigureRoomView.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index 1ff41bc8af..e61dc1524c 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -261,12 +261,12 @@ fun RoomPrivacyOptions( Column(modifier = modifier.selectableGroup()) { items.forEach { item -> RoomPrivacyOption( - privacy = RoomPrivacy.Private, + privacy = item.privacy, icon = item.icon, title = item.title, description = item.description, isSelected = selected == item.privacy, - onOptionSelected = { onOptionSelected(item.privacy) } + onOptionSelected = onOptionSelected, ) } } From 54b76078a8dc21a55b9f9a830a28dbd706780dc0 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 13 Apr 2023 18:06:17 +0200 Subject: [PATCH 22/23] Pass item to RoomPrivacyOption --- .../impl/configureroom/ConfigureRoomView.kt | 39 ++++++++----------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index e61dc1524c..ee718b555a 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -111,7 +111,7 @@ fun ConfigureRoomView( RoomPrivacyOptions( modifier = Modifier.padding(bottom = 40.dp), selected = state.privacy, - onOptionSelected = { state.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(it)) }, + onOptionSelected = { state.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(it.privacy)) }, ) } } @@ -228,20 +228,19 @@ fun RoomTopic( ) } +data class RoomPrivacyItem( + val privacy: RoomPrivacy, + val icon: ImageVector, + val title: String, + val description: String, +) + @Composable fun RoomPrivacyOptions( selected: RoomPrivacy?, modifier: Modifier = Modifier, - onOptionSelected: (RoomPrivacy) -> Unit = {}, + onOptionSelected: (RoomPrivacyItem) -> Unit = {}, ) { - - data class RoomPrivacyItem( - val privacy: RoomPrivacy, - val icon: ImageVector, - val title: String, - val description: String, - ) - val items = RoomPrivacy.values().map { when (it) { RoomPrivacy.Public -> RoomPrivacyItem( @@ -261,10 +260,7 @@ fun RoomPrivacyOptions( Column(modifier = modifier.selectableGroup()) { items.forEach { item -> RoomPrivacyOption( - privacy = item.privacy, - icon = item.icon, - title = item.title, - description = item.description, + roomPrivacyItem = item, isSelected = selected == item.privacy, onOptionSelected = onOptionSelected, ) @@ -274,27 +270,24 @@ fun RoomPrivacyOptions( @Composable fun RoomPrivacyOption( - privacy: RoomPrivacy, - icon: ImageVector, - title: String, - description: String, + roomPrivacyItem: RoomPrivacyItem, modifier: Modifier = Modifier, isSelected: Boolean = false, - onOptionSelected: (RoomPrivacy) -> Unit = {}, + onOptionSelected: (RoomPrivacyItem) -> Unit = {}, ) { Row( modifier .fillMaxWidth() .selectable( selected = isSelected, - onClick = { onOptionSelected(privacy) }, + onClick = { onOptionSelected(roomPrivacyItem) }, role = Role.RadioButton, ) .padding(8.dp), ) { Icon( modifier = Modifier.padding(horizontal = 8.dp), - imageVector = icon, + imageVector = roomPrivacyItem.icon, contentDescription = "", tint = MaterialTheme.colorScheme.secondary, ) @@ -305,13 +298,13 @@ fun RoomPrivacyOption( .padding(horizontal = 8.dp) ) { Text( - text = title, + text = roomPrivacyItem.title, fontSize = 16.sp, color = MaterialTheme.colorScheme.primary, ) Spacer(Modifier.size(3.dp)) Text( - text = description, + text = roomPrivacyItem.description, fontSize = 12.sp, lineHeight = 17.sp, color = MaterialTheme.colorScheme.tertiary, From d5e62dfbf17499a66f8b0f5d1557340f1231842d Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 13 Apr 2023 23:02:56 +0200 Subject: [PATCH 23/23] Split ConfigureRoomView into multiple files --- .../createroom/impl/components/Avatar.kt | 97 ++++++++++ .../impl/components/LabelledTextField.kt | 84 +++++++++ .../impl/components/RoomPrivacyOption.kt | 116 ++++++++++++ .../impl/configureroom/ConfigureRoomView.kt | 170 +----------------- .../impl/configureroom/RoomPrivacyItem.kt | 56 ++++++ ...atarDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 + ...tarLightPreview_0_null,NEXUS_5,1.0,en].png | 3 + ...ieldDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 + ...eldLightPreview_0_null,NEXUS_5,1.0,en].png | 3 + ...tionDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 + ...ionLightPreview_0_null,NEXUS_5,1.0,en].png | 3 + 11 files changed, 377 insertions(+), 164 deletions(-) create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/Avatar.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/LabelledTextField.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/RoomPrivacyOption.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacyItem.kt create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarDarkPreview_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarLightPreview_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_LabelledTextFieldDarkPreview_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_LabelledTextFieldLightPreview_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_RoomPrivacyOptionDarkPreview_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_RoomPrivacyOptionLightPreview_0_null,NEXUS_5,1.0,en].png diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/Avatar.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/Avatar.kt new file mode 100644 index 0000000000..bbaf5c46e5 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/Avatar.kt @@ -0,0 +1,97 @@ +/* + * 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.components + +import android.net.Uri +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.AddAPhoto +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.graphics.painter.ColorPainter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.preview.debugPlaceholderBackground +import io.element.android.libraries.designsystem.theme.LocalColors +import io.element.android.libraries.designsystem.theme.components.Icon + +@Composable +fun Avatar( + avatarUri: Uri?, + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, +) { + val commonModifier = modifier + .size(70.dp) + .clip(CircleShape) + .clickable(onClick = onClick) + + if (avatarUri != null) { + val context = LocalContext.current + val model = ImageRequest.Builder(context) + .data(avatarUri) + .build() + AsyncImage( + modifier = commonModifier, + model = model, + placeholder = debugPlaceholderBackground(ColorPainter(MaterialTheme.colorScheme.surfaceVariant)), + contentScale = ContentScale.Crop, + contentDescription = null, + ) + } else { + Box(modifier = commonModifier.background(LocalColors.current.quinary)) { + Icon( + imageVector = Icons.Outlined.AddAPhoto, + contentDescription = "", + modifier = Modifier + .align(Alignment.Center) + .size(40.dp), + tint = MaterialTheme.colorScheme.secondary, + ) + } + } +} + +@Preview +@Composable +fun AvatarLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun AvatarDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + Row { + Avatar(null) + Avatar(Uri.EMPTY) + } +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/LabelledTextField.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/LabelledTextField.kt new file mode 100644 index 0000000000..382e4b8de2 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/LabelledTextField.kt @@ -0,0 +1,84 @@ +/* + * 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.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.element.android.features.createroom.impl.R +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextField + +@Composable +fun LabelledTextField( + label: String, + value: String, + modifier: Modifier = Modifier, + placeholder: String = "", + maxLines: Int = 1, + onValueChange: (String) -> Unit = {}, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = label + ) + + TextField( + modifier = Modifier.fillMaxWidth(), + value = value, + placeholder = { Text(placeholder) }, + onValueChange = onValueChange, + maxLines = maxLines, + ) + } +} + +@Preview +@Composable +fun LabelledTextFieldLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun LabelledTextFieldDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + Column { + LabelledTextField( + label = stringResource(R.string.screen_create_room_room_name_label), + value = "", + placeholder = stringResource(R.string.screen_create_room_room_name_placeholder), + ) + LabelledTextField( + label = stringResource(R.string.screen_create_room_room_name_label), + value = "a room name", + placeholder = stringResource(R.string.screen_create_room_room_name_placeholder), + ) + } +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/RoomPrivacyOption.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/RoomPrivacyOption.kt new file mode 100644 index 0000000000..8da6d43fcc --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/RoomPrivacyOption.kt @@ -0,0 +1,116 @@ +/* + * 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.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.selection.selectable +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.element.android.features.createroom.impl.configureroom.RoomPrivacyItem +import io.element.android.features.createroom.impl.configureroom.roomPrivacyItems +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.RadioButton +import io.element.android.libraries.designsystem.theme.components.Text + +@Composable +fun RoomPrivacyOption( + roomPrivacyItem: RoomPrivacyItem, + modifier: Modifier = Modifier, + isSelected: Boolean = false, + onOptionSelected: (RoomPrivacyItem) -> Unit = {}, +) { + Row( + modifier + .fillMaxWidth() + .selectable( + selected = isSelected, + onClick = { onOptionSelected(roomPrivacyItem) }, + role = Role.RadioButton, + ) + .padding(8.dp), + ) { + Icon( + modifier = Modifier.padding(horizontal = 8.dp), + imageVector = roomPrivacyItem.icon, + contentDescription = "", + tint = MaterialTheme.colorScheme.secondary, + ) + + Column( + Modifier + .weight(1f) + .padding(horizontal = 8.dp) + ) { + Text( + text = roomPrivacyItem.title, + fontSize = 16.sp, + color = MaterialTheme.colorScheme.primary, + ) + Spacer(Modifier.size(3.dp)) + Text( + text = roomPrivacyItem.description, + fontSize = 12.sp, + lineHeight = 17.sp, + color = MaterialTheme.colorScheme.tertiary, + ) + } + + RadioButton( + modifier = Modifier + .align(Alignment.CenterVertically) + .size(48.dp), + selected = isSelected, + onClick = null // null recommended for accessibility with screenreaders + ) + } +} + +@Preview +@Composable +fun RoomPrivacyOptionLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun RoomPrivacyOptionDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + val aRoomPrivacyItem = roomPrivacyItems().first() + Column { + RoomPrivacyOption( + roomPrivacyItem = aRoomPrivacyItem, + isSelected = true, + ) + RoomPrivacyOption( + roomPrivacyItem = aRoomPrivacyItem, + isSelected = false, + ) + } +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index ee718b555a..ad0b9b0b79 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -14,61 +14,43 @@ * limitations under the License. */ +@file:OptIn(ExperimentalMaterial3Api::class) + package io.element.android.features.createroom.impl.configureroom import android.net.Uri -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.AddAPhoto -import androidx.compose.material.icons.outlined.Lock -import androidx.compose.material.icons.outlined.Public import androidx.compose.material3.ExperimentalMaterial3Api -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.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role 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 coil.compose.AsyncImage -import coil.request.ImageRequest import io.element.android.features.createroom.impl.R +import io.element.android.features.createroom.impl.components.Avatar +import io.element.android.features.createroom.impl.components.LabelledTextField +import io.element.android.features.createroom.impl.components.RoomPrivacyOption import io.element.android.features.userlist.api.SelectedUsersList 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.LocalColors import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar -import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.RadioButton 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.designsystem.theme.components.TextField import io.element.android.libraries.ui.strings.R as StringR -@OptIn(ExperimentalMaterial3Api::class) @Composable fun ConfigureRoomView( state: ConfigureRoomState, @@ -117,7 +99,6 @@ fun ConfigureRoomView( } } -@OptIn(ExperimentalMaterial3Api::class) @Composable fun ConfigureRoomToolbar( isNextActionEnabled: Boolean, @@ -177,41 +158,6 @@ fun RoomNameWithAvatar( } } -@Composable -fun Avatar( - avatarUri: Uri?, - modifier: Modifier = Modifier, - onClick: () -> Unit = {}, -) { - val commonModifier = modifier - .size(70.dp) - .clip(CircleShape) - .clickable(onClick = onClick) - - if (avatarUri != null) { - val context = LocalContext.current - val model = ImageRequest.Builder(context) - .data(avatarUri) - .build() - AsyncImage( - modifier = commonModifier, - model = model, - contentDescription = null, - ) - } else { - Box(modifier = commonModifier.background(LocalColors.current.quinary)) { - Icon( - imageVector = Icons.Outlined.AddAPhoto, - contentDescription = "", - modifier = Modifier - .align(Alignment.Center) - .size(40.dp), - tint = MaterialTheme.colorScheme.secondary, - ) - } - } -} - @Composable fun RoomTopic( topic: String, @@ -228,35 +174,13 @@ fun RoomTopic( ) } -data class RoomPrivacyItem( - val privacy: RoomPrivacy, - val icon: ImageVector, - val title: String, - val description: String, -) - @Composable fun RoomPrivacyOptions( selected: RoomPrivacy?, modifier: Modifier = Modifier, onOptionSelected: (RoomPrivacyItem) -> Unit = {}, ) { - val items = RoomPrivacy.values().map { - when (it) { - RoomPrivacy.Public -> RoomPrivacyItem( - privacy = it, - icon = Icons.Outlined.Lock, - title = stringResource(R.string.screen_create_room_private_option_title), - description = stringResource(R.string.screen_create_room_private_option_description), - ) - RoomPrivacy.Private -> RoomPrivacyItem( - privacy = it, - icon = Icons.Outlined.Public, - title = stringResource(R.string.screen_create_room_public_option_title), - description = stringResource(R.string.screen_create_room_public_option_description), - ) - } - } + val items = roomPrivacyItems() Column(modifier = modifier.selectableGroup()) { items.forEach { item -> RoomPrivacyOption( @@ -268,88 +192,6 @@ fun RoomPrivacyOptions( } } -@Composable -fun RoomPrivacyOption( - roomPrivacyItem: RoomPrivacyItem, - modifier: Modifier = Modifier, - isSelected: Boolean = false, - onOptionSelected: (RoomPrivacyItem) -> Unit = {}, -) { - Row( - modifier - .fillMaxWidth() - .selectable( - selected = isSelected, - onClick = { onOptionSelected(roomPrivacyItem) }, - role = Role.RadioButton, - ) - .padding(8.dp), - ) { - Icon( - modifier = Modifier.padding(horizontal = 8.dp), - imageVector = roomPrivacyItem.icon, - contentDescription = "", - tint = MaterialTheme.colorScheme.secondary, - ) - - Column( - Modifier - .weight(1f) - .padding(horizontal = 8.dp) - ) { - Text( - text = roomPrivacyItem.title, - fontSize = 16.sp, - color = MaterialTheme.colorScheme.primary, - ) - Spacer(Modifier.size(3.dp)) - Text( - text = roomPrivacyItem.description, - fontSize = 12.sp, - lineHeight = 17.sp, - color = MaterialTheme.colorScheme.tertiary, - ) - } - - RadioButton( - modifier = Modifier - .align(Alignment.CenterVertically) - .size(48.dp), - selected = isSelected, - onClick = null // null recommended for accessibility with screenreaders - ) - } -} - -// Move this composable to design module if we want to reuse it in other screens -@Composable -fun LabelledTextField( - label: String, - value: String, - modifier: Modifier = Modifier, - placeholder: String = "", - maxLines: Int = 1, - onValueChange: (String) -> Unit, -) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - modifier = Modifier.padding(horizontal = 16.dp), - text = label - ) - - TextField( - modifier = Modifier.fillMaxWidth(), - value = value, - placeholder = { Text(placeholder) }, - onValueChange = onValueChange, - maxLines = maxLines, - ) - } -} - @Preview @Composable fun ConfigureRoomViewLightPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacyItem.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacyItem.kt new file mode 100644 index 0000000000..0d1f6011c9 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacyItem.kt @@ -0,0 +1,56 @@ +/* + * 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.configureroom + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Lock +import androidx.compose.material.icons.outlined.Public +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import io.element.android.features.createroom.impl.R +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +data class RoomPrivacyItem( + val privacy: RoomPrivacy, + val icon: ImageVector, + val title: String, + val description: String, +) + +@Composable +fun roomPrivacyItems(): ImmutableList { + return RoomPrivacy.values() + .map { + when (it) { + RoomPrivacy.Public -> RoomPrivacyItem( + privacy = it, + icon = Icons.Outlined.Lock, + title = stringResource(R.string.screen_create_room_private_option_title), + description = stringResource(R.string.screen_create_room_private_option_description), + ) + RoomPrivacy.Private -> RoomPrivacyItem( + privacy = it, + icon = Icons.Outlined.Public, + title = stringResource(R.string.screen_create_room_public_option_title), + description = stringResource(R.string.screen_create_room_public_option_description), + ) + } + } + .toImmutableList() +} diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarDarkPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e5f8881070 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aaeb60c55803711952413d99191209f467b4f77e1b03ad46601111691c2fc7fe +size 38188 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarLightPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8197e52f75 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarLightPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c8ccd79709924e19793977ebe5ab4f6ded7f20507d9534dc24f46513fdd7a69 +size 37844 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_LabelledTextFieldDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_LabelledTextFieldDarkPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6f85c2f397 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_LabelledTextFieldDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b1243c92ee9f3735e81c2ab77910a7323b692a72c3c81c4b2ea776c1f0e5c84 +size 16267 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_LabelledTextFieldLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_LabelledTextFieldLightPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..3229cb3561 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_LabelledTextFieldLightPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7db235009f0e0a50eda1196f2cd24eb490af10ebe42e26cc73fdf8ea2fdb0bf8 +size 15906 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_RoomPrivacyOptionDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_RoomPrivacyOptionDarkPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..582b3621ea --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_RoomPrivacyOptionDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5abeb4bb717e33b34ff0a61d657a01b4e7ad965b5d7f8e4252c7e956c4caada3 +size 38719 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_RoomPrivacyOptionLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_RoomPrivacyOptionLightPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..81a08165fa --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_RoomPrivacyOptionLightPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74c34325693f03c620a64493ea9bead27d8db1719d849ef4e1f589940efc73c9 +size 34559