create room : start adding new options in ui

This commit is contained in:
ganfra 2024-10-25 18:27:23 +02:00
parent 5674d10d9b
commit f6938fa737
15 changed files with 447 additions and 110 deletions

View file

@ -8,7 +8,7 @@
package io.element.android.features.createroom.impl package io.element.android.features.createroom.impl
import android.net.Uri import android.net.Uri
import io.element.android.features.createroom.impl.configureroom.RoomPrivacy import io.element.android.features.createroom.impl.configureroom.RoomVisibilityState
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -18,5 +18,5 @@ data class CreateRoomConfig(
val topic: String? = null, val topic: String? = null,
val avatarUri: Uri? = null, val avatarUri: Uri? = null,
val invites: ImmutableList<MatrixUser> = persistentListOf(), val invites: ImmutableList<MatrixUser> = persistentListOf(),
val privacy: RoomPrivacy = RoomPrivacy.Private, val roomVisibility: RoomVisibilityState = RoomVisibilityState.Private,
) )

View file

@ -8,7 +8,11 @@
package io.element.android.features.createroom.impl package io.element.android.features.createroom.impl
import android.net.Uri import android.net.Uri
import io.element.android.features.createroom.impl.configureroom.RoomPrivacy import io.element.android.features.createroom.impl.configureroom.RoomAccess
import io.element.android.features.createroom.impl.configureroom.RoomAccessItem
import io.element.android.features.createroom.impl.configureroom.RoomAddress
import io.element.android.features.createroom.impl.configureroom.RoomVisibilityItem
import io.element.android.features.createroom.impl.configureroom.RoomVisibilityState
import io.element.android.features.createroom.impl.di.CreateRoomScope import io.element.android.features.createroom.impl.di.CreateRoomScope
import io.element.android.features.createroom.impl.userlist.UserListDataStore import io.element.android.features.createroom.impl.userlist.UserListDataStore
import io.element.android.libraries.androidutils.file.safeDelete import io.element.android.libraries.androidutils.file.safeDelete
@ -17,7 +21,9 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.getAndUpdate
import java.io.File import java.io.File
import java.text.Normalizer
import javax.inject.Inject import javax.inject.Inject
@SingleIn(CreateRoomScope::class) @SingleIn(CreateRoomScope::class)
@ -31,28 +37,87 @@ class CreateRoomDataStore @Inject constructor(
field = value field = value
} }
fun getCreateRoomConfig(): Flow<CreateRoomConfig> = combine( val createRoomConfig: Flow<CreateRoomConfig> = combine(
selectedUserListDataStore.selectedUsers(), selectedUserListDataStore.selectedUsers(),
createRoomConfigFlow, createRoomConfigFlow,
) { selectedUsers, config -> ) { selectedUsers, config ->
config.copy(invites = selectedUsers.toImmutableList()) config.copy(invites = selectedUsers.toImmutableList())
} }
fun setRoomName(roomName: String?) { fun setRoomName(roomName: String) {
createRoomConfigFlow.tryEmit(createRoomConfigFlow.value.copy(roomName = roomName?.takeIf { it.isNotEmpty() })) createRoomConfigFlow.getAndUpdate { config ->
val newVisibility = when (config.roomVisibility) {
is RoomVisibilityState.Public -> {
val roomAddress = config.roomVisibility.roomAddress
if (roomAddress is RoomAddress.AutoFilled || roomName.isEmpty()) {
config.roomVisibility.copy(
roomAddress = RoomAddress.AutoFilled(roomName),
)
} else {
config.roomVisibility
}
}
else -> config.roomVisibility
}
config.copy(
roomName = roomName.takeIf { it.isNotEmpty() },
roomVisibility = newVisibility,
)
}
} }
fun setTopic(topic: String?) { fun setTopic(topic: String) {
createRoomConfigFlow.tryEmit(createRoomConfigFlow.value.copy(topic = topic?.takeIf { it.isNotEmpty() })) createRoomConfigFlow.getAndUpdate { config ->
config.copy(topic = topic.takeIf { it.isNotEmpty() })
}
} }
fun setAvatarUri(uri: Uri?, cached: Boolean = false) { fun setAvatarUri(uri: Uri?, cached: Boolean = false) {
cachedAvatarUri = uri.takeIf { cached } cachedAvatarUri = uri.takeIf { cached }
createRoomConfigFlow.tryEmit(createRoomConfigFlow.value.copy(avatarUri = uri)) createRoomConfigFlow.getAndUpdate { config ->
config.copy(avatarUri = uri)
}
} }
fun setPrivacy(privacy: RoomPrivacy) { fun setRoomVisibility(visibility: RoomVisibilityItem) {
createRoomConfigFlow.tryEmit(createRoomConfigFlow.value.copy(privacy = privacy)) createRoomConfigFlow.getAndUpdate { config ->
config.copy(
roomVisibility = when (visibility) {
RoomVisibilityItem.Private -> RoomVisibilityState.Private
RoomVisibilityItem.Public -> RoomVisibilityState.Public(
roomAddress = RoomAddress.AutoFilled(config.roomName.orEmpty()),
roomAccess = RoomAccess.Anyone,
)
}
)
}
}
fun setRoomAddress(address: String) {
createRoomConfigFlow.getAndUpdate { config ->
config.copy(
roomVisibility = when (config.roomVisibility) {
is RoomVisibilityState.Public -> config.roomVisibility.copy(roomAddress = RoomAddress.Edited(address))
else -> config.roomVisibility
}
)
}
}
fun setRoomAccess(access: RoomAccessItem) {
createRoomConfigFlow.getAndUpdate { config ->
config.copy(
roomVisibility = when (config.roomVisibility) {
is RoomVisibilityState.Public -> {
when (access) {
RoomAccessItem.Anyone -> config.roomVisibility.copy(roomAccess = RoomAccess.Anyone)
RoomAccessItem.AskToJoin -> config.roomVisibility.copy(roomAccess = RoomAccess.Knocking)
}
}
else -> config.roomVisibility
}
)
}
} }
fun clearCachedData() { fun clearCachedData() {

View file

@ -0,0 +1,87 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.createroom.impl.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.selectable
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.createroom.impl.configureroom.RoomAccessItem
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.RadioButton
import io.element.android.libraries.designsystem.theme.components.Text
@Composable
fun RoomAccessOption(
roomAccessItem: RoomAccessItem,
onOptionClick: (RoomAccessItem) -> Unit,
modifier: Modifier = Modifier,
isSelected: Boolean = false,
) {
Row(
modifier
.fillMaxWidth()
.selectable(
selected = isSelected,
onClick = { onOptionClick(roomAccessItem) },
role = Role.RadioButton,
)
) {
Column(Modifier.weight(1f)) {
Text(
text = stringResource(roomAccessItem.title),
style = ElementTheme.typography.fontBodyLgRegular,
color = MaterialTheme.colorScheme.primary,
)
Spacer(Modifier.size(8.dp))
Text(
text = stringResource(roomAccessItem.description),
style = ElementTheme.typography.fontBodySmRegular,
color = MaterialTheme.colorScheme.tertiary,
)
}
RadioButton(
modifier = Modifier
.align(Alignment.CenterVertically)
.size(48.dp),
selected = isSelected,
// null recommended for accessibility with screenreaders
onClick = null
)
}
}
@PreviewsDayNight
@Composable
internal fun RoomAccessOptionPreview() = ElementPreview {
val aRoomAccessItem = RoomAccessItem.Anyone
Column {
RoomAccessOption(
roomAccessItem = aRoomAccessItem,
onOptionClick = {},
isSelected = true,
)
RoomAccessOption(
roomAccessItem = aRoomAccessItem,
onOptionClick = {},
isSelected = false,
)
}
}

View file

@ -7,6 +7,8 @@
package io.element.android.features.createroom.impl.components package io.element.android.features.createroom.impl.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@ -14,15 +16,17 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.features.createroom.impl.configureroom.RoomPrivacyItem import io.element.android.features.createroom.impl.configureroom.RoomVisibilityItem
import io.element.android.features.createroom.impl.configureroom.roomPrivacyItems
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
@ -30,9 +34,9 @@ import io.element.android.libraries.designsystem.theme.components.RadioButton
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
@Composable @Composable
fun RoomPrivacyOption( fun RoomVisibilityOption(
roomPrivacyItem: RoomPrivacyItem, roomPrivacyItem: RoomVisibilityItem,
onOptionClick: (RoomPrivacyItem) -> Unit, onOptionClick: (RoomVisibilityItem) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isSelected: Boolean = false, isSelected: Boolean = false,
) { ) {
@ -44,28 +48,31 @@ fun RoomPrivacyOption(
onClick = { onOptionClick(roomPrivacyItem) }, onClick = { onOptionClick(roomPrivacyItem) },
role = Role.RadioButton, role = Role.RadioButton,
) )
.padding(8.dp),
) { ) {
Icon( Box(
modifier = Modifier.padding(horizontal = 8.dp), modifier = modifier
resourceId = roomPrivacyItem.icon, .size(30.dp)
contentDescription = null, .clip(RoundedCornerShape(8.dp))
tint = MaterialTheme.colorScheme.secondary, .background(ElementTheme.colors.bgSubtleSecondary)
) .padding(3.dp),
contentAlignment = Alignment.Center,
Column(
Modifier
.weight(1f)
.padding(horizontal = 8.dp)
) { ) {
Icon(
resourceId = roomPrivacyItem.icon,
contentDescription = null,
tint = if(isSelected) ElementTheme.colors.iconPrimary else ElementTheme.colors.iconSecondary,
)
}
Spacer(Modifier.size(16.dp))
Column(Modifier.weight(1f)) {
Text( Text(
text = roomPrivacyItem.title, text = stringResource(roomPrivacyItem.title),
style = ElementTheme.typography.fontBodyLgRegular, style = ElementTheme.typography.fontBodyLgRegular,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
) )
Spacer(Modifier.size(3.dp)) Spacer(Modifier.size(3.dp))
Text( Text(
text = roomPrivacyItem.description, text = stringResource(roomPrivacyItem.description),
style = ElementTheme.typography.fontBodySmRegular, style = ElementTheme.typography.fontBodySmRegular,
color = MaterialTheme.colorScheme.tertiary, color = MaterialTheme.colorScheme.tertiary,
) )
@ -85,14 +92,14 @@ fun RoomPrivacyOption(
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun RoomPrivacyOptionPreview() = ElementPreview { internal fun RoomPrivacyOptionPreview() = ElementPreview {
val aRoomPrivacyItem = roomPrivacyItems().first() val aRoomPrivacyItem = RoomVisibilityItem.Private
Column { Column {
RoomPrivacyOption( RoomVisibilityOption(
roomPrivacyItem = aRoomPrivacyItem, roomPrivacyItem = aRoomPrivacyItem,
onOptionClick = {}, onOptionClick = {},
isSelected = true, isSelected = true,
) )
RoomPrivacyOption( RoomVisibilityOption(
roomPrivacyItem = aRoomPrivacyItem, roomPrivacyItem = aRoomPrivacyItem,
onOptionClick = {}, onOptionClick = {},
isSelected = false, isSelected = false,

View file

@ -14,8 +14,10 @@ import io.element.android.libraries.matrix.ui.media.AvatarAction
sealed interface ConfigureRoomEvents { sealed interface ConfigureRoomEvents {
data class RoomNameChanged(val name: String) : ConfigureRoomEvents data class RoomNameChanged(val name: String) : ConfigureRoomEvents
data class TopicChanged(val topic: String) : ConfigureRoomEvents data class TopicChanged(val topic: String) : ConfigureRoomEvents
data class RoomPrivacyChanged(val privacy: RoomPrivacy) : ConfigureRoomEvents data class RoomVisibilityChanged(val visibilityItem: RoomVisibilityItem) : ConfigureRoomEvents
data class RemoveFromSelection(val matrixUser: MatrixUser) : ConfigureRoomEvents data class RoomAccessChanged(val roomAccess: RoomAccessItem) : ConfigureRoomEvents
data class RoomAddressChanged(val roomAddress: String) : ConfigureRoomEvents
data class RemoveUserFromSelection(val matrixUser: MatrixUser) : ConfigureRoomEvents
data class CreateRoom(val config: CreateRoomConfig) : ConfigureRoomEvents data class CreateRoom(val config: CreateRoomConfig) : ConfigureRoomEvents
data class HandleAvatarAction(val action: AvatarAction) : ConfigureRoomEvents data class HandleAvatarAction(val action: AvatarAction) : ConfigureRoomEvents
data object CancelCreateRoom : ConfigureRoomEvents data object CancelCreateRoom : ConfigureRoomEvents

View file

@ -54,7 +54,7 @@ class ConfigureRoomPresenter @Inject constructor(
@Composable @Composable
override fun present(): ConfigureRoomState { override fun present(): ConfigureRoomState {
val cameraPermissionState = cameraPermissionPresenter.present() val cameraPermissionState = cameraPermissionPresenter.present()
val createRoomConfig = dataStore.getCreateRoomConfig().collectAsState(CreateRoomConfig()) val createRoomConfig = dataStore.createRoomConfig.collectAsState(CreateRoomConfig())
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker( val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(
onResult = { uri -> if (uri != null) dataStore.setAvatarUri(uri = uri, cached = true) }, onResult = { uri -> if (uri != null) dataStore.setAvatarUri(uri = uri, cached = true) },
@ -92,8 +92,10 @@ class ConfigureRoomPresenter @Inject constructor(
when (event) { when (event) {
is ConfigureRoomEvents.RoomNameChanged -> dataStore.setRoomName(event.name) is ConfigureRoomEvents.RoomNameChanged -> dataStore.setRoomName(event.name)
is ConfigureRoomEvents.TopicChanged -> dataStore.setTopic(event.topic) is ConfigureRoomEvents.TopicChanged -> dataStore.setTopic(event.topic)
is ConfigureRoomEvents.RoomPrivacyChanged -> dataStore.setPrivacy(event.privacy) is ConfigureRoomEvents.RoomVisibilityChanged -> dataStore.setRoomVisibility(event.visibilityItem)
is ConfigureRoomEvents.RemoveFromSelection -> dataStore.selectedUserListDataStore.removeUserFromSelection(event.matrixUser) is ConfigureRoomEvents.RemoveUserFromSelection -> dataStore.selectedUserListDataStore.removeUserFromSelection(event.matrixUser)
is ConfigureRoomEvents.RoomAccessChanged -> dataStore.setRoomAccess(event.roomAccess)
is ConfigureRoomEvents.RoomAddressChanged -> dataStore.setRoomAddress(event.roomAddress)
is ConfigureRoomEvents.CreateRoom -> createRoom(event.config) is ConfigureRoomEvents.CreateRoom -> createRoom(event.config)
is ConfigureRoomEvents.HandleAvatarAction -> { is ConfigureRoomEvents.HandleAvatarAction -> {
when (event.action) { when (event.action) {
@ -109,6 +111,7 @@ class ConfigureRoomPresenter @Inject constructor(
} }
ConfigureRoomEvents.CancelCreateRoom -> createRoomAction.value = AsyncAction.Uninitialized ConfigureRoomEvents.CancelCreateRoom -> createRoomAction.value = AsyncAction.Uninitialized
} }
} }
@ -130,10 +133,10 @@ class ConfigureRoomPresenter @Inject constructor(
val params = CreateRoomParameters( val params = CreateRoomParameters(
name = config.roomName, name = config.roomName,
topic = config.topic, topic = config.topic,
isEncrypted = config.privacy == RoomPrivacy.Private, isEncrypted = config.roomVisibility is RoomVisibilityState.Private,
isDirect = false, isDirect = false,
visibility = if (config.privacy == RoomPrivacy.Public) RoomVisibility.PUBLIC else RoomVisibility.PRIVATE, visibility = if (config.roomVisibility is RoomVisibilityState.Public) RoomVisibility.PUBLIC else RoomVisibility.PRIVATE,
preset = if (config.privacy == RoomPrivacy.Public) RoomPreset.PUBLIC_CHAT else RoomPreset.PRIVATE_CHAT, preset = if (config.roomVisibility is RoomVisibilityState.Public) RoomPreset.PUBLIC_CHAT else RoomPreset.PRIVATE_CHAT,
invite = config.invites.map { it.userId }, invite = config.invites.map { it.userId },
avatar = avatarUrl, avatar = avatarUrl,
) )

View file

@ -24,7 +24,10 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
roomName = "Room 101", roomName = "Room 101",
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldnt be more than 3 lines", topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldnt be more than 3 lines",
invites = aMatrixUserList().toImmutableList(), invites = aMatrixUserList().toImmutableList(),
privacy = RoomPrivacy.Public, roomVisibility = RoomVisibilityState.Public(
roomAddress = RoomAddress.AutoFilled("Room 101"),
roomAccess = RoomAccess.Knocking
),
), ),
), ),
) )

View file

@ -11,16 +11,22 @@ import android.net.Uri
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -29,22 +35,26 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.features.createroom.impl.R import io.element.android.features.createroom.impl.R
import io.element.android.features.createroom.impl.components.RoomPrivacyOption import io.element.android.features.createroom.impl.components.RoomAccessOption
import io.element.android.features.createroom.impl.components.RoomVisibilityOption
import io.element.android.libraries.designsystem.components.LabelledTextField import io.element.android.libraries.designsystem.components.LabelledTextField
import io.element.android.libraries.designsystem.components.async.AsyncActionView 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.async.AsyncActionViewDefaults
import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.modifiers.clearFocusOnTap import io.element.android.libraries.designsystem.modifiers.clearFocusOnTap
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.aliasScreenTitle import io.element.android.libraries.designsystem.theme.aliasScreenTitle
import io.element.android.libraries.designsystem.theme.components.Scaffold 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.Text
import io.element.android.libraries.designsystem.theme.components.TextButton 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.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.core.RoomId 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.AvatarActionBottomSheet
@ -103,23 +113,38 @@ fun ConfigureRoomView(
) )
if (state.config.invites.isNotEmpty()) { if (state.config.invites.isNotEmpty()) {
SelectedUsersRowList( SelectedUsersRowList(
modifier = Modifier.padding(bottom = 16.dp),
contentPadding = PaddingValues(horizontal = 24.dp), contentPadding = PaddingValues(horizontal = 24.dp),
selectedUsers = state.config.invites, selectedUsers = state.config.invites,
onUserRemove = { onUserRemove = {
focusManager.clearFocus() focusManager.clearFocus()
state.eventSink(ConfigureRoomEvents.RemoveFromSelection(it)) state.eventSink(ConfigureRoomEvents.RemoveUserFromSelection(it))
}, },
) )
} }
RoomPrivacyOptions( RoomVisibilityOptions(
modifier = Modifier.padding(bottom = 40.dp), selected = when (state.config.roomVisibility) {
selected = state.config.privacy, is RoomVisibilityState.Private -> RoomVisibilityItem.Private
is RoomVisibilityState.Public -> RoomVisibilityItem.Public
},
onOptionClick = { onOptionClick = {
focusManager.clearFocus() focusManager.clearFocus()
state.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(it.privacy)) state.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(it))
}, },
) )
if (state.config.roomVisibility is RoomVisibilityState.Public) {
RoomAccessOptions(
selected = state.config.roomVisibility.roomAccess,
onOptionClick = {
focusManager.clearFocus()
state.eventSink(ConfigureRoomEvents.RoomAccessChanged(it))
},
)
RoomAddress(
modifier = Modifier.padding(horizontal = 16.dp),
address = state.config.roomVisibility.roomAddress,
onAddressChange = { state.eventSink(ConfigureRoomEvents.RoomAddressChanged(it)) },
)
}
} }
} }
@ -221,24 +246,125 @@ private fun RoomTopic(
} }
@Composable @Composable
private fun RoomPrivacyOptions( private fun ConfigureRoomOptions(
selected: RoomPrivacy?, title: String,
onOptionClick: (RoomPrivacyItem) -> Unit, verticalArrangement: Arrangement.Vertical,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit,
) {
Column(
modifier = modifier
.selectableGroup()
.padding(horizontal = 12.dp),
verticalArrangement = verticalArrangement,
) {
Text(
text = title,
style = ElementTheme.typography.fontBodyLgMedium,
color = ElementTheme.colors.textPrimary,
)
content()
}
}
@Composable
private fun RoomVisibilityOptions(
selected: RoomVisibilityItem,
onOptionClick: (RoomVisibilityItem) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val items = roomPrivacyItems() ConfigureRoomOptions(
Column(modifier = modifier.selectableGroup()) { title = "Room visibility",
items.forEach { item -> modifier = modifier,
RoomPrivacyOption( verticalArrangement = Arrangement.spacedBy(16.dp),
) {
RoomVisibilityItem.entries.forEach { item ->
RoomVisibilityOption(
roomPrivacyItem = item, roomPrivacyItem = item,
isSelected = selected == item.privacy, isSelected = item == selected,
onOptionClick = onOptionClick, onOptionClick = onOptionClick,
) )
} }
} }
} }
@Composable
private fun RoomAccessOptions(
selected: RoomAccess,
onOptionClick: (RoomAccessItem) -> Unit,
modifier: Modifier = Modifier,
) {
ConfigureRoomOptions(
title = "Room access",
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
RoomAccessItem.entries.forEach { item ->
RoomAccessOption(
roomAccessItem = item,
isSelected = when (item) {
RoomAccessItem.Anyone -> selected == RoomAccess.Anyone
RoomAccessItem.AskToJoin -> selected == RoomAccess.Knocking
},
onOptionClick = onOptionClick,
)
}
}
}
@Composable
private fun RoomAddress(
address: RoomAddress,
onAddressChange: (String) -> Unit,
modifier: Modifier = Modifier,
){
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
modifier = Modifier.padding(horizontal = 16.dp),
style = ElementTheme.typography.fontBodyMdRegular,
color = MaterialTheme.colorScheme.primary,
text = "Room address",
)
TextField(
modifier = Modifier.fillMaxWidth(),
value = when(address) {
is RoomAddress.AutoFilled -> address.address
is RoomAddress.Edited -> address.address
},
leadingIcon = {
Text(
text = "#",
style = ElementTheme.typography.fontBodyLgMedium,
color = ElementTheme.colors.textSecondary,
)
},
trailingIcon = {
Text(
text = ":myserver.com",
style = ElementTheme.typography.fontBodyLgMedium,
color = ElementTheme.colors.textSecondary,
modifier = Modifier.padding(end = 16.dp)
)
},
supportingText = {
Text(
text = "In order for this room to be visible in the public room directory, you will need to a room address. ",
style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textSecondary,
)
},
onValueChange = onAddressChange,
singleLine = true,
)
}
}
@PreviewsDayNight @PreviewsDayNight
@PreviewWithLargeHeight
@Composable @Composable
internal fun ConfigureRoomViewPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = ElementPreview { internal fun ConfigureRoomViewPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = ElementPreview {
ConfigureRoomView( ConfigureRoomView(

View file

@ -1,5 +1,5 @@
/* /*
* Copyright 2023, 2024 New Vector Ltd. * Copyright 2024 New Vector Ltd.
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details. * Please see LICENSE in the repository root for full details.
@ -7,7 +7,6 @@
package io.element.android.features.createroom.impl.configureroom package io.element.android.features.createroom.impl.configureroom
enum class RoomPrivacy { enum class RoomAccess {
Private, Anyone, Knocking, Invite
Public,
} }

View file

@ -0,0 +1,26 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.createroom.impl.configureroom
import androidx.annotation.StringRes
import io.element.android.libraries.ui.strings.CommonStrings
enum class RoomAccessItem(
@StringRes val title: Int,
@StringRes val description: Int
) {
Anyone(
title = CommonStrings.screen_create_room_access_section_anyone_option_title,
description = CommonStrings.screen_create_room_access_section_anyone_option_description,
),
AskToJoin(
title = CommonStrings.screen_create_room_access_section_knocking_option_title,
description = CommonStrings.screen_create_room_access_section_knocking_option_description,
)
}

View file

@ -0,0 +1,13 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.createroom.impl.configureroom
sealed interface RoomAddress {
data class AutoFilled(val address: String) : RoomAddress
data class Edited(val address: String) : RoomAddress
}

View file

@ -1,45 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.createroom.impl.configureroom
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import io.element.android.features.createroom.impl.R
import io.element.android.libraries.designsystem.icons.CompoundDrawables
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
data class RoomPrivacyItem(
val privacy: RoomPrivacy,
@DrawableRes val icon: Int,
val title: String,
val description: String,
)
@Composable
fun roomPrivacyItems(): ImmutableList<RoomPrivacyItem> {
return RoomPrivacy.entries
.map {
when (it) {
RoomPrivacy.Private -> RoomPrivacyItem(
privacy = it,
icon = CompoundDrawables.ic_compound_lock_solid,
title = stringResource(R.string.screen_create_room_private_option_title),
description = stringResource(R.string.screen_create_room_private_option_description),
)
RoomPrivacy.Public -> RoomPrivacyItem(
privacy = it,
icon = CompoundDrawables.ic_compound_public,
title = stringResource(R.string.screen_create_room_public_option_title),
description = stringResource(R.string.screen_create_room_public_option_description),
)
}
}
.toImmutableList()
}

View file

@ -0,0 +1,30 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
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,
)
}

View file

@ -0,0 +1,21 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.createroom.impl.configureroom
sealed interface RoomVisibilityState {
val roomAccess: RoomAccess
data object Private : RoomVisibilityState {
override val roomAccess: RoomAccess = RoomAccess.Invite
}
data class Public(
val roomAddress: RoomAddress,
override val roomAccess: RoomAccess
) : RoomVisibilityState
}

View file

@ -103,7 +103,7 @@ class ConfigureRoomPresenterTest {
assertThat(initialState.config.topic).isNull() assertThat(initialState.config.topic).isNull()
assertThat(initialState.config.invites).isEmpty() assertThat(initialState.config.invites).isEmpty()
assertThat(initialState.config.avatarUri).isNull() assertThat(initialState.config.avatarUri).isNull()
assertThat(initialState.config.privacy).isEqualTo(RoomPrivacy.Private) assertThat(initialState.config.roomVisibility).isEqualTo(RoomVisibilityState.Private)
} }
} }
@ -200,13 +200,13 @@ class ConfigureRoomPresenterTest {
assertThat(newState.config).isEqualTo(expectedConfig) assertThat(newState.config).isEqualTo(expectedConfig)
// Room privacy // Room privacy
newState.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(RoomPrivacy.Public)) newState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityState.Public))
newState = awaitItem() newState = awaitItem()
expectedConfig = expectedConfig.copy(privacy = RoomPrivacy.Public) expectedConfig = expectedConfig.copy(roomVisibility = RoomVisibilityState.Public)
assertThat(newState.config).isEqualTo(expectedConfig) assertThat(newState.config).isEqualTo(expectedConfig)
// Remove user // Remove user
newState.eventSink(ConfigureRoomEvents.RemoveFromSelection(selectedUser1)) newState.eventSink(ConfigureRoomEvents.RemoveUserFromSelection(selectedUser1))
newState = awaitItem() newState = awaitItem()
expectedConfig = expectedConfig.copy(invites = expectedConfig.invites.minus(selectedUser1).toImmutableList()) expectedConfig = expectedConfig.copy(invites = expectedConfig.invites.minus(selectedUser1).toImmutableList())
assertThat(newState.config).isEqualTo(expectedConfig) assertThat(newState.config).isEqualTo(expectedConfig)