Merge pull request #308 from vector-im/feature/fre/create_room_screen

Create a room screen (UI)
This commit is contained in:
Florian Renaud
2023-04-13 23:40:25 +02:00
committed by GitHub
50 changed files with 1140 additions and 28 deletions

1
changelog.d/110.feature Normal file
View File

@@ -0,0 +1 @@
[Create and join rooms] Create a room screen (UI)

View File

@@ -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)

View File

@@ -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)
}
}

View File

@@ -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,
)
}
}

View File

@@ -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) },
)
}
}

View File

@@ -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)
}
}

View File

@@ -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),
)
}
}

View File

@@ -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,
)
}
}

View File

@@ -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
}

View File

@@ -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
)
}
}

View File

@@ -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,
)
}
}

View File

@@ -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>,
)

View File

@@ -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
)

View File

@@ -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 = {}
)

View File

@@ -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,
)
}

View File

@@ -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,
}

View File

@@ -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()
}

View File

@@ -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(

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 cant validate this users Matrix ID. The invite might not be received."</string>
</resources>

View File

@@ -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)
}
}
}

View File

@@ -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 ->

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="error_no_compatible_app_found">"No se encontró ninguna aplicación compatible con esta acción."</string>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="error_no_compatible_app_found">"Non è stata trovata alcuna app compatibile per gestire questa azione."</string>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="error_no_compatible_app_found">"Nu a fost găsită nicio aplicație capabilă să gestioneze această acțiune."</string>
</resources>

View File

@@ -19,6 +19,7 @@
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.ksp)
id("kotlin-parcelize")
}
android {

View File

@@ -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()

View File

@@ -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 = {})
}
}

View File

@@ -20,6 +20,7 @@ plugins {
id("io.element.android-compose-library")
alias(libs.plugins.anvil)
alias(libs.plugins.ksp)
id("kotlin-parcelize")
}
android {

View File

@@ -38,6 +38,19 @@ fun aMatrixUser(id: String = "@id_of_alice:server.org", userName: String = "Alic
avatarData = anAvatarData()
)
fun aMatrixUserList() = listOf(
aMatrixUser("@alice:server.org", "Alice"),
aMatrixUser("@bob:server.org", "Bob"),
aMatrixUser("@carol:server.org", "Carol"),
aMatrixUser("@david:server.org", "David"),
aMatrixUser("@eve:server.org", "Eve"),
aMatrixUser("@justin:server.org", "Justin"),
aMatrixUser("@mallory:server.org", "Mallory"),
aMatrixUser("@susie:server.org", "Susie"),
aMatrixUser("@victor:server.org", "Victor"),
aMatrixUser("@walter:server.org", "Walter"),
)
open class MatrixUserWithNullProvider : PreviewParameterProvider<MatrixUser?> {
override val values: Sequence<MatrixUser?>
get() = sequenceOf(

View File

@@ -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

View File

@@ -137,11 +137,9 @@
<string name="screen_room_member_details_unblock_alert_action">"Desbloquear"</string>
<string name="screen_room_member_details_unblock_alert_description">"Al desbloquear al usuario, podrás volver a ver todos sus mensajes."</string>
<string name="screen_room_member_details_unblock_user">"Desbloquear usuario"</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>
<string name="settings_rageshake">"Agitar con fuerza"</string>
<string name="settings_rageshake_detection_threshold">"Umbral de detección"</string>
<string name="settings_title_general">"General"</string>
<string name="settings_version_number">"Versión: %1$s (%2$s)"</string>
<string name="test_language_identifier">"es"</string>
</resources>
</resources>

View File

@@ -137,11 +137,9 @@
<string name="screen_room_member_details_unblock_alert_action">"Sblocca"</string>
<string name="screen_room_member_details_unblock_alert_description">"Dopo aver sbloccato l\'utente, potrai vedere nuovamente tutti i suoi messaggi."</string>
<string name="screen_room_member_details_unblock_user">"Sblocca utente"</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>
<string name="settings_rageshake">"Rageshake"</string>
<string name="settings_rageshake_detection_threshold">"Soglia di rilevamento"</string>
<string name="settings_title_general">"Generali"</string>
<string name="settings_version_number">"Versione: %1$s (%2$s)"</string>
<string name="test_language_identifier">"it"</string>
</resources>
</resources>

View File

@@ -139,11 +139,9 @@
<string name="screen_room_member_details_unblock_alert_action">"Deblocați"</string>
<string name="screen_room_member_details_unblock_alert_description">"La deblocarea utilizatorului, veți putea vedea din nou toate mesajele de la acesta."</string>
<string name="screen_room_member_details_unblock_user">"Deblocați utilizatorul"</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>
<string name="settings_rageshake">"Rageshake"</string>
<string name="settings_rageshake_detection_threshold">"Prag de detecție"</string>
<string name="settings_title_general">"General"</string>
<string name="settings_version_number">"Versiunea: %1$s (%2$s)"</string>
<string name="test_language_identifier">"ro"</string>
</resources>
</resources>

View File

@@ -181,6 +181,7 @@
<string name="room_timeline_beginning_of_room">"This is the beginning of %1$s."</string>
<string name="room_timeline_beginning_of_room_no_name">"This is the beginning of this conversation."</string>
<string name="room_timeline_read_marker_title">"New"</string>
<string name="screen_invites_empty_list">"No Invites"</string>
<string name="screen_invites_invited_you">"%1$s invited you"</string>
<string name="screen_report_content_block_user">"Block user"</string>
<string name="screen_report_content_block_user_hint">"Check if you want to hide all current and future messages from this user"</string>
@@ -190,8 +191,6 @@
<string name="screen_room_member_details_unblock_alert_action">"Unblock"</string>
<string name="screen_room_member_details_unblock_alert_description">"On unblocking the user, you will be able to see all messages by them again."</string>
<string name="screen_room_member_details_unblock_user">"Unblock user"</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 cant validate this users Matrix ID. The invite might not be received."</string>
<string name="settings_rageshake">"Rageshake"</string>
<string name="settings_rageshake_detection_threshold">"Detection threshold"</string>
<string name="settings_title_general">"General"</string>

View File

@@ -28,7 +28,8 @@
{
"name": ":features:createroom:impl",
"includeRegex": [
"screen_create_room_.*"
"screen_create_room_.*",
"screen_start_chat_.*"
]
},
{