Merge pull request #308 from vector-im/feature/fre/create_room_screen
Create a room screen (UI)
This commit is contained in:
commit
62866965d9
50 changed files with 1140 additions and 28 deletions
|
|
@ -49,12 +49,14 @@ 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)
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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,6 +60,9 @@ class CreateRoomFlowNode @AssistedInject constructor(
|
|||
|
||||
@Parcelize
|
||||
object NewRoom : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class ConfigureRoom(val users: List<MatrixUser>) : NavTarget
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
|
|
@ -72,9 +77,19 @@ class CreateRoomFlowNode @AssistedInject constructor(
|
|||
plugins<CreateRoomEntryPoint.Callback>().forEach { it.onOpenRoom(roomId) }
|
||||
}
|
||||
}
|
||||
createNode<CreateRoomRootNode>(buildContext, plugins = listOf(callback))
|
||||
createNode<CreateRoomRootNode>(context = buildContext, plugins = listOf(callback))
|
||||
}
|
||||
NavTarget.NewRoom -> {
|
||||
val callback = object : AddPeopleNode.Callback {
|
||||
override fun onContinue(selectedUsers: List<MatrixUser>) {
|
||||
backstack.push(NavTarget.ConfigureRoom(selectedUsers))
|
||||
}
|
||||
}
|
||||
createNode<AddPeopleNode>(context = buildContext, plugins = listOf(callback))
|
||||
}
|
||||
is NavTarget.ConfigureRoom -> {
|
||||
createNode<ConfigureRoomNode>(context = buildContext, plugins = listOf(ConfigureRoomNode.Inputs(navTarget.users)))
|
||||
}
|
||||
NavTarget.NewRoom -> createNode<AddPeopleNode>(buildContext)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<MatrixUser>)
|
||||
}
|
||||
|
||||
private fun onContinue(selectedUsers: List<MatrixUser>) {
|
||||
plugins<Callback>().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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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<MatrixUser>) -> Unit = {},
|
||||
) {
|
||||
val eventSink = state.eventSink
|
||||
|
||||
|
|
@ -56,7 +57,7 @@ fun AddPeopleView(
|
|||
AddPeopleViewTopBar(
|
||||
hasSelectedUsers = state.userListState.selectedUsers.isNotEmpty(),
|
||||
onBackPressed = onBackPressed,
|
||||
onNextPressed = onNextPressed,
|
||||
onNextPressed = { onNextPressed(state.userListState.selectedUsers) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 android.net.Uri
|
||||
|
||||
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
|
||||
object CreateRoom : ConfigureRoomEvents
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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<Plugin>,
|
||||
private val presenterFactory: ConfigureRoomPresenter.Factory,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
data class Inputs(
|
||||
val selectedUsers: List<MatrixUser>
|
||||
) : 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,
|
||||
onBackPressed = { navigateUp() } // TODO we should keep in memory the current view state
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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 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
|
||||
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,
|
||||
) : Presenter<ConfigureRoomState> {
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(args: ConfigureRoomPresenterArgs): ConfigureRoomPresenter
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): ConfigureRoomState {
|
||||
var roomName by rememberSaveable { mutableStateOf("") }
|
||||
var topic by rememberSaveable { mutableStateOf("") }
|
||||
var avatarUri by rememberSaveable { mutableStateOf<Uri?>(null) }
|
||||
var privacy by rememberSaveable { mutableStateOf<RoomPrivacy?>(null) }
|
||||
val isCreateButtonEnabled by remember {
|
||||
derivedStateOf {
|
||||
roomName.isNotEmpty() && privacy != 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
|
||||
ConfigureRoomEvents.CreateRoom -> Unit // TODO
|
||||
}
|
||||
}
|
||||
|
||||
return ConfigureRoomState(
|
||||
selectedUsers = args.selectedUsers.toImmutableList(),
|
||||
roomName = roomName,
|
||||
topic = topic,
|
||||
avatarUri = avatarUri,
|
||||
privacy = privacy,
|
||||
isCreateButtonEnabled = isCreateButtonEnabled,
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<MatrixUser>,
|
||||
)
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 android.net.Uri
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class ConfigureRoomState(
|
||||
val selectedUsers: ImmutableList<MatrixUser>,
|
||||
val roomName: String,
|
||||
val topic: String,
|
||||
val avatarUri: Uri?,
|
||||
val privacy: RoomPrivacy?,
|
||||
val isCreateButtonEnabled: Boolean,
|
||||
val eventSink: (ConfigureRoomEvents) -> Unit
|
||||
)
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomState> {
|
||||
override val values: Sequence<ConfigureRoomState>
|
||||
get() = sequenceOf(
|
||||
aConfigureRoomState(),
|
||||
)
|
||||
}
|
||||
|
||||
fun aConfigureRoomState() = ConfigureRoomState(
|
||||
selectedUsers = aMatrixUserList().toImmutableList(),
|
||||
roomName = "",
|
||||
topic = "",
|
||||
avatarUri = null,
|
||||
privacy = null,
|
||||
isCreateButtonEnabled = false,
|
||||
eventSink = {}
|
||||
)
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
/*
|
||||
* 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(ExperimentalMaterial3Api::class)
|
||||
|
||||
package io.element.android.features.createroom.impl.configureroom
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.padding
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.selection.selectableGroup
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.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.components.CenterAlignedTopAppBar
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@Composable
|
||||
fun ConfigureRoomView(
|
||||
state: ConfigureRoomState,
|
||||
modifier: Modifier = Modifier,
|
||||
onBackPressed: () -> Unit = {},
|
||||
) {
|
||||
val selectedUsersListState = rememberLazyListState()
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
ConfigureRoomToolbar(
|
||||
isNextActionEnabled = state.isCreateButtonEnabled,
|
||||
onBackPressed = onBackPressed,
|
||||
onNextPressed = { state.eventSink(ConfigureRoomEvents.CreateRoom) },
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier.padding(padding),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) {
|
||||
RoomNameWithAvatar(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
avatarUri = state.avatarUri,
|
||||
roomName = state.roomName,
|
||||
onRoomNameChanged = { state.eventSink(ConfigureRoomEvents.RoomNameChanged(it)) },
|
||||
)
|
||||
RoomTopic(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
topic = state.topic,
|
||||
onTopicChanged = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) },
|
||||
)
|
||||
SelectedUsersList(
|
||||
listState = selectedUsersListState,
|
||||
contentPadding = PaddingValues(horizontal = 24.dp),
|
||||
selectedUsers = state.selectedUsers,
|
||||
onUserRemoved = { }, // TODO
|
||||
)
|
||||
Spacer(Modifier.weight(1f))
|
||||
RoomPrivacyOptions(
|
||||
modifier = Modifier.padding(bottom = 40.dp),
|
||||
selected = state.privacy,
|
||||
onOptionSelected = { state.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(it.privacy)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ConfigureRoomToolbar(
|
||||
isNextActionEnabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onBackPressed: () -> Unit = {},
|
||||
onNextPressed: () -> Unit = {},
|
||||
) {
|
||||
CenterAlignedTopAppBar(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.screen_create_room_title),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
},
|
||||
navigationIcon = { BackButton(onClick = onBackPressed) },
|
||||
actions = {
|
||||
TextButton(
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
enabled = isNextActionEnabled,
|
||||
onClick = onNextPressed,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(StringR.string.action_create),
|
||||
fontSize = 16.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RoomNameWithAvatar(
|
||||
avatarUri: Uri?,
|
||||
roomName: String,
|
||||
modifier: Modifier = Modifier,
|
||||
onAvatarClick: () -> Unit = {},
|
||||
onRoomNameChanged: (String) -> Unit = {},
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Avatar(
|
||||
avatarUri = avatarUri,
|
||||
onClick = onAvatarClick,
|
||||
)
|
||||
|
||||
LabelledTextField(
|
||||
label = stringResource(R.string.screen_create_room_room_name_label),
|
||||
value = roomName,
|
||||
placeholder = stringResource(R.string.screen_create_room_room_name_placeholder),
|
||||
onValueChange = onRoomNameChanged
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RoomTopic(
|
||||
topic: String,
|
||||
modifier: Modifier = Modifier,
|
||||
onTopicChanged: (String) -> Unit = {},
|
||||
) {
|
||||
LabelledTextField(
|
||||
modifier = modifier,
|
||||
label = stringResource(R.string.screen_create_room_topic_label),
|
||||
value = topic,
|
||||
placeholder = stringResource(R.string.screen_create_room_topic_placeholder),
|
||||
onValueChange = onTopicChanged,
|
||||
maxLines = 3,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RoomPrivacyOptions(
|
||||
selected: RoomPrivacy?,
|
||||
modifier: Modifier = Modifier,
|
||||
onOptionSelected: (RoomPrivacyItem) -> Unit = {},
|
||||
) {
|
||||
val items = roomPrivacyItems()
|
||||
Column(modifier = modifier.selectableGroup()) {
|
||||
items.forEach { item ->
|
||||
RoomPrivacyOption(
|
||||
roomPrivacyItem = item,
|
||||
isSelected = selected == item.privacy,
|
||||
onOptionSelected = onOptionSelected,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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<RoomPrivacyItem> {
|
||||
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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -3,4 +3,6 @@
|
|||
<string name="screen_create_room_action_create_room">"Nueva sala"</string>
|
||||
<string name="screen_create_room_action_invite_people">"Invitar gente"</string>
|
||||
<string name="screen_create_room_add_people_title">"Añadir personas"</string>
|
||||
<string name="screen_start_chat_error_starting_chat">"Se ha producido un error al intentar iniciar un chat"</string>
|
||||
<string name="screen_start_chat_unknown_profile">"No podemos validar el ID de Matrix de este usuario. Es posible que no reciba la invitación."</string>
|
||||
</resources>
|
||||
|
|
@ -3,4 +3,6 @@
|
|||
<string name="screen_create_room_action_create_room">"Nuova stanza"</string>
|
||||
<string name="screen_create_room_action_invite_people">"Invita persone"</string>
|
||||
<string name="screen_create_room_add_people_title">"Aggiungi persone"</string>
|
||||
<string name="screen_start_chat_error_starting_chat">"Si è verificato un errore durante il tentativo di avviare una chat"</string>
|
||||
<string name="screen_start_chat_unknown_profile">"Non possiamo convalidare l\'ID Matrix di questo utente. L\'invito potrebbe non essere ricevuto."</string>
|
||||
</resources>
|
||||
|
|
@ -3,4 +3,6 @@
|
|||
<string name="screen_create_room_action_create_room">"Cameră nouă"</string>
|
||||
<string name="screen_create_room_action_invite_people">"Invitați persoane"</string>
|
||||
<string name="screen_create_room_add_people_title">"Adaugați persoane"</string>
|
||||
<string name="screen_start_chat_error_starting_chat">"A apărut o eroare la încercarea începerii conversației"</string>
|
||||
<string name="screen_start_chat_unknown_profile">"Nu am putut valida ID-ul Matrix al acestui utilizator. Este posibil ca invitația să nu fi fost primită."</string>
|
||||
</resources>
|
||||
|
|
@ -12,4 +12,6 @@
|
|||
<string name="screen_create_room_title">"Create a room"</string>
|
||||
<string name="screen_create_room_topic_label">"Topic (optional)"</string>
|
||||
<string name="screen_create_room_topic_placeholder">"What is this room about?"</string>
|
||||
<string name="screen_start_chat_error_starting_chat">"An error occurred when trying to start a chat"</string>
|
||||
<string name="screen_start_chat_unknown_profile">"We can’t validate this user’s Matrix ID. The invite might not be received."</string>
|
||||
</resources>
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -97,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))
|
||||
|
|
@ -174,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,
|
||||
)
|
||||
|
|
@ -244,11 +245,13 @@ fun SelectedUsersList(
|
|||
listState: LazyListState,
|
||||
selectedUsers: ImmutableList<MatrixUser>,
|
||||
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 ->
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue