Create spaces (#5982)

* Allow creating a space with `CreateRoomParameters`

* Add 'Create space' menu item in the spaces home screen. Also, imports new strings related to spaces.

* Link the 'Create space' button with the screen to create the space

* Unify room access and visibility for `ConfigureRoom`, use the updated design

* Fix `EditRoomDetails` avatar size (68dp)

* Replace `EditableAvatarView` and `UnsavedAvatar` copmonents with `AvatarPickerView`

* `AvatarDataFetcherFactory`: Make sure we use a fallback image fetcher when the URL is not an MXC one (a local one, i.e.). This removes the previous need for a separate `UnsavedAvatarView`

* Use `AvatarPickerView` in all the screens where `EditableAvatarView` was used

* Improve naming and previews

* Update strings, remove unused ones for `RoomAccessItem`

* Make `isSpace` part of the `CreateRoomConfig`

* Ensure the content fits in the screenshots for `AvatarPickerSizesPreview`

* Add `AvatarDataFetcherFactoryTest`

* Add new feature flag for creating spaces

* Fix ripple being too large for the `Pick` state

* Tweak margins and section titles a bit

* Add preview for `HomeTopBar` with the spaces case

* Update screenshots

---------

Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
Jorge Martin Espinosa
2026-01-13 14:35:49 +01:00
committed by GitHub
parent a234eb3e29
commit 03d14087e6
150 changed files with 1097 additions and 778 deletions

View File

@@ -47,6 +47,7 @@ import io.element.android.appnav.room.RoomFlowNode
import io.element.android.appnav.room.RoomNavigationTarget
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.features.createroom.api.CreateRoomEntryPoint
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.api.SessionEnterpriseService
import io.element.android.features.ftue.api.FtueEntryPoint
@@ -144,6 +145,7 @@ class LoggedInFlowNode(
snackbarDispatcher: SnackbarDispatcher,
private val analyticsService: AnalyticsService,
private val analyticsRoomListStateWatcher: AnalyticsRoomListStateWatcher,
private val createRoomEntryPoint: CreateRoomEntryPoint,
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Placeholder,
@@ -287,6 +289,9 @@ class LoggedInFlowNode(
@Parcelize
data object CreateRoom : NavTarget
@Parcelize
data object CreateSpace : NavTarget
@Parcelize
data class SecureBackup(
val initialElement: SecureBackupEntryPoint.InitialTarget = SecureBackupEntryPoint.InitialTarget.Root
@@ -338,6 +343,10 @@ class LoggedInFlowNode(
backstack.push(NavTarget.CreateRoom)
}
override fun navigateToCreateSpace() {
backstack.push(NavTarget.CreateSpace)
}
override fun navigateToSetUpRecovery() {
backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.Root))
}
@@ -469,6 +478,14 @@ class LoggedInFlowNode(
callback = callback,
)
}
is NavTarget.CreateSpace -> {
val callback = object : CreateRoomEntryPoint.Callback {
override fun onRoomCreated(roomId: RoomId) {
backstack.replace(NavTarget.Room(roomIdOrAlias = RoomIdOrAlias.Id(roomId), serverNames = emptyList()))
}
}
createRoomEntryPoint.createNode(isSpace = true, parentNode = this, buildContext = buildContext, callback = callback)
}
is NavTarget.SecureBackup -> {
secureBackupEntryPoint.createNode(
parentNode = this,

View File

@@ -16,6 +16,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
interface CreateRoomEntryPoint : FeatureEntryPoint {
fun createNode(
isSpace: Boolean,
parentNode: Node,
buildContext: BuildContext,
callback: Callback,

View File

@@ -24,6 +24,7 @@ import io.element.android.features.createroom.impl.addpeople.AddPeopleNode
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomNode
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.SessionScope
@@ -37,23 +38,29 @@ class CreateRoomFlowNode(
@Assisted plugins: List<Plugin>,
) : BaseFlowNode<CreateRoomFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.ConfigureRoom,
initialElement = NavTarget.ConfigureRoom(isSpace = plugins.filterIsInstance<Inputs>().first().isSpace),
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
plugins = plugins
) {
@Parcelize
data class Inputs(
val isSpace: Boolean
) : NodeInputs, Parcelable
private val callback: CreateRoomEntryPoint.Callback = callback()
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
NavTarget.ConfigureRoom -> {
is NavTarget.ConfigureRoom -> {
val inputs = ConfigureRoomNode.Inputs(isSpace = navTarget.isSpace)
val callback = object : ConfigureRoomNode.Callback {
override fun onCreateRoomSuccess(roomId: RoomId) {
backstack.replace(NavTarget.AddPeople(roomId))
}
}
createNode<ConfigureRoomNode>(buildContext, plugins = listOf(callback))
createNode<ConfigureRoomNode>(buildContext, plugins = listOf(inputs, callback))
}
is NavTarget.AddPeople -> {
val inputs = AddPeopleNode.Inputs(navTarget.roomId)
@@ -74,7 +81,7 @@ class CreateRoomFlowNode(
sealed interface NavTarget : Parcelable {
@Parcelize
data object ConfigureRoom : NavTarget
data class ConfigureRoom(val isSpace: Boolean) : NavTarget
@Parcelize
data class AddPeople(val roomId: RoomId) : NavTarget

View File

@@ -18,10 +18,12 @@ import io.element.android.libraries.di.SessionScope
@ContributesBinding(SessionScope::class)
class DefaultCreateRoomEntryPoint : CreateRoomEntryPoint {
override fun createNode(
isSpace: Boolean,
parentNode: Node,
buildContext: BuildContext,
callback: CreateRoomEntryPoint.Callback,
): Node {
return parentNode.createNode<CreateRoomFlowNode>(buildContext, listOf(callback))
val inputs = CreateRoomFlowNode.Inputs(isSpace)
return parentNode.createNode<CreateRoomFlowNode>(buildContext, listOf(inputs, callback))
}
}

View File

@@ -8,6 +8,7 @@
package io.element.android.features.createroom.impl.configureroom
import android.os.Parcelable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.lifecycle.subscribe
@@ -18,23 +19,35 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
@AssistedInject
class ConfigureRoomNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: ConfigureRoomPresenter,
presenterFactory: ConfigureRoomPresenter.Factory,
private val analyticsService: AnalyticsService,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun onCreateRoomSuccess(roomId: RoomId)
}
@Parcelize
data class Inputs(
val isSpace: Boolean,
) : NodeInputs, Parcelable
private val inputs = inputs<Inputs>()
private val presenter = presenterFactory.create(inputs.isSpace)
init {
lifecycle.subscribe(
onResume = {

View File

@@ -19,7 +19,9 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.core.net.toUri
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.CreatedRoom
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
@@ -49,8 +51,9 @@ import kotlinx.coroutines.launch
import timber.log.Timber
import kotlin.jvm.optionals.getOrDefault
@Inject
@AssistedInject
class ConfigureRoomPresenter(
@Assisted private val isSpace: Boolean,
private val dataStore: CreateRoomConfigStore,
private val matrixClient: MatrixClient,
private val mediaPickerProvider: PickerProvider,
@@ -61,13 +64,22 @@ class ConfigureRoomPresenter(
private val roomAliasHelper: RoomAliasHelper,
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
) : Presenter<ConfigureRoomState> {
@AssistedFactory
interface Factory {
fun create(isSpace: Boolean): ConfigureRoomPresenter
}
private val cameraPermissionPresenter: PermissionsPresenter = permissionsPresenterFactory.create(android.Manifest.permission.CAMERA)
private var pendingPermissionRequest = false
init {
dataStore.setIsSpace(isSpace)
}
@Composable
override fun present(): ConfigureRoomState {
val cameraPermissionState = cameraPermissionPresenter.present()
val createRoomConfig by dataStore.getCreateRoomConfigFlow().collectAsState(CreateRoomConfig())
val createRoomConfig by dataStore.getCreateRoomConfigFlow().collectAsState()
val homeserverName = remember { matrixClient.userIdServerName() }
val isKnockFeatureEnabled by remember {
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock)
@@ -171,7 +183,8 @@ class ConfigureRoomPresenter(
preset = RoomPreset.PUBLIC_CHAT,
invite = config.invites.map { it.userId },
avatar = avatarUrl,
roomAliasName = config.roomVisibility.roomAddress()
roomAliasName = config.roomVisibility.roomAddress(),
isSpace = isSpace,
)
} else {
CreateRoomParameters(
@@ -184,6 +197,7 @@ class ConfigureRoomPresenter(
preset = RoomPreset.PRIVATE_CHAT,
invite = config.invites.map { it.userId },
avatar = avatarUrl,
isSpace = isSpace,
)
}
matrixClient.createRoom(params)

View File

@@ -78,6 +78,18 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
),
roomAddressValidity = RoomAddressValidity.Valid,
),
aConfigureRoomState(
config = CreateRoomConfig(
isSpace = true,
roomName = "Space 101",
topic = "Space topic for this space when the text goes onto multiple lines and is really long, there shouldnt be more than 3 lines",
roomVisibility = RoomVisibilityState.Public(
roomAddress = RoomAddress.AutoFilled("Space-101"),
roomAccess = RoomAccess.Anyone,
),
),
roomAddressValidity = RoomAddressValidity.Valid,
),
)
}

View File

@@ -8,15 +8,16 @@
package io.element.android.features.createroom.impl.configureroom
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.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.foundation.text.KeyboardOptions
@@ -27,10 +28,9 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
@@ -41,15 +41,18 @@ import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtomSize
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.icons.CompoundDrawables
import io.element.android.libraries.designsystem.modifiers.clearFocusOnTap
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.ListSectionHeader
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
@@ -57,10 +60,12 @@ import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet
import io.element.android.libraries.matrix.ui.components.UnsavedAvatar
import io.element.android.libraries.matrix.ui.components.AvatarPickerState
import io.element.android.libraries.matrix.ui.components.AvatarPickerView
import io.element.android.libraries.matrix.ui.room.address.RoomAddressField
import io.element.android.libraries.permissions.api.PermissionsView
import io.element.android.libraries.ui.strings.CommonStrings
import kotlin.jvm.optionals.getOrNull
@Composable
fun ConfigureRoomView(
@@ -69,6 +74,7 @@ fun ConfigureRoomView(
onCreateRoomSuccess: (RoomId) -> Unit,
modifier: Modifier = Modifier,
) {
val isSpace = state.config.isSpace
val focusManager = LocalFocusManager.current
val isAvatarActionsSheetVisible = remember { mutableStateOf(false) }
@@ -81,6 +87,7 @@ fun ConfigureRoomView(
modifier = modifier.clearFocusOnTap(focusManager),
topBar = {
ConfigureRoomToolbar(
isSpace = isSpace,
isNextActionEnabled = state.isValid,
onBackClick = onBackClick,
onNextClick = {
@@ -96,9 +103,10 @@ fun ConfigureRoomView(
.imePadding()
.verticalScroll(rememberScrollState())
.consumeWindowInsets(padding),
verticalArrangement = Arrangement.spacedBy(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
RoomNameWithAvatar(
isSpace = isSpace,
modifier = Modifier.padding(horizontal = 16.dp),
avatarUri = state.config.avatarUri,
roomName = state.config.roomName.orEmpty(),
@@ -110,37 +118,35 @@ fun ConfigureRoomView(
topic = state.config.topic.orEmpty(),
onTopicChange = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) },
)
RoomVisibilityOptions(
RoomVisibilityAndAccessOptions(
selected = when (state.config.roomVisibility) {
is RoomVisibilityState.Private -> RoomVisibilityItem.Private
is RoomVisibilityState.Public -> RoomVisibilityItem.Public
is RoomVisibilityState.Public -> when (state.config.roomVisibility.roomAccess) {
RoomAccess.Knocking -> RoomVisibilityItem.AskToJoin
RoomAccess.Anyone -> RoomVisibilityItem.Public
}
},
isKnockingEnabled = state.isKnockFeatureEnabled,
onOptionClick = {
focusManager.clearFocus()
state.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(it))
},
)
if (state.config.roomVisibility is RoomVisibilityState.Public && state.isKnockFeatureEnabled) {
RoomAccessOptions(
selected = when (state.config.roomVisibility.roomAccess) {
RoomAccess.Anyone -> RoomAccessItem.Anyone
RoomAccess.Knocking -> RoomAccessItem.AskToJoin
},
onOptionClick = {
focusManager.clearFocus()
state.eventSink(ConfigureRoomEvents.RoomAccessChanged(it))
},
)
RoomAddressField(
modifier = Modifier.padding(horizontal = 16.dp),
address = state.config.roomVisibility.roomAddress.value,
homeserverName = state.homeserverName,
addressValidity = state.roomAddressValidity,
onAddressChange = { state.eventSink(ConfigureRoomEvents.RoomAddressChanged(it)) },
label = stringResource(R.string.screen_create_room_room_address_section_title),
supportingText = stringResource(R.string.screen_create_room_room_address_section_footer),
)
Spacer(Modifier)
if (state.config.roomVisibility !is RoomVisibilityState.Private) {
Column {
ListSectionHeader(title = stringResource(R.string.screen_create_room_room_address_section_title))
RoomAddressField(
modifier = Modifier.padding(horizontal = 16.dp),
address = state.config.roomVisibility.roomAddress().getOrNull().orEmpty(),
homeserverName = state.homeserverName,
addressValidity = state.roomAddressValidity,
onAddressChange = { state.eventSink(ConfigureRoomEvents.RoomAddressChanged(it)) },
label = null,
supportingText = stringResource(R.string.screen_create_room_room_address_section_footer),
)
}
}
}
}
@@ -156,11 +162,11 @@ fun ConfigureRoomView(
async = state.createRoomAction,
progressDialog = {
AsyncActionViewDefaults.ProgressDialog(
progressText = stringResource(CommonStrings.common_creating_room),
progressText = stringResource(if (isSpace) CommonStrings.common_creating_space else CommonStrings.common_creating_room),
)
},
onSuccess = { onCreateRoomSuccess(it) },
errorMessage = { stringResource(R.string.screen_create_room_error_creating_room) },
errorMessage = { stringResource(if (isSpace) R.string.screen_create_room_error_creating_space else R.string.screen_create_room_error_creating_room) },
onRetry = { state.eventSink(ConfigureRoomEvents.CreateRoom) },
onErrorDismiss = { state.eventSink(ConfigureRoomEvents.CancelCreateRoom) },
)
@@ -173,12 +179,13 @@ fun ConfigureRoomView(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ConfigureRoomToolbar(
isSpace: Boolean,
isNextActionEnabled: Boolean,
onBackClick: () -> Unit,
onNextClick: () -> Unit,
) {
TopAppBar(
titleStr = stringResource(R.string.screen_create_room_title),
titleStr = stringResource(if (isSpace) R.string.screen_create_room_new_space_title else R.string.screen_create_room_new_room_title),
navigationIcon = { BackButton(onClick = onBackClick) },
actions = {
TextButton(
@@ -192,6 +199,7 @@ private fun ConfigureRoomToolbar(
@Composable
private fun RoomNameWithAvatar(
isSpace: Boolean,
avatarUri: String?,
roomName: String,
onAvatarClick: () -> Unit,
@@ -203,25 +211,33 @@ private fun RoomNameWithAvatar(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
val a11yAvatar = stringResource(CommonStrings.a11y_room_avatar)
UnsavedAvatar(
avatarUri = avatarUri,
avatarSize = AvatarSize.EditRoomDetails,
avatarType = AvatarType.Room(),
modifier = Modifier
.clickable(
onClick = onAvatarClick,
onClickLabel = stringResource(CommonStrings.action_open_context_menu),
)
.clearAndSetSemantics {
contentDescription = a11yAvatar
},
)
Box(
modifier = Modifier.padding(end = 8.dp).size(AvatarSize.EditRoomDetails.dp),
contentAlignment = Alignment.Center,
) {
val avatarState = remember(avatarUri) {
if (avatarUri != null) {
AvatarPickerState.Selected(
avatarData = AvatarData(id = "#", name = null, url = avatarUri, size = AvatarSize.EditRoomDetails),
type = if (isSpace) AvatarType.Space() else AvatarType.Room(),
)
} else {
val containerSize = 48.dp
val padding = PaddingValues((AvatarSize.EditRoomDetails.dp - containerSize) / 2)
AvatarPickerState.Pick(buttonSize = 48.dp, iconSize = 24.dp, externalPadding = padding)
}
}
AvatarPickerView(
state = avatarState,
onClick = onAvatarClick,
)
}
TextField(
label = stringResource(R.string.screen_create_room_room_name_label),
modifier = Modifier.padding(bottom = 18.dp),
label = stringResource(CommonStrings.common_name),
value = roomName,
placeholder = stringResource(CommonStrings.common_room_name_placeholder),
placeholder = stringResource(R.string.screen_create_room_name_placeholder),
singleLine = true,
onValueChange = onChangeRoomName,
)
@@ -240,7 +256,7 @@ private fun RoomTopic(
value = topic,
onValueChange = onTopicChange,
maxLines = 3,
supportingText = stringResource(CommonStrings.common_topic_placeholder),
placeholder = stringResource(R.string.screen_create_room_topic_placeholder),
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.Sentences,
),
@@ -256,38 +272,58 @@ private fun ConfigureRoomOptions(
Column(
modifier = modifier.selectableGroup()
) {
Text(
text = title,
style = ElementTheme.typography.fontBodyLgMedium,
color = ElementTheme.colors.textPrimary,
modifier = Modifier.padding(horizontal = 16.dp),
)
ListSectionHeader(title = title)
content()
}
}
@Composable
private fun RoomVisibilityOptions(
private fun RoomVisibilityAndAccessOptions(
selected: RoomVisibilityItem,
isKnockingEnabled: Boolean,
onOptionClick: (RoomVisibilityItem) -> Unit,
modifier: Modifier = Modifier,
) {
ConfigureRoomOptions(
title = stringResource(R.string.screen_create_room_room_visibility_section_title),
title = stringResource(R.string.screen_create_room_room_access_section_title),
modifier = modifier,
) {
RoomVisibilityItem.entries.forEach { item ->
if (item == RoomVisibilityItem.AskToJoin && !isKnockingEnabled) {
return@forEach
}
val isSelected = item == selected
ListItem(
leadingContent = ListItemContent.Custom {
RoundedIconAtom(
size = RoundedIconAtomSize.Big,
resourceId = item.icon,
resourceId = when (item) {
RoomVisibilityItem.Public -> CompoundDrawables.ic_compound_public
RoomVisibilityItem.AskToJoin -> CompoundDrawables.ic_compound_user_add
RoomVisibilityItem.Private -> CompoundDrawables.ic_compound_lock
},
tint = if (isSelected) ElementTheme.colors.iconPrimary else ElementTheme.colors.iconSecondary,
backgroundTint = Color.Transparent,
)
},
headlineContent = { Text(text = stringResource(item.title)) },
supportingContent = { Text(text = stringResource(item.description)) },
headlineContent = {
val title = when (item) {
RoomVisibilityItem.Public -> stringResource(R.string.screen_create_room_public_option_title)
RoomVisibilityItem.AskToJoin -> stringResource(R.string.screen_create_room_room_access_section_knocking_option_title)
RoomVisibilityItem.Private -> stringResource(R.string.screen_create_room_private_option_title)
}
Text(text = title)
},
supportingContent = {
// TODO handle description of items in a certain space/org
val description = when (item) {
RoomVisibilityItem.Public -> stringResource(R.string.screen_create_room_public_option_short_description)
RoomVisibilityItem.AskToJoin -> stringResource(R.string.screen_create_room_room_access_section_knocking_option_description)
RoomVisibilityItem.Private -> stringResource(R.string.screen_create_room_private_option_description)
}
Text(text = description)
},
trailingContent = ListItemContent.RadioButton(selected = isSelected),
onClick = { onOptionClick(item) },
)
@@ -295,27 +331,6 @@ private fun RoomVisibilityOptions(
}
}
@Composable
private fun RoomAccessOptions(
selected: RoomAccessItem,
onOptionClick: (RoomAccessItem) -> Unit,
modifier: Modifier = Modifier,
) {
ConfigureRoomOptions(
title = stringResource(R.string.screen_create_room_room_access_section_header),
modifier = modifier,
) {
RoomAccessItem.entries.forEach { item ->
ListItem(
headlineContent = { Text(text = stringResource(item.title)) },
supportingContent = { Text(text = stringResource(item.description)) },
trailingContent = ListItemContent.RadioButton(selected = item == selected),
onClick = { onOptionClick(item) },
)
}
}
}
@PreviewWithLargeHeight
@Composable
internal fun ConfigureRoomViewLightPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) =

View File

@@ -13,6 +13,7 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
data class CreateRoomConfig(
val isSpace: Boolean = false,
val roomName: String? = null,
val topic: String? = null,
val avatarUri: String? = null,

View File

@@ -72,11 +72,11 @@ class CreateRoomConfigStore(
config.copy(
roomVisibility = when (visibility) {
RoomVisibilityItem.Private -> RoomVisibilityState.Private
RoomVisibilityItem.Public -> {
RoomVisibilityItem.Public, RoomVisibilityItem.AskToJoin -> {
val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(config.roomName.orEmpty())
RoomVisibilityState.Public(
roomAddress = RoomAddress.AutoFilled(roomAliasName),
roomAccess = RoomAccess.Anyone,
roomAccess = if (visibility == RoomVisibilityItem.AskToJoin) RoomAccess.Knocking else RoomAccess.Anyone,
)
}
}
@@ -114,6 +114,12 @@ class CreateRoomConfigStore(
}
}
fun setIsSpace(isSpace: Boolean) {
createRoomConfigFlow.getAndUpdate { config ->
config.copy(isSpace = isSpace)
}
}
fun clearCachedData() {
cachedAvatarUri = null
}

View File

@@ -8,19 +8,7 @@
package io.element.android.features.createroom.impl.configureroom
import androidx.annotation.StringRes
import io.element.android.features.createroom.impl.R
enum class RoomAccessItem(
@StringRes val title: Int,
@StringRes val description: Int
) {
Anyone(
title = R.string.screen_create_room_room_access_section_anyone_option_title,
description = R.string.screen_create_room_room_access_section_anyone_option_description,
),
AskToJoin(
title = R.string.screen_create_room_room_access_section_knocking_option_title,
description = R.string.screen_create_room_room_access_section_knocking_option_description,
),
enum class RoomAccessItem {
Anyone,
AskToJoin,
}

View File

@@ -8,24 +8,8 @@
package io.element.android.features.createroom.impl.configureroom
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import io.element.android.features.createroom.impl.R
import io.element.android.libraries.designsystem.icons.CompoundDrawables
enum class RoomVisibilityItem(
@DrawableRes val icon: Int,
@StringRes val title: Int,
@StringRes val description: Int
) {
Private(
icon = CompoundDrawables.ic_compound_lock,
title = R.string.screen_create_room_private_option_title,
description = R.string.screen_create_room_private_option_description,
),
Public(
icon = CompoundDrawables.ic_compound_public,
title = R.string.screen_create_room_public_option_title,
description = R.string.screen_create_room_public_option_description,
)
enum class RoomVisibilityItem {
Public,
AskToJoin,
Private
}

View File

@@ -8,10 +8,6 @@
<string name="screen_create_room_public_option_description">"Любы можа знайсці гэты пакой.
Вы можаце змяніць гэта ў любы час у наладах пакоя."</string>
<string name="screen_create_room_public_option_title">"Публічны пакой"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Хто заўгодна"</string>
<string name="screen_create_room_room_access_section_header">"Доступ у пакой"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Папрасіце далучыцца"</string>
<string name="screen_create_room_room_name_label">"Назва пакоя"</string>
<string name="screen_create_room_title">"Стварыце пакой"</string>
<string name="screen_create_room_topic_label">"Тэма (неабавязкова)"</string>
</resources>

View File

@@ -8,11 +8,7 @@
<string name="screen_create_room_public_option_description">"Всеки може да намери тази стая.
Можете да промените това по всяко време в настройките на стаята."</string>
<string name="screen_create_room_public_option_title">"Общодостъпна стая"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Всеки може да се присъедини към тази стая"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Всеки"</string>
<string name="screen_create_room_room_address_section_footer">"За да бъде тази стая видима в директорията на общодостъпните стаи, ще ви е необходим адрес на стаята."</string>
<string name="screen_create_room_room_name_label">"Име на стаята"</string>
<string name="screen_create_room_room_visibility_section_title">"Видимост на стаята"</string>
<string name="screen_create_room_title">"Създаване на стая"</string>
<string name="screen_create_room_topic_label">"Тема за разговор (незадължително)"</string>
</resources>

View File

@@ -8,15 +8,10 @@
<string name="screen_create_room_public_option_description">"Tuto místnost může najít kdokoli.
To můžete kdykoli změnit v nastavení místnosti."</string>
<string name="screen_create_room_public_option_title">"Veřejná místnost"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Do této místnosti může vstoupit kdokoli"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Kdokoliv"</string>
<string name="screen_create_room_room_access_section_header">"Přístup do místnosti"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Kdokoli může požádat o vstup do místnosti, ale správce nebo moderátor bude muset žádost přijmout"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Požádat o připojení"</string>
<string name="screen_create_room_room_address_section_footer">"Aby byla tato místnost viditelná v adresáři veřejných místností, budete potřebovat adresu místnosti."</string>
<string name="screen_create_room_room_address_section_title">"Adresa místnosti"</string>
<string name="screen_create_room_room_name_label">"Název místnosti"</string>
<string name="screen_create_room_room_visibility_section_title">"Viditelnost místnosti"</string>
<string name="screen_create_room_title">"Vytvořit místnost"</string>
<string name="screen_create_room_topic_label">"Téma (nepovinné)"</string>
</resources>

View File

@@ -8,15 +8,10 @@
<string name="screen_create_room_public_option_description">"Gall unrhyw un ddod o hyd i\'r ystafell hon.
Gallwch newid hyn unrhyw bryd yng ngosodiadau ystafell."</string>
<string name="screen_create_room_public_option_title">"Ystafell gyhoeddus"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Gall unrhyw un ymuno â\'r ystafell hon"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Unrhyw un"</string>
<string name="screen_create_room_room_access_section_header">"Mynediad i\'r Ystafell"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Gall unrhyw un ofyn am gael ymuno â\'r ystafell ond bydd rhaid i weinyddwr neu gymedrolwr dderbyn y cais"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Gofyn i gael ymuno"</string>
<string name="screen_create_room_room_address_section_footer">"Er mwyn i\'r ystafell hon fod yn weladwy yn y cyfeiriadur ystafelloedd cyhoeddus, bydd angen cyfeiriad ystafell arnoch."</string>
<string name="screen_create_room_room_address_section_title">"Cyfeiriad yr ystafell"</string>
<string name="screen_create_room_room_name_label">"Enw\'r ystafell"</string>
<string name="screen_create_room_room_visibility_section_title">"Gwelededd yr ystafell"</string>
<string name="screen_create_room_title">"Creu ystafell"</string>
<string name="screen_create_room_topic_label">"Pwnc (dewisol)"</string>
</resources>

View File

@@ -8,15 +8,10 @@
<string name="screen_create_room_public_option_description">"Alle kan finde dette rum.
Du kan ændre dette når som helst i rummets indstillinger."</string>
<string name="screen_create_room_public_option_title">"Offentligt rum"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Alle kan deltage i dette rum"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Enhver"</string>
<string name="screen_create_room_room_access_section_header">"Adgang til rummet"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Alle kan bede om at deltage i rummet, men en administrator eller en moderator skal acceptere anmodningen"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Spørg om at deltage"</string>
<string name="screen_create_room_room_address_section_footer">"Hvis dette rum skal være synligt i det offentlige register, skal du bruge en rum-adresse."</string>
<string name="screen_create_room_room_address_section_title">"Rummets adresse"</string>
<string name="screen_create_room_room_name_label">"Navn på rum"</string>
<string name="screen_create_room_room_visibility_section_title">"Rummets synlighed"</string>
<string name="screen_create_room_title">"Opret et rum"</string>
<string name="screen_create_room_topic_label">"Emne (valgfrit)"</string>
</resources>

View File

@@ -8,15 +8,10 @@
<string name="screen_create_room_public_option_description">"Jeder kann diesen Chat finden.
Du kannst dies jederzeit in den Einstellungen des Chats ändern."</string>
<string name="screen_create_room_public_option_title">"Öffentlicher Chat"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Jeder darf diesem Chat beitreten"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Jeder"</string>
<string name="screen_create_room_room_access_section_header">"Chat Zugang"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Jeder kann den Beitritt zum Chat erbitten, aber ein Admin oder Moderator muss die Anfrage akzeptieren."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Beitritt beantragen"</string>
<string name="screen_create_room_room_address_section_footer">"Du benötigst eine Chat-Adresse, damit dieser Chat im öffentlichen Verzeichnis sichtbar ist."</string>
<string name="screen_create_room_room_address_section_title">"Chatroom Adresse"</string>
<string name="screen_create_room_room_name_label">"Chat-Name"</string>
<string name="screen_create_room_room_visibility_section_title">" Sichtbarkeit des Chats"</string>
<string name="screen_create_room_title">"Chat erstellen"</string>
<string name="screen_create_room_topic_label">"Thema (optional)"</string>
</resources>

View File

@@ -8,15 +8,10 @@
<string name="screen_create_room_public_option_description">"Ο καθένας μπορεί να βρει αυτή την αίθουσα.
Αυτό μπορείτε να το αλλάξετε ανά πάσα στιγμή στις ρυθμίσεις της αίθουσας."</string>
<string name="screen_create_room_public_option_title">"Δημόσια αίθουσα"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Οποιοσδήποτε μπορεί να συμμετάσχει σε αυτή την αίθουσα"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Οποιοσδήποτε"</string>
<string name="screen_create_room_room_access_section_header">"Πρόσβαση στην Αίθουσα"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Οποιοσδήποτε μπορεί να ζητήσει να συμμετάσχει στην αίθουσα, αλλά ένας διαχειριστής ή ένας συντονιστής θα πρέπει να αποδεχτεί το αίτημα"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Αίτημα συμμετοχής"</string>
<string name="screen_create_room_room_address_section_footer">"Για να είναι ορατή αυτή η αίθουσα στον δημόσιο κατάλογο αιθουσών, θα χρειαστείτε μια διεύθυνση αίθουσας."</string>
<string name="screen_create_room_room_address_section_title">"Διεύθυνση δωματίου"</string>
<string name="screen_create_room_room_name_label">"Όνομα αίθουσας"</string>
<string name="screen_create_room_room_visibility_section_title">"Ορατότητα αίθουσας"</string>
<string name="screen_create_room_title">"Δημιουργία αίθουσας"</string>
<string name="screen_create_room_topic_label">"Θέμα (προαιρετικό)"</string>
</resources>

View File

@@ -8,14 +8,9 @@
<string name="screen_create_room_public_option_description">"Cualquiera puede encontrar esta sala.
Puedes cambiar esto en cualquier momento en los ajustes de la sala."</string>
<string name="screen_create_room_public_option_title">"Sala pública"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Cualquiera puede unirse a esta sala"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Cualquiera"</string>
<string name="screen_create_room_room_access_section_header">"Acceso a la sala"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Cualquiera puede solicitar unirse a la sala, pero un administrador o un moderador tendrá que aceptar la solicitud"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Solicitud para unirse"</string>
<string name="screen_create_room_room_address_section_footer">"Para que esta sala sea visible en el directorio de salas públicas, necesitarás una dirección de sala."</string>
<string name="screen_create_room_room_name_label">"Nombre de la sala"</string>
<string name="screen_create_room_room_visibility_section_title">"Visibilidad de la sala"</string>
<string name="screen_create_room_title">"Crear una sala"</string>
<string name="screen_create_room_topic_label">"Tema (opcional)"</string>
</resources>

View File

@@ -8,15 +8,10 @@
<string name="screen_create_room_public_option_description">"Kõik saavad seda jututuba leida.
Sa võid seda jututoa seadistustest alati muuta."</string>
<string name="screen_create_room_public_option_title">"Avalik jututuba"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Kõik võivad selle jututoaga liituda"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Kõik kasutajad"</string>
<string name="screen_create_room_room_access_section_header">"Ligipääs jututoale"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Kõik võivad paluda selle jututoaga liitumist, kuid peakasutaja või moderaator peavad selle kinnitama"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Küsi võimalust liitumiseks"</string>
<string name="screen_create_room_room_address_section_footer">"Selleks, et see jututuba oleks nähtav jututubade avalikus kataloogis, sa vajad jututoa aadressi."</string>
<string name="screen_create_room_room_address_section_title">"Jututoa aadress"</string>
<string name="screen_create_room_room_name_label">"Jututoa nimi"</string>
<string name="screen_create_room_room_visibility_section_title">"Jututoa nähtavus"</string>
<string name="screen_create_room_title">"Loo jututuba"</string>
<string name="screen_create_room_topic_label">"Teema (kui soovid lisada)"</string>
</resources>

View File

@@ -8,12 +8,7 @@
<string name="screen_create_room_public_option_description">"Edonork aurki dezake gela hau.
Gelaren ezarpenetan aldatu dezakezu hobespena."</string>
<string name="screen_create_room_public_option_title">"Gela publikoa"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Edonor sar daiteke gela honetara"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Edonork"</string>
<string name="screen_create_room_room_access_section_header">"Gelarako sarbidea"</string>
<string name="screen_create_room_room_address_section_title">"Gelaren helbidea"</string>
<string name="screen_create_room_room_name_label">"Gelaren izena"</string>
<string name="screen_create_room_room_visibility_section_title">"Gelaren ikusgarritasuna"</string>
<string name="screen_create_room_title">"Sortu gela"</string>
<string name="screen_create_room_topic_label">"Mintzagaia (aukerakoa)"</string>
</resources>

View File

@@ -8,13 +8,8 @@
<string name="screen_create_room_public_option_description">"هرکسی می‌تواند اتاق را بیابد.
می‌توانید بعداً در تظیمات اتاق عوضش کنید."</string>
<string name="screen_create_room_public_option_title">"اتاق عمومی"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"هرکسی می‌تواند به این اتاق بپیوندد"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"هرکسی"</string>
<string name="screen_create_room_room_access_section_header">"دسترسی اتاق"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"درخواست دعوت"</string>
<string name="screen_create_room_room_address_section_title">"نشانی اتاق"</string>
<string name="screen_create_room_room_name_label">"نام اتاق"</string>
<string name="screen_create_room_room_visibility_section_title">"نمایانی اتاق"</string>
<string name="screen_create_room_title">"ایجاد اتاق"</string>
<string name="screen_create_room_topic_label">"موضوع (اختیاری)"</string>
</resources>

View File

@@ -8,15 +8,10 @@
<string name="screen_create_room_public_option_description">"Kuka tahansa voi löytää tämän huoneen.
Voit muuttaa tämän milloin tahansa huoneen asetuksista."</string>
<string name="screen_create_room_public_option_title">"Julkinen huone"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Kuka tahansa voi liittyä tähän huoneeseen"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Kuka tahansa"</string>
<string name="screen_create_room_room_access_section_header">"Huoneeseen Pääsy"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Kuka tahansa voi pyytää saada liittyä huoneeseen, mutta ylläpitäjän tai valvojan on hyväksyttävä pyyntö"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Pyydä liittymistä"</string>
<string name="screen_create_room_room_address_section_footer">"Jotta tämä huone näkyisi julkisessa huonehakemistossa, tarvitset huoneen osoitteen."</string>
<string name="screen_create_room_room_address_section_title">"Huoneen osoite"</string>
<string name="screen_create_room_room_name_label">"Huoneen nimi"</string>
<string name="screen_create_room_room_visibility_section_title">"Huoneen näkyvyys"</string>
<string name="screen_create_room_title">"Luo huone"</string>
<string name="screen_create_room_topic_label">"Aihe (valinnainen)"</string>
</resources>

View File

@@ -8,15 +8,10 @@
<string name="screen_create_room_public_option_description">"Nimporte qui peut trouver ce salon.
Vous pouvez modifier cela à tout moment dans les paramètres du salon."</string>
<string name="screen_create_room_public_option_title">"Salon public"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Tout le monde peut rejoindre ce salon"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Tout le monde"</string>
<string name="screen_create_room_room_access_section_header">"Accès au salon"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Tout le monde peut demander à rejoindre le salon, mais un administrateur ou un modérateur devra accepter la demande"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Demander à rejoindre"</string>
<string name="screen_create_room_room_address_section_footer">"Pour que ce salon soit visible dans le répertoire des salons publics, vous aurez besoin dune adresse de salon."</string>
<string name="screen_create_room_room_address_section_title">"Adresse du salon"</string>
<string name="screen_create_room_room_name_label">"Nom du salon"</string>
<string name="screen_create_room_room_visibility_section_title">"Visibilité du salon"</string>
<string name="screen_create_room_title">"Créer un salon"</string>
<string name="screen_create_room_topic_label">"Sujet (facultatif)"</string>
</resources>

View File

@@ -8,15 +8,10 @@
<string name="screen_create_room_public_option_description">"Svatko može pronaći ovu sobu.
To možete u svakom trenutku promijeniti u postavkama sobe."</string>
<string name="screen_create_room_public_option_title">"Javna soba"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Svatko se može pridružiti ovoj sobi"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Svatko"</string>
<string name="screen_create_room_room_access_section_header">"Pristup sobi"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Svatko može zatražiti pridruživanje sobi, ali administrator ili moderator morat će prihvatiti zahtjev."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Zatraži pridruživanje"</string>
<string name="screen_create_room_room_address_section_footer">"Da bi ova soba bila vidljiva u javnom direktoriju soba, trebat će vam adresa sobe."</string>
<string name="screen_create_room_room_address_section_title">"Adresa sobe"</string>
<string name="screen_create_room_room_name_label">"Naziv sobe"</string>
<string name="screen_create_room_room_visibility_section_title">"Vidljivost sobe"</string>
<string name="screen_create_room_title">"Stvori sobu"</string>
<string name="screen_create_room_topic_label">"Tema (neobavezno)"</string>
</resources>

View File

@@ -8,15 +8,10 @@
<string name="screen_create_room_public_option_description">"Bárki megtalálhatja ezt a szobát.
Ezt bármikor módosíthatja a szobabeállításokban."</string>
<string name="screen_create_room_public_option_title">"Nyilvános szoba"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Bárki csatlakozhat ehhez a szobához"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Bárki"</string>
<string name="screen_create_room_room_access_section_header">"Szobahozzáférés"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Bárki kérheti, hogy csatlakozzon a szobához, de egy adminisztrátornak vagy moderátornak el kell fogadnia a kérést"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Csatlakozás kérése"</string>
<string name="screen_create_room_room_address_section_footer">"Ahhoz, hogy ez a szoba látható legyen a nyilvános szobák címtárában, meg kell adnia a szoba címét."</string>
<string name="screen_create_room_room_address_section_title">"Szoba címe"</string>
<string name="screen_create_room_room_name_label">"Szoba neve"</string>
<string name="screen_create_room_room_visibility_section_title">"Szoba láthatósága"</string>
<string name="screen_create_room_title">"Szoba létrehozása"</string>
<string name="screen_create_room_topic_label">"Téma (nem kötelező)"</string>
</resources>

View File

@@ -8,15 +8,10 @@
<string name="screen_create_room_public_option_description">"Siapa pun dapat mencari ruangan ini.
Anda dapat mengubah ini kapan pun dalam pengaturan ruangan."</string>
<string name="screen_create_room_public_option_title">"Ruangan publik"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Siapa pun dapat bergabung dengan ruangan ini"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Siapa pun"</string>
<string name="screen_create_room_room_access_section_header">"Akses Ruangan"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Siapa pun dapat meminta untuk bergabung dengan ruangan tetapi administrator atau moderator harus menerima permintaan tersebut"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Minta untuk bergabung"</string>
<string name="screen_create_room_room_address_section_footer">"Supaya ruangan ini terlihat di direktori ruangan publik, Anda memerlukan alamat ruangan."</string>
<string name="screen_create_room_room_address_section_title">"Alamat ruangan"</string>
<string name="screen_create_room_room_name_label">"Nama ruangan"</string>
<string name="screen_create_room_room_visibility_section_title">"Keterlihatan ruangan"</string>
<string name="screen_create_room_title">"Buat ruangan"</string>
<string name="screen_create_room_topic_label">"Topik (opsional)"</string>
</resources>

View File

@@ -8,15 +8,10 @@
<string name="screen_create_room_public_option_description">"Chiunque può trovare questa stanza.
Puoi modificarlo in qualsiasi momento nelle impostazioni della stanza."</string>
<string name="screen_create_room_public_option_title">"Stanza pubblica"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Chiunque può entrare in questa stanza"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Chiunque"</string>
<string name="screen_create_room_room_access_section_header">"Accesso alla stanza"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Chiunque può chiedere di entrare nella stanza, ma un amministratore o un moderatore dovrà accettare la richiesta"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Chiedi di entrare"</string>
<string name="screen_create_room_room_address_section_footer">"Affinché questa stanza sia visibile nell\'elenco delle stanze pubbliche, è necessario un indirizzo della stanza."</string>
<string name="screen_create_room_room_address_section_title">"Indirizzo della stanza"</string>
<string name="screen_create_room_room_name_label">"Nome stanza"</string>
<string name="screen_create_room_room_visibility_section_title">"Visibilità della stanza"</string>
<string name="screen_create_room_title">"Crea una stanza"</string>
<string name="screen_create_room_topic_label">"Argomento (facoltativo)"</string>
</resources>

View File

@@ -7,7 +7,5 @@
<string name="screen_create_room_private_option_title">"კერძო ოთახი"</string>
<string name="screen_create_room_public_option_description">"ყველას ამ ოთახის მოძებნა შეუძლია.
თქვენ ნებისმიერ დროს შეგიძლიათ ამის შეცვლა ოთახის პარამეტრებში."</string>
<string name="screen_create_room_room_name_label">"ოთახის სახელი"</string>
<string name="screen_create_room_title">"ოთახის შექმნა"</string>
<string name="screen_create_room_topic_label">"თემა (სურვილისამებრ)"</string>
</resources>

View File

@@ -8,14 +8,9 @@
<string name="screen_create_room_public_option_description">"누구나 이 방을 찾을 수 있습니다.
방 설정에서 언제든지 변경할 수 있습니다."</string>
<string name="screen_create_room_public_option_title">"공개 방"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"누구나 이 방에 참여할 수 있습니다."</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"누구나"</string>
<string name="screen_create_room_room_access_section_header">"방 액세스"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"누구나 방에 참여 요청을 할 수 있지만, 관리자나 운영자가 요청을 수락해야 합니다."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"참가 요청"</string>
<string name="screen_create_room_room_address_section_footer">"이 방이 공개 방 디렉토리에 표시되려면 방 주소가 필요합니다."</string>
<string name="screen_create_room_room_name_label">"방 이름"</string>
<string name="screen_create_room_room_visibility_section_title">"방 표시 여부"</string>
<string name="screen_create_room_title">"방 만들기"</string>
<string name="screen_create_room_topic_label">"주제 (선택)"</string>
</resources>

View File

@@ -7,7 +7,5 @@
<string name="screen_create_room_private_option_title">"Privatus kambarys"</string>
<string name="screen_create_room_public_option_description">"Bet kas gali rasti šį kambarį.
Tai galite bet kada pakeisti kambario nustatymuose."</string>
<string name="screen_create_room_room_name_label">"Kambario pavadinimas"</string>
<string name="screen_create_room_title">"Kurti kambarį"</string>
<string name="screen_create_room_topic_label">"Tema (nebūtina)"</string>
</resources>

View File

@@ -8,15 +8,10 @@
<string name="screen_create_room_public_option_description">"Alle kan finne dette rommet.
Du kan endre dette når som helst i rominnstillingene."</string>
<string name="screen_create_room_public_option_title">"Offentlig rom"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Alle kan bli med i dette rommet"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Alle"</string>
<string name="screen_create_room_room_access_section_header">"Tilgang til rom"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Alle kan be om å få bli med i rommet, men en administrator eller moderator må godta forespørselen"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Be om å bli med"</string>
<string name="screen_create_room_room_address_section_footer">"For at dette rommet skal være synlig i den offentlige romkatalogen, trenger du en romadresse."</string>
<string name="screen_create_room_room_address_section_title">"Romadresse"</string>
<string name="screen_create_room_room_name_label">"Romnavn"</string>
<string name="screen_create_room_room_visibility_section_title">"Romsynlighet"</string>
<string name="screen_create_room_title">"Opprett et rom"</string>
<string name="screen_create_room_topic_label">"Emne (valgfritt)"</string>
</resources>

View File

@@ -8,12 +8,7 @@
<string name="screen_create_room_public_option_description">"Iedereen kan deze kamer vinden.
Je kunt dit op elk gewenst moment wijzigen in de kamerinstellingen."</string>
<string name="screen_create_room_public_option_title">"Openbare kamer"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Iedereen kan toetreden tot deze kamer"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Iedereen"</string>
<string name="screen_create_room_room_access_section_header">"Toegang tot de kamer"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Iedereen kan vragen om toe te treden tot de kamer, maar een beheerder of moderator moet het verzoek accepteren"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Vraag om toe te treden"</string>
<string name="screen_create_room_room_name_label">"Naam van de kamer"</string>
<string name="screen_create_room_title">"Creëer een kamer"</string>
<string name="screen_create_room_topic_label">"Onderwerp (optioneel)"</string>
</resources>

View File

@@ -8,15 +8,10 @@
<string name="screen_create_room_public_option_description">"Każdy może znaleźć ten pokój.
Możesz to zmienić w ustawieniach pokoju."</string>
<string name="screen_create_room_public_option_title">"Pokój publiczny"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Każdy może dołączyć do tego pokoju"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Wszyscy"</string>
<string name="screen_create_room_room_access_section_header">"Dostęp do pokoju"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Każdy może poprosić o dołączenie do pokoju, ale administrator lub moderator będzie musiał zatwierdzić prośbę"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Poproś o dołączenie"</string>
<string name="screen_create_room_room_address_section_footer">"Aby ten pokój był widoczny w katalogu pomieszczeń publicznych, będziesz potrzebował adres pokoju."</string>
<string name="screen_create_room_room_address_section_title">"Adres pokoju"</string>
<string name="screen_create_room_room_name_label">"Nazwa pokoju"</string>
<string name="screen_create_room_room_visibility_section_title">"Widoczność pomieszczenia"</string>
<string name="screen_create_room_title">"Utwórz pokój"</string>
<string name="screen_create_room_topic_label">"Temat (opcjonalnie)"</string>
</resources>

View File

@@ -8,15 +8,10 @@
<string name="screen_create_room_public_option_description">"Qualquer um pode encontrar esta sala.
Você pode mudar isso a qualquer momento nas configurações da sala."</string>
<string name="screen_create_room_public_option_title">"Sala pública"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Qualquer pessoa pode entrar nesta sala"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Qualquer pessoa"</string>
<string name="screen_create_room_room_access_section_header">"Acesso à sala"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Qualquer pessoa pode pedir para entrar na sala, mas um administrador ou moderador terá de aceitar a solicitação"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Pedir para entrar"</string>
<string name="screen_create_room_room_address_section_footer">"Para que esta sala fique visível no diretório público de salas, você precisará de um endereço de sala."</string>
<string name="screen_create_room_room_address_section_title">"Endereço da sala"</string>
<string name="screen_create_room_room_name_label">"Nome da sala"</string>
<string name="screen_create_room_room_visibility_section_title">"Visibilidade da sala"</string>
<string name="screen_create_room_title">"Criar uma sala"</string>
<string name="screen_create_room_topic_label">"Tópico (opcional)"</string>
</resources>

View File

@@ -8,15 +8,10 @@
<string name="screen_create_room_public_option_description">"Qualquer um pode encontrar esta sala.
Pode alterar esta opção nas definições da sala."</string>
<string name="screen_create_room_public_option_title">"Sala pública"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Qualquer pessoa pode entrar nesta sala"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Qualquer pessoa"</string>
<string name="screen_create_room_room_access_section_header">"Acesso à sala"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Qualquer pessoa pode pedir para entrar na sala, mas um administrador ou um moderador terá de aceitar o pedido"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Pedir para participar"</string>
<string name="screen_create_room_room_address_section_footer">"Para que esta sala seja visível no diretório público de salas, precisas de um endereço de sala."</string>
<string name="screen_create_room_room_address_section_title">"Endereço da sala"</string>
<string name="screen_create_room_room_name_label">"Nome da sala"</string>
<string name="screen_create_room_room_visibility_section_title">"Visibilidade da sala"</string>
<string name="screen_create_room_title">"Criar uma sala"</string>
<string name="screen_create_room_topic_label">"Descrição (opcional)"</string>
</resources>

View File

@@ -8,15 +8,10 @@
<string name="screen_create_room_public_option_description">"Oricine poate găsi această cameră.
Puteți modifica acest lucru oricând în setări."</string>
<string name="screen_create_room_public_option_title">"Cameră publică"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Oricine se poate alătura acestei camere"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Oricine"</string>
<string name="screen_create_room_room_access_section_header">"Acces la cameră"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Oricine poate cere să se alăture camerei, dar un administrator sau un moderator va trebui să accepte cererea"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Cereți să vă alăturați"</string>
<string name="screen_create_room_room_address_section_footer">"Pentru ca această cameră să fie vizibilă în directorul de camere publice, veți avea nevoie de o adresă de cameră."</string>
<string name="screen_create_room_room_address_section_title">"Adresa camerei"</string>
<string name="screen_create_room_room_name_label">"Numele camerei"</string>
<string name="screen_create_room_room_visibility_section_title">"Vizibilitatea camerei"</string>
<string name="screen_create_room_title">"Creați o cameră"</string>
<string name="screen_create_room_topic_label">"Subiect (opțional)"</string>
</resources>

View File

@@ -8,15 +8,10 @@
<string name="screen_create_room_public_option_description">"Любой желающий может найти эту комнату.
Вы можете изменить это в любое время в настройках комнаты."</string>
<string name="screen_create_room_public_option_title">"Общедоступная комната"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Любой желающий может присоединиться к этой комнате"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Любой"</string>
<string name="screen_create_room_room_access_section_header">"Доступ в комнату"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Любой желающий может подать заявку на присоединение к комнате, но администратор или модератор должен будет принять запрос."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Попросить присоединиться"</string>
<string name="screen_create_room_room_address_section_footer">"Чтобы эта комната была видна в каталоге общедоступных, вам необходим ее адрес"</string>
<string name="screen_create_room_room_address_section_title">"Адрес комнаты"</string>
<string name="screen_create_room_room_name_label">"Название комнаты"</string>
<string name="screen_create_room_room_visibility_section_title">"Видимость комнаты"</string>
<string name="screen_create_room_title">"Создать комнату"</string>
<string name="screen_create_room_topic_label">"Тема (необязательно)"</string>
</resources>

View File

@@ -8,15 +8,10 @@
<string name="screen_create_room_public_option_description">"Túto miestnosť môže nájsť ktokoľvek.
Môžete to kedykoľvek zmeniť v nastaveniach miestnosti."</string>
<string name="screen_create_room_public_option_title">"Verejná miestnosť"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Do tejto miestnosti sa môže pripojiť ktokoľvek"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Ktokoľvek"</string>
<string name="screen_create_room_room_access_section_header">"Prístup do miestnosti"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Ktokoľvek môže požiadať o pripojenie sa k miestnosti, ale administrátor alebo moderátor bude musieť žiadosť schváliť"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Požiadať o pripojenie"</string>
<string name="screen_create_room_room_address_section_footer">"Aby bola táto miestnosť viditeľná v adresári verejných miestností, budete potrebovať adresu miestnosti."</string>
<string name="screen_create_room_room_address_section_title">"Adresa miestnosti"</string>
<string name="screen_create_room_room_name_label">"Názov miestnosti"</string>
<string name="screen_create_room_room_visibility_section_title">"Viditeľnosť miestnosti"</string>
<string name="screen_create_room_title">"Vytvoriť miestnosť"</string>
<string name="screen_create_room_topic_label">"Téma (voliteľné)"</string>
</resources>

View File

@@ -8,15 +8,10 @@
<string name="screen_create_room_public_option_description">"Vem som helst kan hitta det här rummet.
Du kan ändra detta när som helst i rumsinställningarna."</string>
<string name="screen_create_room_public_option_title">"Offentligt rum"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Vem som helst kan gå med i det här rummet"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Vem som helst"</string>
<string name="screen_create_room_room_access_section_header">"Rumsåtkomst"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Vem som helst kan be om att gå med i rummet men en administratör eller en moderator måste acceptera begäran"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Be om att gå med"</string>
<string name="screen_create_room_room_address_section_footer">"För att detta rum ska vara synligt i den allmänna rumskatalogen behöver du en rumsadress."</string>
<string name="screen_create_room_room_address_section_title">"Rumsadress"</string>
<string name="screen_create_room_room_name_label">"Rumsnamn"</string>
<string name="screen_create_room_room_visibility_section_title">"Rumssynlighet"</string>
<string name="screen_create_room_title">"Skapa ett rum"</string>
<string name="screen_create_room_topic_label">"Ämne (valfritt)"</string>
</resources>

View File

@@ -8,15 +8,10 @@
<string name="screen_create_room_public_option_description">"Bu odayı herkes bulabilir.
Bunu istediğiniz zaman oda ayarlarından değiştirebilirsiniz."</string>
<string name="screen_create_room_public_option_title">"Herkese açık oda"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Bu odaya herkes katılabilir"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Herkes"</string>
<string name="screen_create_room_room_access_section_header">"Oda Erişimi"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Herkes odaya katılmayı isteyebilir ancak bir yönetici veya moderatörün isteği kabul etmesi gerekecektir"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Katılmak için sor"</string>
<string name="screen_create_room_room_address_section_footer">"Bu odanın genel oda dizininde görünür olması için bir oda adresine ihtiyacınız olacaktır."</string>
<string name="screen_create_room_room_address_section_title">"Oda adresi"</string>
<string name="screen_create_room_room_name_label">"Oda adı"</string>
<string name="screen_create_room_room_visibility_section_title">"Oda görünürlüğü"</string>
<string name="screen_create_room_title">"Bir oda oluştur"</string>
<string name="screen_create_room_topic_label">"Konu (isteğe bağlı)"</string>
</resources>

View File

@@ -8,15 +8,10 @@
<string name="screen_create_room_public_option_description">"Будь-хто може знайти цю кімнату.
Ви можете змінити це в будь-який час у налаштуваннях кімнати."</string>
<string name="screen_create_room_public_option_title">"Загальнодоступна кімната"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Будь-хто може приєднатися до цієї кімнати"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Кожний"</string>
<string name="screen_create_room_room_access_section_header">"Доступ до кімнати"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Будь-хто може попросити приєднатися до кімнати, але адміністратор або модератор повинен буде прийняти запит"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Запросити приєднатися"</string>
<string name="screen_create_room_room_address_section_footer">"Щоб цю кімнату було видно в каталозі загальнодоступних кімнат, вам знадобиться її адреса."</string>
<string name="screen_create_room_room_address_section_title">"Адреса кімнати"</string>
<string name="screen_create_room_room_name_label">"Назва кімнати"</string>
<string name="screen_create_room_room_visibility_section_title">"Видимість кімнати"</string>
<string name="screen_create_room_title">"Створити кімнату"</string>
<string name="screen_create_room_topic_label">"Тема (необов\'язково)"</string>
</resources>

View File

@@ -8,7 +8,5 @@
<string name="screen_create_room_public_option_description">"کوئی بھی یہ کمرہ ڈھونڈ سکتا ہے۔
آپ اسے کمرے کی ترتیبات میں کسی بھی وقت تبدیل کرسکتے ہیں۔"</string>
<string name="screen_create_room_public_option_title">"عوامی کمرہ"</string>
<string name="screen_create_room_room_name_label">"کمرے کا نام"</string>
<string name="screen_create_room_title">"ایک کمرہ بنائیں"</string>
<string name="screen_create_room_topic_label">"موضوع (اختیاری)"</string>
</resources>

View File

@@ -8,14 +8,9 @@
<string name="screen_create_room_public_option_description">"Bu xonani har kim topishi mumkin.
Buni xona sozlamalaridan istalgan vaqtda oʻzgartirishingiz mumkin."</string>
<string name="screen_create_room_public_option_title">"Jamoat xonasi"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Bu xonaga istalgan kishi qoshilishi mumkin"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Har kim"</string>
<string name="screen_create_room_room_access_section_header">"Xonaga kirish"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Xonaga qoshilishni istalgan kishi sorashi mumkin, lekin administrator yoki moderator sorovni qabul qilishi kerak"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Qoshilishni sorang"</string>
<string name="screen_create_room_room_address_section_footer">"Ushbu xona ommaviy xonalar royxatida korinishi uchun sizga xona manzili kerak boladi."</string>
<string name="screen_create_room_room_name_label">"Xona nomi"</string>
<string name="screen_create_room_room_visibility_section_title">"Xonaning korinishi"</string>
<string name="screen_create_room_title">"Xonani yaratish"</string>
<string name="screen_create_room_topic_label">"Mavzu (ixtiyoriy)"</string>
</resources>

View File

@@ -8,15 +8,10 @@
<string name="screen_create_room_public_option_description">"任何人都可以找到此聊天室。
您隨時都可以在聊天室設定中變更此設定。"</string>
<string name="screen_create_room_public_option_title">"公開的聊天室"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"任何人都可以加入此聊天室"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"任何人"</string>
<string name="screen_create_room_room_access_section_header">"聊天室存取權"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"任何人都可以要求加入聊天室,但管理員或版主必須接受該請求"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"要求加入"</string>
<string name="screen_create_room_room_address_section_footer">"為了讓此聊天室在公開聊天室目錄中可見,您需要聊天室地址。"</string>
<string name="screen_create_room_room_address_section_title">"聊天室地址"</string>
<string name="screen_create_room_room_name_label">"聊天室名稱"</string>
<string name="screen_create_room_room_visibility_section_title">"聊天室能見度"</string>
<string name="screen_create_room_title">"建立聊天室"</string>
<string name="screen_create_room_topic_label">"主題(非必填)"</string>
</resources>

View File

@@ -8,15 +8,10 @@
<string name="screen_create_room_public_option_description">"任何人都能找到此聊天室。
你可以随时在聊天室设置中更改。"</string>
<string name="screen_create_room_public_option_title">"公共聊天室"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"任何人都可以加入此房间"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"任何人"</string>
<string name="screen_create_room_room_access_section_header">"房间访问权限"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"任何人都可以请求加入房间,但必须由管理员或审核人接受"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"请求加入"</string>
<string name="screen_create_room_room_address_section_footer">"要使该房间在公开房间目录中可见,您需要一个房间地址。"</string>
<string name="screen_create_room_room_address_section_title">"房间地址"</string>
<string name="screen_create_room_room_name_label">"聊天室名称"</string>
<string name="screen_create_room_room_visibility_section_title">"房间可见性"</string>
<string name="screen_create_room_title">"创建聊天室"</string>
<string name="screen_create_room_topic_label">"主题(可选)"</string>
</resources>

View File

@@ -3,20 +3,26 @@
<string name="screen_create_room_action_create_room">"New room"</string>
<string name="screen_create_room_add_people_title">"Invite people"</string>
<string name="screen_create_room_error_creating_room">"An error occurred when creating the room"</string>
<string name="screen_create_room_private_option_description">"Only people invited can access this room. All messages are end-to-end encrypted."</string>
<string name="screen_create_room_private_option_title">"Private room"</string>
<string name="screen_create_room_error_creating_space">"The space could not be created because of an unknown error. Try again later."</string>
<string name="screen_create_room_name_placeholder">"Add name…"</string>
<string name="screen_create_room_new_room_title">"New room"</string>
<string name="screen_create_room_new_space_title">"New space"</string>
<string name="screen_create_room_private_option_description">"Only people invited can join."</string>
<string name="screen_create_room_private_option_title">"Private"</string>
<string name="screen_create_room_public_option_description">"Anyone can find this room.
You can change this anytime in room settings."</string>
<string name="screen_create_room_public_option_title">"Public room"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Anyone can join this room"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Anyone"</string>
<string name="screen_create_room_room_access_section_header">"Room Access"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Anyone can ask to join the room but an administrator or a moderator will have to accept the request"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Ask to join"</string>
<string name="screen_create_room_room_address_section_footer">"In order for this room to be visible in the public room directory, you will need a room address."</string>
<string name="screen_create_room_room_address_section_title">"Room address"</string>
<string name="screen_create_room_room_name_label">"Room name"</string>
<string name="screen_create_room_public_option_short_description">"Anyone can join."</string>
<string name="screen_create_room_public_option_title">"Public"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Anyone can ask to join but an administrator or a moderator must accept the request."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Allow ask to join"</string>
<string name="screen_create_room_room_access_section_private_option_description">"Only people invited can join."</string>
<string name="screen_create_room_room_access_section_private_option_title">"Private"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Anyone can join."</string>
<string name="screen_create_room_room_access_section_public_option_title">"Public"</string>
<string name="screen_create_room_room_access_section_title">"Who has access"</string>
<string name="screen_create_room_room_address_section_footer">"Youll need an address in order to make it visible in the public directory."</string>
<string name="screen_create_room_room_address_section_title">"Address"</string>
<string name="screen_create_room_room_visibility_section_title">"Room visibility"</string>
<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">"Add description…"</string>
</resources>

View File

@@ -40,6 +40,7 @@ class DefaultCreateRoomEntryPointTest {
override fun onRoomCreated(roomId: RoomId) = lambdaError()
}
val result = entryPoint.createNode(
isSpace = false,
parentNode = parentNode,
buildContext = BuildContext.root(null),
callback = callback,

View File

@@ -380,6 +380,7 @@ class ConfigureRoomPresenterTest {
)
private fun createConfigureRoomPresenter(
isSpace: Boolean = false,
roomAliasHelper: RoomAliasHelper = FakeRoomAliasHelper(),
dataStore: CreateRoomConfigStore = CreateRoomConfigStore(roomAliasHelper),
matrixClient: MatrixClient = createMatrixClient(),
@@ -390,6 +391,7 @@ class ConfigureRoomPresenterTest {
isKnockFeatureEnabled: Boolean = true,
mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(),
) = ConfigureRoomPresenter(
isSpace = isSpace,
dataStore = dataStore,
matrixClient = matrixClient,
mediaPickerProvider = pickerProvider,

View File

@@ -14,6 +14,7 @@ import io.element.android.tests.testutils.lambda.lambdaError
class FakeCreateRoomEntryPoint : CreateRoomEntryPoint {
override fun createNode(
isSpace: Boolean,
parentNode: Node,
buildContext: BuildContext,
callback: CreateRoomEntryPoint.Callback,

View File

@@ -25,6 +25,7 @@ interface HomeEntryPoint : FeatureEntryPoint {
interface Callback : Plugin {
fun navigateToRoom(roomId: RoomId, joinedRoom: JoinedRoom?)
fun navigateToCreateRoom()
fun navigateToCreateSpace()
fun navigateToSettings()
fun navigateToSetUpRecovery()
fun navigateToEnterRecoveryKey()

View File

@@ -220,6 +220,7 @@ class HomeFlowNode(
onRoomClick = ::navigateToRoom,
onSettingsClick = callback::navigateToSettings,
onStartChatClick = callback::navigateToCreateRoom,
onCreateSpaceClick = callback::navigateToCreateSpace,
onSetUpRecoveryClick = callback::navigateToSetUpRecovery,
onConfirmRecoveryKeyClick = callback::navigateToEnterRecoveryKey,
onRoomSettingsClick = callback::navigateToRoomSettings,

View File

@@ -74,6 +74,7 @@ fun HomeView(
onSetUpRecoveryClick: () -> Unit,
onConfirmRecoveryKeyClick: () -> Unit,
onStartChatClick: () -> Unit,
onCreateSpaceClick: () -> Unit,
onRoomSettingsClick: (roomId: RoomId) -> Unit,
onMenuActionClick: (RoomListMenuAction) -> Unit,
onReportRoomClick: (roomId: RoomId) -> Unit,
@@ -113,6 +114,7 @@ fun HomeView(
onRoomClick = { if (firstThrottler.canHandle()) onRoomClick(it) },
onOpenSettings = { if (firstThrottler.canHandle()) onSettingsClick() },
onStartChatClick = { if (firstThrottler.canHandle()) onStartChatClick() },
onCreateSpaceClick = { if (firstThrottler.canHandle()) onCreateSpaceClick() },
onMenuActionClick = onMenuActionClick,
)
// This overlaid view will only be visible when state.displaySearchResults is true
@@ -138,6 +140,7 @@ private fun HomeScaffold(
onRoomClick: (RoomId) -> Unit,
onOpenSettings: () -> Unit,
onStartChatClick: () -> Unit,
onCreateSpaceClick: () -> Unit,
onMenuActionClick: (RoomListMenuAction) -> Unit,
modifier: Modifier = Modifier,
) {
@@ -164,6 +167,7 @@ private fun HomeScaffold(
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
HomeTopBar(
selectedNavigationItem = state.currentHomeNavigationBarItem,
title = stringResource(state.currentHomeNavigationBarItem.labelRes),
currentUserAndNeighbors = state.currentUserAndNeighbors,
showAvatarIndicator = state.showAvatarIndicator,
@@ -174,10 +178,11 @@ private fun HomeScaffold(
onAccountSwitch = {
state.eventSink(HomeEvents.SwitchToAccount(it))
},
onCreateSpace = onCreateSpaceClick,
scrollBehavior = scrollBehavior,
displayMenuItems = state.displayActions,
displayFilters = state.displayRoomListFilters,
filtersState = roomListState.filtersState,
canCreateSpaces = state.homeSpacesState.canCreateSpaces,
canReportBug = state.canReportBug,
modifier = Modifier.hazeEffect(
state = hazeState,
@@ -328,6 +333,7 @@ internal fun HomeViewPreview(@PreviewParameter(HomeStateProvider::class) state:
onSetUpRecoveryClick = {},
onConfirmRecoveryKeyClick = {},
onStartChatClick = {},
onCreateSpaceClick = {},
onRoomSettingsClick = {},
onReportRoomClick = {},
onMenuActionClick = {},
@@ -347,6 +353,7 @@ internal fun HomeViewA11yPreview() = ElementPreview {
onSetUpRecoveryClick = {},
onConfirmRecoveryKeyClick = {},
onStartChatClick = {},
onCreateSpaceClick = {},
onRoomSettingsClick = {},
onReportRoomClick = {},
onMenuActionClick = {},

View File

@@ -39,6 +39,7 @@ import androidx.compose.ui.unit.dp
import io.element.android.appconfig.RoomListConfig
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.home.impl.HomeNavigationBarItem
import io.element.android.features.home.impl.R
import io.element.android.features.home.impl.filters.RoomListFiltersState
import io.element.android.features.home.impl.filters.RoomListFiltersView
@@ -73,6 +74,7 @@ import kotlinx.collections.immutable.toImmutableList
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeTopBar(
selectedNavigationItem: HomeNavigationBarItem,
title: String,
currentUserAndNeighbors: ImmutableList<MatrixUser>,
showAvatarIndicator: Boolean,
@@ -81,8 +83,9 @@ fun HomeTopBar(
onMenuActionClick: (RoomListMenuAction) -> Unit,
onOpenSettings: () -> Unit,
onAccountSwitch: (SessionId) -> Unit,
onCreateSpace: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior,
displayMenuItems: Boolean,
canCreateSpaces: Boolean,
canReportBug: Boolean,
displayFilters: Boolean,
filtersState: RoomListFiltersState,
@@ -117,63 +120,16 @@ fun HomeTopBar(
)
},
actions = {
if (displayMenuItems) {
IconButton(
onClick = onToggleSearch,
) {
Icon(
imageVector = CompoundIcons.Search(),
contentDescription = stringResource(CommonStrings.action_search),
)
}
if (RoomListConfig.HAS_DROP_DOWN_MENU) {
var showMenu by remember { mutableStateOf(false) }
IconButton(
onClick = { showMenu = !showMenu }
) {
Icon(
imageVector = CompoundIcons.OverflowVertical(),
contentDescription = null,
)
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
if (RoomListConfig.SHOW_INVITE_MENU_ITEM) {
DropdownMenuItem(
onClick = {
showMenu = false
onMenuActionClick(RoomListMenuAction.InviteFriends)
},
text = { Text(stringResource(id = CommonStrings.action_invite)) },
leadingIcon = {
Icon(
imageVector = CompoundIcons.ShareAndroid(),
tint = ElementTheme.colors.iconSecondary,
contentDescription = null,
)
}
)
}
if (RoomListConfig.SHOW_REPORT_PROBLEM_MENU_ITEM && canReportBug) {
DropdownMenuItem(
onClick = {
showMenu = false
onMenuActionClick(RoomListMenuAction.ReportBug)
},
text = { Text(stringResource(id = CommonStrings.common_report_a_problem)) },
leadingIcon = {
Icon(
imageVector = CompoundIcons.ChatProblem(),
tint = ElementTheme.colors.iconSecondary,
contentDescription = null,
)
}
)
}
}
}
when (selectedNavigationItem) {
HomeNavigationBarItem.Chats -> RoomListMenuItems(
onToggleSearch = onToggleSearch,
onMenuActionClick = onMenuActionClick,
canReportBug = canReportBug
)
HomeNavigationBarItem.Spaces -> SpacesMenuItems(
canCreateSpaces = canCreateSpaces,
onCreateSpace = onCreateSpace
)
}
},
// We want a 16dp left padding for the navigationIcon :
@@ -193,6 +149,85 @@ fun HomeTopBar(
}
}
@Composable
private fun RoomListMenuItems(
onToggleSearch: () -> Unit,
onMenuActionClick: (RoomListMenuAction) -> Unit,
canReportBug: Boolean,
) {
IconButton(
onClick = onToggleSearch,
) {
Icon(
imageVector = CompoundIcons.Search(),
contentDescription = stringResource(CommonStrings.action_search),
)
}
if (RoomListConfig.HAS_DROP_DOWN_MENU) {
var showMenu by remember { mutableStateOf(false) }
IconButton(
onClick = { showMenu = !showMenu }
) {
Icon(
imageVector = CompoundIcons.OverflowVertical(),
contentDescription = null,
)
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
if (RoomListConfig.SHOW_INVITE_MENU_ITEM) {
DropdownMenuItem(
onClick = {
showMenu = false
onMenuActionClick(RoomListMenuAction.InviteFriends)
},
text = { Text(stringResource(id = CommonStrings.action_invite)) },
leadingIcon = {
Icon(
imageVector = CompoundIcons.ShareAndroid(),
tint = ElementTheme.colors.iconSecondary,
contentDescription = null,
)
}
)
}
if (RoomListConfig.SHOW_REPORT_PROBLEM_MENU_ITEM && canReportBug) {
DropdownMenuItem(
onClick = {
showMenu = false
onMenuActionClick(RoomListMenuAction.ReportBug)
},
text = { Text(stringResource(id = CommonStrings.common_report_a_problem)) },
leadingIcon = {
Icon(
imageVector = CompoundIcons.ChatProblem(),
tint = ElementTheme.colors.iconSecondary,
contentDescription = null,
)
}
)
}
}
}
}
@Composable
private fun SpacesMenuItems(
canCreateSpaces: Boolean,
onCreateSpace: () -> Unit
) {
if (canCreateSpaces) {
IconButton(onClick = onCreateSpace) {
Icon(
imageVector = CompoundIcons.Plus(),
contentDescription = stringResource(CommonStrings.action_create_space)
)
}
}
}
@Composable
private fun NavigationIcon(
currentUserAndNeighbors: ImmutableList<MatrixUser>,
@@ -273,6 +308,7 @@ private fun AccountIcon(
@Composable
internal fun HomeTopBarPreview() = ElementPreview {
HomeTopBar(
selectedNavigationItem = HomeNavigationBarItem.Chats,
title = stringResource(R.string.screen_roomlist_main_space_title),
currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")),
showAvatarIndicator = false,
@@ -281,7 +317,8 @@ internal fun HomeTopBarPreview() = ElementPreview {
onOpenSettings = {},
onAccountSwitch = {},
onToggleSearch = {},
displayMenuItems = true,
onCreateSpace = {},
canCreateSpaces = true,
canReportBug = true,
displayFilters = true,
filtersState = aRoomListFiltersState(),
@@ -289,11 +326,35 @@ internal fun HomeTopBarPreview() = ElementPreview {
)
}
@OptIn(ExperimentalMaterial3Api::class)
@PreviewsDayNight
@Composable
internal fun HomeTopBarSpacesPreview() = ElementPreview {
HomeTopBar(
selectedNavigationItem = HomeNavigationBarItem.Spaces,
title = stringResource(R.string.screen_home_tab_spaces),
currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")),
showAvatarIndicator = false,
areSearchResultsDisplayed = false,
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()),
onOpenSettings = {},
onAccountSwitch = {},
onToggleSearch = {},
onCreateSpace = {},
canCreateSpaces = true,
canReportBug = true,
displayFilters = false,
filtersState = aRoomListFiltersState(),
onMenuActionClick = {},
)
}
@OptIn(ExperimentalMaterial3Api::class)
@PreviewsDayNight
@Composable
internal fun HomeTopBarWithIndicatorPreview() = ElementPreview {
HomeTopBar(
selectedNavigationItem = HomeNavigationBarItem.Chats,
title = stringResource(R.string.screen_roomlist_main_space_title),
currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")),
showAvatarIndicator = true,
@@ -302,7 +363,8 @@ internal fun HomeTopBarWithIndicatorPreview() = ElementPreview {
onOpenSettings = {},
onAccountSwitch = {},
onToggleSearch = {},
displayMenuItems = true,
onCreateSpace = {},
canCreateSpaces = true,
canReportBug = true,
displayFilters = true,
filtersState = aRoomListFiltersState(),
@@ -315,6 +377,7 @@ internal fun HomeTopBarWithIndicatorPreview() = ElementPreview {
@Composable
internal fun HomeTopBarMultiAccountPreview() = ElementPreview {
HomeTopBar(
selectedNavigationItem = HomeNavigationBarItem.Chats,
title = stringResource(R.string.screen_roomlist_main_space_title),
currentUserAndNeighbors = aMatrixUserList().take(3).toImmutableList(),
showAvatarIndicator = false,
@@ -323,7 +386,8 @@ internal fun HomeTopBarMultiAccountPreview() = ElementPreview {
onOpenSettings = {},
onAccountSwitch = {},
onToggleSearch = {},
displayMenuItems = true,
onCreateSpace = {},
canCreateSpaces = true,
canReportBug = true,
displayFilters = true,
filtersState = aRoomListFiltersState(),

View File

@@ -15,6 +15,8 @@ import androidx.compose.runtime.remember
import dev.zacsweers.metro.Inject
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar
import kotlinx.collections.immutable.persistentListOf
@@ -27,9 +29,11 @@ import kotlinx.coroutines.flow.map
class HomeSpacesPresenter(
private val client: MatrixClient,
private val seenInvitesStore: SeenInvitesStore,
private val featureFlagsService: FeatureFlagService,
) : Presenter<HomeSpacesState> {
@Composable
override fun present(): HomeSpacesState {
val canCreateSpaces by featureFlagsService.isFeatureEnabledFlow(FeatureFlags.CreateSpaces).collectAsState(false)
val hideInvitesAvatar by client.rememberHideInvitesAvatar()
val spaceRooms by remember {
client.spaceService.spaceRoomsFlow.map { it.toImmutableList() }
@@ -48,6 +52,7 @@ class HomeSpacesPresenter(
spaceRooms = spaceRooms,
seenSpaceInvites = seenSpaceInvites,
hideInvitesAvatar = hideInvitesAvatar,
canCreateSpaces = canCreateSpaces,
eventSink = ::handleEvent,
)
}

View File

@@ -18,6 +18,7 @@ data class HomeSpacesState(
val spaceRooms: ImmutableList<SpaceRoom>,
val seenSpaceInvites: ImmutableSet<RoomId>,
val hideInvitesAvatar: Boolean,
val canCreateSpaces: Boolean,
val eventSink: (HomeSpacesEvents) -> Unit,
)

View File

@@ -30,6 +30,13 @@ open class HomeSpacesStateProvider : PreviewParameterProvider<HomeSpacesState> {
),
spaceRooms = aListOfSpaceRooms(),
),
aHomeSpacesState(
space = CurrentSpace.Space(
spaceRoom = aSpaceRoom(roomId = RoomId("!mySpace:example.com"))
),
spaceRooms = aListOfSpaceRooms(),
canCreateSpaces = false,
),
)
}
@@ -38,12 +45,14 @@ internal fun aHomeSpacesState(
spaceRooms: List<SpaceRoom> = aListOfSpaceRooms(),
seenSpaceInvites: Set<RoomId> = emptySet(),
hideInvitesAvatar: Boolean = false,
canCreateSpaces: Boolean = true,
eventSink: (HomeSpacesEvents) -> Unit = {},
) = HomeSpacesState(
space = space,
spaceRooms = spaceRooms.toImmutableList(),
seenSpaceInvites = seenSpaceInvites.toImmutableSet(),
hideInvitesAvatar = hideInvitesAvatar,
canCreateSpaces = canCreateSpaces,
eventSink = eventSink,
)

View File

@@ -47,6 +47,7 @@ class DefaultHomeEntryPointTest {
val callback = object : HomeEntryPoint.Callback {
override fun navigateToRoom(roomId: RoomId, joinedRoom: JoinedRoom?) = lambdaError()
override fun navigateToCreateRoom() = lambdaError()
override fun navigateToCreateSpace() = lambdaError()
override fun navigateToSettings() = lambdaError()
override fun navigateToSetUpRecovery() = lambdaError()
override fun navigateToEnterRecoveryKey() = lambdaError()

View File

@@ -273,6 +273,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL
onSetUpRecoveryClick: () -> Unit = EnsureNeverCalled(),
onConfirmRecoveryKeyClick: () -> Unit = EnsureNeverCalled(),
onCreateRoomClick: () -> Unit = EnsureNeverCalled(),
onCreateSpaceClick: () -> Unit = EnsureNeverCalled(),
onRoomSettingsClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
onMenuActionClick: (RoomListMenuAction) -> Unit = EnsureNeverCalledWithParam(),
onReportRoomClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
@@ -286,6 +287,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL
onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onStartChatClick = onCreateRoomClick,
onCreateSpaceClick = onCreateSpaceClick,
onRoomSettingsClick = onRoomSettingsClick,
onMenuActionClick = onMenuActionClick,
onDeclineInviteAndBlockUser = onDeclineInviteAndBlockUser,

View File

@@ -11,6 +11,9 @@ package io.element.android.features.home.impl.spaces
import com.google.common.truth.Truth.assertThat
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.test.InMemorySeenInvitesStore
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.tests.testutils.test
@@ -23,18 +26,25 @@ class HomeSpacesPresenterTest {
val presenter = createPresenter()
presenter.test {
val state = awaitItem()
// canCreateSpaces is initially false
assertThat(state.canCreateSpaces).isFalse()
assertThat(state.space).isEqualTo(CurrentSpace.Root)
assertThat(state.spaceRooms).isEmpty()
assertThat(state.hideInvitesAvatar).isFalse()
assertThat(state.seenSpaceInvites).isEmpty()
// It'll eventually be true
assertThat(awaitItem().canCreateSpaces).isTrue()
}
}
private fun createPresenter(
client: MatrixClient = FakeMatrixClient(),
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
featureFlagsService: FeatureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.CreateSpaces.key to true)),
) = HomeSpacesPresenter(
client = client,
seenInvitesStore = seenInvitesStore,
featureFlagsService = featureFlagsService,
)
}

View File

@@ -34,6 +34,7 @@ import io.element.android.features.preferences.impl.R
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.components.button.BackButton
@@ -47,7 +48,8 @@ import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet
import io.element.android.libraries.matrix.ui.components.EditableAvatarView
import io.element.android.libraries.matrix.ui.components.AvatarPickerState
import io.element.android.libraries.matrix.ui.components.AvatarPickerView
import io.element.android.libraries.permissions.api.PermissionsView
import io.element.android.libraries.ui.strings.CommonStrings
@@ -103,13 +105,17 @@ fun EditUserProfileView(
.verticalScroll(rememberScrollState())
) {
Spacer(modifier = Modifier.height(24.dp))
EditableAvatarView(
matrixId = state.userId.value,
displayName = state.displayName,
avatarUrl = state.userAvatarUrl,
avatarSize = AvatarSize.EditProfileDetails,
avatarType = AvatarType.User,
onAvatarClick = { onAvatarClick() },
val avatarPickerState = remember(state.userAvatarUrl) {
val size = AvatarSize.EditProfileDetails
val type = AvatarType.User
AvatarPickerState.Selected(
avatarData = AvatarData(id = state.userId.value, name = state.displayName, size = size, url = state.userAvatarUrl),
type = type
)
}
AvatarPickerView(
state = avatarPickerState,
onClick = ::onAvatarClick,
modifier = Modifier.align(Alignment.CenterHorizontally),
)
Spacer(modifier = Modifier.height(16.dp))

View File

@@ -63,7 +63,7 @@
<string name="screen_room_details_profile_row_title">"Profile"</string>
<string name="screen_room_details_requests_to_join_title">"Requests to join"</string>
<string name="screen_room_details_roles_and_permissions">"Roles &amp; permissions"</string>
<string name="screen_room_details_room_name_label">"Room name"</string>
<string name="screen_room_details_room_name_label">"Name"</string>
<string name="screen_room_details_security_and_privacy_title">"Security &amp; privacy"</string>
<string name="screen_room_details_security_title">"Security"</string>
<string name="screen_room_details_share_room_title">"Share room"</string>
@@ -143,7 +143,7 @@ We do not recommend enabling encryption for rooms that anyone can find and join.
<string name="screen_security_and_privacy_encryption_toggle_title">"Enable end-to-end encryption"</string>
<string name="screen_security_and_privacy_room_access_anyone_option_description">"Anyone can join."</string>
<string name="screen_security_and_privacy_room_access_anyone_option_title">"Anyone"</string>
<string name="screen_security_and_privacy_room_access_footer">"Choose which spaces members can join this room without an invitation. %1$s"</string>
<string name="screen_security_and_privacy_room_access_footer">"Choose which spaces members can join this room without an invitation. %1$s"</string>
<string name="screen_security_and_privacy_room_access_footer_manage_spaces_action">"Manage spaces"</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Only invited people can join."</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_title">"Invite only"</string>

View File

@@ -13,7 +13,6 @@ package io.element.android.features.roomdetailsedit.impl
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
@@ -24,6 +23,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
@@ -33,6 +33,7 @@ import androidx.compose.ui.unit.dp
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.components.button.BackButton
@@ -45,7 +46,8 @@ import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet
import io.element.android.libraries.matrix.ui.components.EditableAvatarView
import io.element.android.libraries.matrix.ui.components.AvatarPickerState
import io.element.android.libraries.matrix.ui.components.AvatarPickerView
import io.element.android.libraries.permissions.api.PermissionsView
import io.element.android.libraries.ui.strings.CommonStrings
@@ -99,20 +101,18 @@ fun RoomDetailsEditView(
.verticalScroll(rememberScrollState())
) {
Spacer(modifier = Modifier.height(24.dp))
EditableAvatarView(
matrixId = state.roomId.value,
// As per Element Web, we use the raw name for the avatar as well
displayName = state.roomRawName,
avatarUrl = state.roomAvatarUrl,
avatarSize = AvatarSize.EditRoomDetails,
avatarType = if (state.isSpace) {
AvatarType.Space()
} else {
AvatarType.Room()
},
enabled = state.canChangeAvatar,
onAvatarClick = ::onAvatarClick,
modifier = Modifier.fillMaxWidth(),
val avatarPickerState = remember(state.roomAvatarUrl) {
val size = AvatarSize.EditRoomDetails
val type = AvatarType.Room()
AvatarPickerState.Selected(
avatarData = AvatarData(id = state.roomId.value, name = state.roomRawName, size = size, url = state.roomAvatarUrl),
type = type
)
}
AvatarPickerView(
state = avatarPickerState,
onClick = ::onAvatarClick,
modifier = Modifier.align(Alignment.CenterHorizontally),
)
Spacer(modifier = Modifier.height(32.dp))

View File

@@ -22,7 +22,7 @@ We do not recommend enabling encryption for rooms that anyone can find and join.
<string name="screen_security_and_privacy_encryption_toggle_title">"Enable end-to-end encryption"</string>
<string name="screen_security_and_privacy_room_access_anyone_option_description">"Anyone can join."</string>
<string name="screen_security_and_privacy_room_access_anyone_option_title">"Anyone"</string>
<string name="screen_security_and_privacy_room_access_footer">"Choose which spaces members can join this room without an invitation. %1$s"</string>
<string name="screen_security_and_privacy_room_access_footer">"Choose which spaces members can join this room without an invitation. %1$s"</string>
<string name="screen_security_and_privacy_room_access_footer_manage_spaces_action">"Manage spaces"</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Only invited people can join."</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_title">"Invite only"</string>

View File

@@ -81,6 +81,7 @@ class StartChatFlowNode(
}
}
createRoomEntryPoint.createNode(
isSpace = false,
parentNode = this,
buildContext = buildContext,
callback = callback,

View File

@@ -46,7 +46,7 @@ enum class AvatarSize(val dp: Dp) {
RoomInviteItem(52.dp),
InviteSender(16.dp),
EditRoomDetails(70.dp),
EditRoomDetails(68.dp),
RoomListManageUser(96.dp),
NotificationsOptIn(32.dp),

View File

@@ -70,6 +70,13 @@ enum class FeatureFlags(
defaultValue = { false },
isFinished = false,
),
CreateSpaces(
key = "feature.createSpaces",
title = "Create spaces",
description = "Allow creating spaces.",
defaultValue = { false },
isFinished = false,
),
SpaceSettings(
key = "feature.spaceSettings",
title = "Space settings",

View File

@@ -26,4 +26,5 @@ data class CreateRoomParameters(
val joinRuleOverride: JoinRule? = null,
val historyVisibilityOverride: RoomHistoryVisibility? = null,
val roomAliasName: Optional<String> = Optional.empty(),
val isSpace: Boolean = false,
)

View File

@@ -393,6 +393,7 @@ class RustMatrixClient(
joinRuleOverride = createRoomParams.joinRuleOverride?.map(),
historyVisibilityOverride = createRoomParams.historyVisibilityOverride?.map(),
canonicalAlias = createRoomParams.roomAliasName.getOrNull(),
isSpace = createRoomParams.isSpace,
)
val roomId = RoomId(innerClient.createRoom(rustParams))
// Wait to receive the room back from the sync but do not returns failure if it fails.

View File

@@ -11,6 +11,7 @@ package io.element.android.libraries.matrix.ui.media
import coil3.ImageLoader
import coil3.fetch.Fetcher
import coil3.request.Options
import coil3.toUri
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
@@ -21,10 +22,19 @@ internal class AvatarDataFetcherFactory(
data: AvatarData,
options: Options,
imageLoader: ImageLoader
): Fetcher {
return CoilMediaFetcher(
mediaLoader = matrixMediaLoader,
mediaData = data.toMediaRequestData(),
)
): Fetcher? {
return when {
data.url == null -> null
data.url?.startsWith("mxc") == true -> CoilMediaFetcher(
mediaLoader = matrixMediaLoader,
mediaData = data.toMediaRequestData(),
)
else -> {
// If the URL does not use the mxc scheme, it might be a local one using `content://`, try using a fallback fetcher
data.url?.toUri()?.let { uri ->
imageLoader.components.newFetcher(uri, options, imageLoader)
}?.first
}
}
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.ui.media
import android.graphics.Bitmap
import coil3.ComponentRegistry
import coil3.ImageLoader
import coil3.asImage
import coil3.disk.DiskCache
import coil3.memory.MemoryCache
import coil3.request.Disposable
import coil3.request.ImageRequest
import coil3.request.ImageResult
import coil3.request.Options
import coil3.request.SuccessResult
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.designsystem.components.avatar.anAvatarData
import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader
import io.mockk.mockk
import org.junit.Test
class AvatarDataFetcherFactoryTest {
@Test
fun `create - with mxc returns CoilMediaFetcher`() {
val factory = AvatarDataFetcherFactory(matrixMediaLoader = FakeMatrixMediaLoader())
val fetcher = factory.create(anAvatarData(url = "mxc://test"), Options(mockk()), imageLoader = FakeImageLoader())
assertThat(fetcher).isInstanceOf(CoilMediaFetcher::class.java)
}
@Test
fun `create - with http or https returns null, which means fallback default fetcher will be used`() {
val factory = AvatarDataFetcherFactory(matrixMediaLoader = FakeMatrixMediaLoader())
val fetcherHttp = factory.create(anAvatarData(url = "http://test"), Options(mockk()), imageLoader = FakeImageLoader())
assertThat(fetcherHttp).isNull()
val fetcherHttps = factory.create(anAvatarData(url = "https://test"), Options(mockk()), imageLoader = FakeImageLoader())
assertThat(fetcherHttps).isNull()
}
@Test
fun `create - with content scheme returns null, which means fallback default fetcher will be used`() {
val factory = AvatarDataFetcherFactory(matrixMediaLoader = FakeMatrixMediaLoader())
val fetcher = factory.create(anAvatarData(url = "content://test"), Options(mockk()), imageLoader = FakeImageLoader())
assertThat(fetcher).isNull()
}
}
private class FakeImageLoader : ImageLoader {
override val defaults: ImageRequest.Defaults = ImageRequest.Defaults.DEFAULT
override val components: ComponentRegistry = ComponentRegistry.Builder().build()
override val memoryCache: MemoryCache? = null
override val diskCache: DiskCache? = null
override fun enqueue(request: ImageRequest): Disposable {
return mockk()
}
override suspend fun execute(request: ImageRequest): ImageResult {
return SuccessResult(
image = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8).asImage(),
request = request,
)
}
override fun shutdown() {}
override fun newBuilder(): ImageLoader.Builder {
return ImageLoader.Builder(mockk())
}
}

View File

@@ -0,0 +1,436 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.ui.components
import androidx.annotation.DrawableRes
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.icons.CompoundDrawables
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings
/**
* Avatar picker view, based on https://www.figma.com/design/kcnHxunG1LDWXsJhaNuiHz/ER-145--Spaces-on-Element-X?node-id=5918-97417&t=JYDQysgjS33AZb74-4
*
* It takes a [state], which can be [AvatarPickerState.Pick] for displaying the 'pick avatar' button, or [AvatarPickerState.Selected] when an avatar has
* already been selected.
*
* Note: this function contains lots of 'magic numbers', but those are just the fractions used to scale the different dimensions based on the Figma design.
*/
@Composable
fun AvatarPickerView(
state: AvatarPickerState,
modifier: Modifier = Modifier,
onClick: (() -> Unit) = {},
onClickLabel: String? = stringResource(CommonStrings.a11y_edit_avatar),
enabled: Boolean = true,
) {
val a11yAvatar = stringResource(CommonStrings.a11y_avatar)
val clickableModifier = Modifier.clickable(
enabled = enabled,
interactionSource = remember { MutableInteractionSource() },
onClickLabel = onClickLabel,
onClick = onClick,
indication = ripple(bounded = false),
)
.testTag(TestTags.editAvatar)
.clearAndSetSemantics {
contentDescription = a11yAvatar
}
val layoutDirection = LocalLayoutDirection.current
fun eraseBackgroundModifier(
parentWidth: Dp,
editIconRadius: Dp,
) = Modifier
.graphicsLayer {
compositingStrategy = CompositingStrategy.Offscreen
}
.drawWithContent {
drawContent()
drawCircle(
color = Color.Black,
center = Offset(
x = if (layoutDirection == LayoutDirection.Ltr) {
parentWidth.toPx() - editIconRadius.toPx() * 0.48f
} else {
editIconRadius.toPx() * 0.48f
},
y = size.height - editIconRadius.toPx(),
),
radius = editIconRadius.toPx() * 1.2f,
blendMode = BlendMode.Clear,
)
}
when (state) {
is AvatarPickerState.Pick -> {
PickButton(
buttonSize = state.buttonSize,
iconSize = state.iconSize,
iconId = state.iconId,
modifier = modifier.padding(state.externalPadding).then(clickableModifier),
)
}
is AvatarPickerState.Selected -> {
Box(modifier = modifier) {
Avatar(
avatarData = state.avatarData,
avatarType = state.type,
modifier = clickableModifier.then(eraseBackgroundModifier(state.avatarData.size.dp, state.avatarData.size.dp * 0.225f)),
)
OverlayEditButton(editButtonSize = state.avatarData.size.dp * 0.44f)
}
}
}
}
@Composable
private fun PickButton(
buttonSize: Dp,
iconSize: Dp,
@DrawableRes iconId: Int,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier
.size(buttonSize)
.clip(CircleShape)
.border(BorderStroke(1.dp, ElementTheme.colors.borderInteractiveSecondary), shape = CircleShape)
) {
Icon(
resourceId = iconId,
contentDescription = null,
modifier = Modifier
.align(Alignment.Center)
.size(iconSize),
tint = ElementTheme.colors.iconPrimary,
)
}
}
@Composable
private fun BoxScope.OverlayEditButton(editButtonSize: Dp) {
Box(
modifier = Modifier.align(Alignment.BottomEnd)
.size(editButtonSize)
.offset(x = editButtonSize * 0.266f)
.clip(CircleShape)
.background(ElementTheme.colors.bgCanvasDefault)
.border(BorderStroke(1.dp, ElementTheme.colors.borderInteractiveSecondary), shape = CircleShape),
contentAlignment = Alignment.Center,
) {
Icon(
modifier = Modifier.size(editButtonSize * 0.66f),
imageVector = CompoundIcons.Edit(),
contentDescription = null,
)
}
}
@Immutable
sealed interface AvatarPickerState {
data class Pick(
val buttonSize: Dp,
val iconSize: Dp = buttonSize / 2,
val externalPadding: PaddingValues = PaddingValues.Zero,
@DrawableRes val iconId: Int = CompoundDrawables.ic_compound_take_photo,
) : AvatarPickerState
data class Selected(
val avatarData: AvatarData,
val type: AvatarType,
) : AvatarPickerState
}
@PreviewsDayNight
@Composable
internal fun AvatarPickerViewPreview() = ElementPreview {
PreviewContent()
}
@PreviewsDayNight
@Composable
internal fun AvatarPickerViewRtlPreview() = CompositionLocalProvider(
LocalLayoutDirection provides LayoutDirection.Rtl,
) {
ElementPreview { PreviewContent() }
}
@PreviewsDayNight
@Composable
internal fun AvatarPickerSizesPreview() = ElementPreview {
Column {
Row {
AvatarPickerView(AvatarPickerState.Pick(buttonSize = 24.dp, externalPadding = PaddingValues(6.dp)), onClick = {})
AvatarPickerView(AvatarPickerState.Pick(buttonSize = 32.dp, externalPadding = PaddingValues(6.dp)), onClick = {})
AvatarPickerView(AvatarPickerState.Pick(buttonSize = 48.dp, externalPadding = PaddingValues(6.dp)), onClick = {})
AvatarPickerView(AvatarPickerState.Pick(buttonSize = 64.dp, externalPadding = PaddingValues(6.dp)), onClick = {})
AvatarPickerView(AvatarPickerState.Pick(buttonSize = 96.dp, externalPadding = PaddingValues(6.dp)), onClick = {})
}
Row {
AvatarPickerView(
AvatarPickerState.Selected(
avatarData = AvatarData("@user:example.com", "User", "content://test", size = AvatarSize.TimelineThreadLatestEventSender),
type = AvatarType.User
),
onClick = {},
modifier = Modifier.padding(6.dp)
)
AvatarPickerView(
AvatarPickerState.Selected(
avatarData = AvatarData("@user:example.com", "User", "content://test", size = AvatarSize.ReadReceiptList),
type = AvatarType.User
),
onClick = {},
modifier = Modifier.padding(6.dp)
)
AvatarPickerView(
AvatarPickerState.Selected(
avatarData = AvatarData("@user:example.com", "User", "content://test", size = AvatarSize.SelectedUser),
type = AvatarType.User
),
onClick = {},
modifier = Modifier.padding(6.dp)
)
AvatarPickerView(
AvatarPickerState.Selected(
avatarData = AvatarData("@user:example.com", "User", "content://test", size = AvatarSize.EditRoomDetails),
type = AvatarType.User
),
onClick = {},
modifier = Modifier.padding(6.dp)
)
AvatarPickerView(
AvatarPickerState.Selected(
avatarData = AvatarData("@user:example.com", "User", "content://test", size = AvatarSize.RoomListManageUser),
type = AvatarType.User
),
onClick = {},
modifier = Modifier.padding(6.dp)
)
}
Row {
AvatarPickerView(
AvatarPickerState.Selected(
avatarData = AvatarData("@user:example.com", "User", "content://test", size = AvatarSize.TimelineThreadLatestEventSender),
type = AvatarType.Space()
),
onClick = {},
modifier = Modifier.padding(6.dp)
)
AvatarPickerView(
AvatarPickerState.Selected(
avatarData = AvatarData("@user:example.com", "User", "content://test", size = AvatarSize.ReadReceiptList),
type = AvatarType.Space()
),
onClick = {},
modifier = Modifier.padding(6.dp)
)
AvatarPickerView(
AvatarPickerState.Selected(
avatarData = AvatarData("@user:example.com", "User", "content://test", size = AvatarSize.SelectedUser),
type = AvatarType.Space()
),
onClick = {},
modifier = Modifier.padding(6.dp)
)
AvatarPickerView(
AvatarPickerState.Selected(
avatarData = AvatarData("@user:example.com", "User", "content://test", size = AvatarSize.EditRoomDetails),
type = AvatarType.Space()
),
onClick = {},
modifier = Modifier.padding(6.dp)
)
AvatarPickerView(
AvatarPickerState.Selected(
avatarData = AvatarData("@user:example.com", "User", "content://test", size = AvatarSize.RoomListManageUser),
type = AvatarType.Space()
),
onClick = {},
modifier = Modifier.padding(6.dp)
)
}
}
}
@Composable
private fun PreviewContent() {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Pick image")
AvatarPickerView(AvatarPickerState.Pick(buttonSize = 48.dp, externalPadding = PaddingValues(6.dp)), onClick = {})
HorizontalDivider()
Text("User avatar")
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("No url")
AvatarPickerView(
AvatarPickerState.Selected(
avatarData = AvatarData("@user:example.com", "User", null, size = AvatarSize.EditRoomDetails),
type = AvatarType.User
),
onClick = {},
modifier = Modifier.padding(10.dp)
)
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("Local")
AvatarPickerView(
AvatarPickerState.Selected(
avatarData = AvatarData("@user:example.com", "User", "content://test", size = AvatarSize.EditRoomDetails),
type = AvatarType.User
),
onClick = {},
modifier = Modifier.padding(10.dp)
)
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("MXC")
AvatarPickerView(
AvatarPickerState.Selected(
avatarData = AvatarData("@user:example.com", "User", "mxc://test", size = AvatarSize.EditRoomDetails),
type = AvatarType.User
),
onClick = {},
modifier = Modifier.padding(10.dp)
)
}
}
HorizontalDivider()
Text("Room avatar")
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("No url")
AvatarPickerView(
AvatarPickerState.Selected(
avatarData = AvatarData("!room:example.com", "Room", null, size = AvatarSize.EditRoomDetails),
type = AvatarType.Room()
),
onClick = {},
modifier = Modifier.padding(10.dp)
)
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("Local")
AvatarPickerView(
AvatarPickerState.Selected(
avatarData = AvatarData("!room:example.com", "Room", "content://test", size = AvatarSize.EditRoomDetails),
type = AvatarType.Room()
),
onClick = {},
modifier = Modifier.padding(10.dp)
)
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("MXC")
AvatarPickerView(
AvatarPickerState.Selected(
avatarData = AvatarData("!room:example.com", "Room", "mxc://test", size = AvatarSize.EditRoomDetails),
type = AvatarType.Room()
),
onClick = {},
modifier = Modifier.padding(10.dp)
)
}
}
HorizontalDivider()
Text("Space avatar")
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("No url")
AvatarPickerView(
AvatarPickerState.Selected(
avatarData = AvatarData("!room:example.com", "Space", null, size = AvatarSize.EditRoomDetails),
type = AvatarType.Space()
),
onClick = {},
modifier = Modifier.padding(10.dp)
)
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("Local")
AvatarPickerView(
AvatarPickerState.Selected(
avatarData = AvatarData("!room:example.com", "Space", "content://test", size = AvatarSize.EditRoomDetails),
type = AvatarType.Space()
),
onClick = {},
modifier = Modifier.padding(10.dp)
)
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("MXC")
AvatarPickerView(
AvatarPickerState.Selected(
avatarData = AvatarData("!room:example.com", "Space", "mxc://test", size = AvatarSize.EditRoomDetails),
type = AvatarType.Space()
),
onClick = {},
modifier = Modifier.padding(10.dp)
)
}
}
}
}

View File

@@ -1,157 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.ui.components
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun EditableAvatarView(
matrixId: String,
displayName: String?,
avatarUrl: String?,
avatarSize: AvatarSize,
avatarType: AvatarType,
onAvatarClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
val a11yAvatar = stringResource(CommonStrings.a11y_avatar)
val editIconRadius = 15.dp
val parentHeight = avatarSize.dp
val parentWidth = avatarSize.dp + editIconRadius / 2f
Box(
modifier = modifier
.wrapContentSize()
.size(height = parentHeight, width = parentWidth)
.clickable(
enabled = enabled,
interactionSource = remember { MutableInteractionSource() },
onClickLabel = stringResource(CommonStrings.a11y_edit_avatar),
onClick = onAvatarClick,
indication = ripple(bounded = false),
)
.testTag(TestTags.editAvatar)
.clearAndSetSemantics {
contentDescription = a11yAvatar
},
) {
Box(
modifier = Modifier
.graphicsLayer {
compositingStrategy = CompositingStrategy.Offscreen
}
.drawWithContent {
drawContent()
drawCircle(
color = Color.Black,
center = Offset(
x = parentWidth.toPx() - editIconRadius.toPx(),
y = size.height - editIconRadius.toPx(),
),
radius = (editIconRadius + 4.dp).toPx(),
blendMode = BlendMode.Clear,
)
}
) {
when {
avatarUrl == null || avatarUrl.startsWith("mxc://") -> {
Avatar(
avatarData = AvatarData(
id = matrixId,
name = displayName,
url = avatarUrl,
size = avatarSize,
),
avatarType = avatarType,
)
}
else -> {
UnsavedAvatar(
avatarUri = avatarUrl,
avatarSize = avatarSize,
avatarType = avatarType,
)
}
}
}
Icon(
modifier = Modifier
.align(Alignment.BottomEnd)
.size(editIconRadius * 2)
.border(1.dp, ElementTheme.colors.borderInteractiveSecondary, CircleShape)
.padding(6.dp),
imageVector = CompoundIcons.Edit(),
contentDescription = null,
tint = ElementTheme.colors.iconPrimary,
)
}
}
@PreviewsDayNight
@Composable
internal fun EditableAvatarViewPreview(
@PreviewParameter(EditableAvatarViewUriProvider::class) uri: String?
) = ElementPreview(
drawableFallbackForImages = CommonDrawables.sample_avatar,
) {
EditableAvatarView(
matrixId = "id",
displayName = "Room",
avatarUrl = uri,
avatarSize = AvatarSize.RoomDetailsHeader,
avatarType = AvatarType.User,
onAvatarClick = {},
)
}
open class EditableAvatarViewUriProvider : PreviewParameterProvider<String?> {
override val values: Sequence<String?>
get() = sequenceOf(
null,
"mxc://matrix.org/123456",
"https://example.com/avatar.jpg",
)
}

View File

@@ -1,93 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
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.unit.dp
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.components.avatar.avatarShape
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.temporaryColorBgSpecial
/**
* An avatar that the user has selected, but which has not yet been uploaded to Matrix.
*
* The image is loaded from a local resource instead of from a MXC URI.
*/
@Composable
fun UnsavedAvatar(
avatarUri: String?,
avatarSize: AvatarSize,
avatarType: AvatarType,
modifier: Modifier = Modifier,
) {
val commonModifier = modifier
.size(avatarSize.dp)
.clip(avatarType.avatarShape(avatarSize.dp))
if (avatarUri != null) {
val context = LocalContext.current
val model = ImageRequest.Builder(context)
.data(avatarUri)
.build()
AsyncImage(
modifier = commonModifier,
model = model,
placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant),
contentScale = ContentScale.Crop,
contentDescription = null,
)
} else {
Box(modifier = commonModifier.background(ElementTheme.colors.temporaryColorBgSpecial)) {
Icon(
imageVector = Icons.Outlined.AddAPhoto,
contentDescription = null,
modifier = Modifier
.align(Alignment.Center)
.size(avatarSize.dp * 4 / 7),
tint = ElementTheme.colors.iconSecondary,
)
}
}
}
@PreviewsDayNight
@Composable
internal fun UnsavedAvatarPreview() = ElementPreview {
Row(
modifier = Modifier.padding(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
UnsavedAvatar(null, AvatarSize.EditRoomDetails, AvatarType.User)
UnsavedAvatar("", AvatarSize.EditRoomDetails, AvatarType.User)
UnsavedAvatar(null, AvatarSize.EditRoomDetails, AvatarType.Space())
UnsavedAvatar("", AvatarSize.EditRoomDetails, AvatarType.Space())
}
}

View File

@@ -27,7 +27,7 @@ fun RoomAddressField(
homeserverName: String,
addressValidity: RoomAddressValidity,
onAddressChange: (String) -> Unit,
label: String,
label: String?,
supportingText: String,
modifier: Modifier = Modifier,
) {

View File

@@ -56,6 +56,7 @@
<string name="a11y_your_avatar">"Your avatar"</string>
<string name="action_accept">"Accept"</string>
<string name="action_add_caption">"Add caption"</string>
<string name="action_add_existing_rooms">"Add existing rooms"</string>
<string name="action_add_to_timeline">"Add to timeline"</string>
<string name="action_back">"Back"</string>
<string name="action_call">"Call"</string>
@@ -75,6 +76,7 @@
<string name="action_copy_text">"Copy text"</string>
<string name="action_create">"Create"</string>
<string name="action_create_a_room">"Create a room"</string>
<string name="action_create_space">"Create space"</string>
<string name="action_deactivate">"Deactivate"</string>
<string name="action_deactivate_account">"Deactivate account"</string>
<string name="action_decline">"Decline"</string>
@@ -112,6 +114,7 @@
<string name="action_load_more">"Load more"</string>
<string name="action_manage_account">"Manage account"</string>
<string name="action_manage_devices">"Manage devices"</string>
<string name="action_manage_rooms">"Manage rooms"</string>
<string name="action_message">"Message"</string>
<string name="action_minimize">"Minimise"</string>
<string name="action_next">"Next"</string>
@@ -191,6 +194,7 @@
<string name="common_copied_to_clipboard">"Copied to clipboard"</string>
<string name="common_copyright">"Copyright"</string>
<string name="common_creating_room">"Creating room…"</string>
<string name="common_creating_space">"Creating space…"</string>
<string name="common_current_user_canceled_knock">"Request canceled"</string>
<string name="common_current_user_left_room">"Left room"</string>
<string name="common_current_user_left_space">"Left space"</string>
@@ -337,6 +341,7 @@ Reason: %1$s."</string>
<string name="common_starting_chat">"Starting chat…"</string>
<string name="common_sticker">"Sticker"</string>
<string name="common_success">"Success"</string>
<string name="common_suggested">"Suggested"</string>
<string name="common_suggestions">"Suggestions"</string>
<string name="common_syncing">"Syncing"</string>
<string name="common_system">"System"</string>
@@ -473,6 +478,7 @@ Are you sure you want to continue?"</string>
<string name="screen_share_this_location_action">"Share this location"</string>
<string name="screen_space_list_description">"Spaces you have created or joined."</string>
<string name="screen_space_list_details">"%1$s • %2$s"</string>
<string name="screen_space_list_empty_state_title">"Create spaces to organize rooms"</string>
<string name="screen_space_list_parent_space">"%1$s space"</string>
<string name="screen_space_list_title">"Spaces"</string>
<string name="screen_space_menu_action_members">"View members"</string>

View File

@@ -79,6 +79,9 @@ class KonsistPreviewTest {
private val previewNameExceptions = listOf(
"AsyncIndicatorFailurePreview",
"AsyncIndicatorLoadingPreview",
"AvatarPickerSizesPreview",
"AvatarPickerViewPreview",
"AvatarPickerViewRtlPreview",
"BackgroundVerticalGradientDisabledPreview",
"BackgroundVerticalGradientPreview",
"ColorAliasesPreview",
@@ -86,6 +89,7 @@ class KonsistPreviewTest {
"GradientFloatingActionButtonCircleShapePreview",
"HeaderFooterPageScrollablePreview",
"HomeTopBarMultiAccountPreview",
"HomeTopBarSpacesPreview",
"HomeTopBarWithIndicatorPreview",
"IconsOtherPreview",
"MarkdownTextComposerEditPreview",

Some files were not shown because too many files have changed in this diff Show More