Create a new room in a space (#6061)
* Add `SpaceService.editableSpaces` and `SpaceService.addChildToSpace` * Add `parentSpace` to `CreateRoomConfig` * Allow setting a parent space to a room in `ConfigureRoomPresenter`, make sure the room is added to the parent space when creating it * `ConfigureRoomPresenter`: Load the list of possible spaces a room can be added to * Refactor `RoomVisibilityState` to internally use `JoinRuleItem` This gets rid of `RoomAccess` and `RoomAccessItem`, and it will allow us to map the join rule items in a cleaner way to both join rules and the UI * Implement the UI changes: - Display the parent space. - Allow selecting a new one. - Import needed strings. * Fix existing tests * Add `@Immutable` annotation to `SpaceRoom`, since it was detected as unstable. Maybe because of `RoomType`? * Update screenshots --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
committed by
GitHub
parent
5644e9225a
commit
0313fa56dd
@@ -38,6 +38,7 @@ dependencies {
|
||||
implementation(projects.libraries.mediapickers.api)
|
||||
implementation(projects.libraries.mediaupload.api)
|
||||
implementation(projects.libraries.permissions.api)
|
||||
implementation(projects.libraries.previewutils)
|
||||
implementation(projects.libraries.usersearch.impl)
|
||||
implementation(projects.services.analytics.api)
|
||||
implementation(libs.coil.compose)
|
||||
|
||||
@@ -8,15 +8,16 @@
|
||||
|
||||
package io.element.android.features.createroom.impl.configureroom
|
||||
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||
|
||||
sealed interface ConfigureRoomEvents {
|
||||
data class RoomNameChanged(val name: String) : ConfigureRoomEvents
|
||||
data class TopicChanged(val topic: String) : ConfigureRoomEvents
|
||||
data class RoomVisibilityChanged(val visibilityItem: RoomVisibilityItem) : ConfigureRoomEvents
|
||||
data class RoomAccessChanged(val roomAccess: RoomAccessItem) : ConfigureRoomEvents
|
||||
data class JoinRuleChanged(val joinRuleItem: JoinRuleItem) : ConfigureRoomEvents
|
||||
data class RoomAddressChanged(val roomAddress: String) : ConfigureRoomEvents
|
||||
data object CreateRoom : ConfigureRoomEvents
|
||||
data class HandleAvatarAction(val action: AvatarAction) : ConfigureRoomEvents
|
||||
data class SetParentSpace(val space: SpaceRoom?) : ConfigureRoomEvents
|
||||
data object CancelCreateRoom : ConfigureRoomEvents
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.net.toUri
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
@@ -35,7 +36,9 @@ import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
|
||||
import io.element.android.libraries.matrix.api.createroom.RoomPreset
|
||||
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
|
||||
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
|
||||
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidityEffect
|
||||
@@ -45,11 +48,17 @@ import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||
import io.element.android.libraries.permissions.api.PermissionsEvent
|
||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import timber.log.Timber
|
||||
import kotlin.jvm.optionals.getOrDefault
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@AssistedInject
|
||||
class ConfigureRoomPresenter(
|
||||
@@ -78,6 +87,7 @@ class ConfigureRoomPresenter(
|
||||
|
||||
@Composable
|
||||
override fun present(): ConfigureRoomState {
|
||||
val canAddRoomToSpace by featureFlagService.isFeatureEnabledFlow(FeatureFlags.CreateSpaces).collectAsState(false)
|
||||
val cameraPermissionState = cameraPermissionPresenter.present()
|
||||
val createRoomConfig by dataStore.getCreateRoomConfigFlow().collectAsState()
|
||||
val homeserverName = remember { matrixClient.userIdServerName() }
|
||||
@@ -105,6 +115,15 @@ class ConfigureRoomPresenter(
|
||||
}
|
||||
}
|
||||
|
||||
var spaces by remember { mutableStateOf<ImmutableList<SpaceRoom>>(persistentListOf()) }
|
||||
LaunchedEffect(canAddRoomToSpace) {
|
||||
spaces = if (canAddRoomToSpace) {
|
||||
matrixClient.spaceService.editableSpaces().getOrElse { emptyList() }.toImmutableList()
|
||||
} else {
|
||||
persistentListOf()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(cameraPermissionState.permissionGranted) {
|
||||
if (cameraPermissionState.permissionGranted && pendingPermissionRequest) {
|
||||
pendingPermissionRequest = false
|
||||
@@ -115,7 +134,7 @@ class ConfigureRoomPresenter(
|
||||
RoomAddressValidityEffect(
|
||||
client = matrixClient,
|
||||
roomAliasHelper = roomAliasHelper,
|
||||
newRoomAddress = createRoomConfig.roomVisibility.roomAddress().getOrDefault(""),
|
||||
newRoomAddress = createRoomConfig.visibilityState.roomAddress().getOrDefault(""),
|
||||
knownRoomAddress = null,
|
||||
) { newRoomAddressValidity ->
|
||||
roomAddressValidity.value = newRoomAddressValidity
|
||||
@@ -124,12 +143,25 @@ class ConfigureRoomPresenter(
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
val createRoomAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
|
||||
val availableVisibilityOptions = remember(isSpace, isKnockFeatureEnabled) {
|
||||
listOfNotNull(
|
||||
RoomVisibilityItem.Public,
|
||||
RoomVisibilityItem.AskToJoin.takeIf { !isSpace && isKnockFeatureEnabled },
|
||||
RoomVisibilityItem.Private,
|
||||
).toImmutableList()
|
||||
// Calculate available join rules based:
|
||||
// 1. If we are creating a space.
|
||||
// 2. If it has a parent space.
|
||||
// 3. If knocking is enabled.
|
||||
val parentSpace = createRoomConfig.parentSpace
|
||||
val availableJoinRules = remember(createRoomConfig.parentSpace, isSpace, isKnockFeatureEnabled) {
|
||||
when {
|
||||
isSpace && parentSpace != null -> TODO("Adding a space to a parent space is not supported yet! How did you get here?")
|
||||
parentSpace == null || parentSpace.joinRule == JoinRule.Public -> listOfNotNull(
|
||||
JoinRuleItem.PublicVisibility.Public,
|
||||
JoinRuleItem.PublicVisibility.AskToJoin.takeIf { !isSpace && isKnockFeatureEnabled },
|
||||
JoinRuleItem.Private,
|
||||
).toImmutableList()
|
||||
else -> listOfNotNull(
|
||||
JoinRuleItem.PublicVisibility.Restricted(parentSpace.roomId),
|
||||
JoinRuleItem.PublicVisibility.AskToJoinRestricted(parentSpace.roomId).takeIf { !isSpace && isKnockFeatureEnabled },
|
||||
JoinRuleItem.Private,
|
||||
).toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
fun createRoom(config: CreateRoomConfig) {
|
||||
@@ -141,8 +173,7 @@ class ConfigureRoomPresenter(
|
||||
when (event) {
|
||||
is ConfigureRoomEvents.RoomNameChanged -> dataStore.setRoomName(event.name)
|
||||
is ConfigureRoomEvents.TopicChanged -> dataStore.setTopic(event.topic)
|
||||
is ConfigureRoomEvents.RoomVisibilityChanged -> dataStore.setRoomVisibility(event.visibilityItem)
|
||||
is ConfigureRoomEvents.RoomAccessChanged -> dataStore.setRoomAccess(event.roomAccess)
|
||||
is ConfigureRoomEvents.JoinRuleChanged -> dataStore.setJoinRule(event.joinRuleItem)
|
||||
is ConfigureRoomEvents.RoomAddressChanged -> dataStore.setRoomAddress(event.roomAddress)
|
||||
is ConfigureRoomEvents.CreateRoom -> createRoom(createRoomConfig)
|
||||
is ConfigureRoomEvents.HandleAvatarAction -> {
|
||||
@@ -157,8 +188,12 @@ class ConfigureRoomPresenter(
|
||||
AvatarAction.Remove -> dataStore.setAvatarUri(uri = null)
|
||||
}
|
||||
}
|
||||
|
||||
ConfigureRoomEvents.CancelCreateRoom -> createRoomAction.value = AsyncAction.Uninitialized
|
||||
is ConfigureRoomEvents.SetParentSpace -> {
|
||||
dataStore.setParentSpace(event.space)
|
||||
}
|
||||
ConfigureRoomEvents.CancelCreateRoom -> {
|
||||
createRoomAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +204,8 @@ class ConfigureRoomPresenter(
|
||||
cameraPermissionState = cameraPermissionState,
|
||||
homeserverName = homeserverName,
|
||||
roomAddressValidity = roomAddressValidity.value,
|
||||
availableVisibilityOptions = availableVisibilityOptions,
|
||||
availableJoinRules = availableJoinRules,
|
||||
spaces = spaces,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
@@ -180,25 +216,27 @@ class ConfigureRoomPresenter(
|
||||
) = launch {
|
||||
suspend {
|
||||
val avatarUrl = config.avatarUri?.let { uploadAvatar(it.toUri()) }
|
||||
val params = if (config.roomVisibility is RoomVisibilityState.Public) {
|
||||
val params = if (config.visibilityState is RoomVisibilityState.Public) {
|
||||
CreateRoomParameters(
|
||||
name = config.roomName,
|
||||
topic = config.topic,
|
||||
isEncrypted = false,
|
||||
isDirect = false,
|
||||
visibility = RoomVisibility.Public,
|
||||
joinRuleOverride = config.roomVisibility.roomAccess.toJoinRule(),
|
||||
joinRuleOverride = config.visibilityState.joinRuleItem.toJoinRule()
|
||||
// No need to specify the public join rule override, since the preset is already PUBLIC_CHAT
|
||||
.takeIf { it != JoinRule.Public },
|
||||
preset = RoomPreset.PUBLIC_CHAT,
|
||||
invite = config.invites.map { it.userId },
|
||||
avatar = avatarUrl,
|
||||
roomAliasName = config.roomVisibility.roomAddress(),
|
||||
roomAliasName = config.visibilityState.roomAddress(),
|
||||
isSpace = isSpace,
|
||||
)
|
||||
} else {
|
||||
CreateRoomParameters(
|
||||
name = config.roomName,
|
||||
topic = config.topic,
|
||||
isEncrypted = config.roomVisibility is RoomVisibilityState.Private,
|
||||
isEncrypted = config.visibilityState is RoomVisibilityState.Private,
|
||||
isDirect = false,
|
||||
visibility = RoomVisibility.Private,
|
||||
historyVisibilityOverride = RoomHistoryVisibility.Invited,
|
||||
@@ -208,7 +246,7 @@ class ConfigureRoomPresenter(
|
||||
isSpace = isSpace,
|
||||
)
|
||||
}
|
||||
matrixClient.createRoom(params)
|
||||
val roomId = matrixClient.createRoom(params)
|
||||
.onFailure { failure ->
|
||||
Timber.e(failure, "Failed to create room")
|
||||
}
|
||||
@@ -217,7 +255,22 @@ class ConfigureRoomPresenter(
|
||||
analyticsService.capture(CreatedRoom(isDM = false))
|
||||
}
|
||||
.getOrThrow()
|
||||
|
||||
// Add the newly created room to the parent space too
|
||||
if (config.parentSpace != null) {
|
||||
Timber.d("Adding room $roomId to parent space ${config.parentSpace.roomId}")
|
||||
// Wait until we receive the power level info for the room, as it's needed to check if it can be added to a space
|
||||
// TODO create some SDK function that does this instead?
|
||||
withTimeoutOrNull(30.seconds) {
|
||||
matrixClient.getRoomInfoFlow(roomId).first { it.getOrNull()?.roomPowerLevels != null }
|
||||
} ?: error("Did not receive created room power levels for room $roomId, needed for adding it to a space")
|
||||
|
||||
matrixClient.spaceService.addChildToSpace(spaceId = config.parentSpace.roomId, childId = roomId).getOrThrow()
|
||||
}
|
||||
|
||||
roomId
|
||||
}.runCatchingUpdatingState(createRoomAction)
|
||||
.onFailure { Timber.e(it, "Could not create room or add it to parent space ${config.parentSpace?.roomId}") }
|
||||
}
|
||||
|
||||
private suspend fun uploadAvatar(avatarUri: Uri): String {
|
||||
|
||||
@@ -10,6 +10,7 @@ package io.element.android.features.createroom.impl.configureroom
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
|
||||
import io.element.android.libraries.permissions.api.PermissionsState
|
||||
@@ -22,9 +23,10 @@ data class ConfigureRoomState(
|
||||
val cameraPermissionState: PermissionsState,
|
||||
val roomAddressValidity: RoomAddressValidity,
|
||||
val homeserverName: String,
|
||||
val availableVisibilityOptions: ImmutableList<RoomVisibilityItem>,
|
||||
val availableJoinRules: ImmutableList<JoinRuleItem>,
|
||||
val spaces: ImmutableList<SpaceRoom>,
|
||||
val eventSink: (ConfigureRoomEvents) -> Unit
|
||||
) {
|
||||
val isValid: Boolean = config.roomName?.isNotEmpty() == true &&
|
||||
(config.roomVisibility is RoomVisibilityState.Private || roomAddressValidity == RoomAddressValidity.Valid)
|
||||
(config.visibilityState is RoomVisibilityState.Private || roomAddressValidity == RoomAddressValidity.Valid)
|
||||
}
|
||||
|
||||
@@ -10,12 +10,15 @@ package io.element.android.features.createroom.impl.configureroom
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
|
||||
import io.element.android.libraries.permissions.api.PermissionsState
|
||||
import io.element.android.libraries.permissions.api.aPermissionsState
|
||||
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomState> {
|
||||
@@ -28,9 +31,9 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
||||
roomName = "Room 101",
|
||||
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
||||
invites = aMatrixUserList().toImmutableList(),
|
||||
roomVisibility = RoomVisibilityState.Public(
|
||||
visibilityState = RoomVisibilityState.Public(
|
||||
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
||||
roomAccess = RoomAccess.Knocking,
|
||||
joinRuleItem = JoinRuleItem.PublicVisibility.AskToJoin,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -39,9 +42,9 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
||||
roomName = "Room 101",
|
||||
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
||||
invites = aMatrixUserList().toImmutableList(),
|
||||
roomVisibility = RoomVisibilityState.Public(
|
||||
visibilityState = RoomVisibilityState.Public(
|
||||
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
||||
roomAccess = RoomAccess.Knocking,
|
||||
joinRuleItem = JoinRuleItem.PublicVisibility.AskToJoin,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -49,9 +52,9 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
||||
config = CreateRoomConfig(
|
||||
roomName = "Room 101",
|
||||
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
||||
roomVisibility = RoomVisibilityState.Public(
|
||||
visibilityState = RoomVisibilityState.Public(
|
||||
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
||||
roomAccess = RoomAccess.Knocking,
|
||||
joinRuleItem = JoinRuleItem.PublicVisibility.AskToJoin,
|
||||
),
|
||||
),
|
||||
roomAddressValidity = RoomAddressValidity.NotAvailable,
|
||||
@@ -60,9 +63,9 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
||||
config = CreateRoomConfig(
|
||||
roomName = "Room 101",
|
||||
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
||||
roomVisibility = RoomVisibilityState.Public(
|
||||
visibilityState = RoomVisibilityState.Public(
|
||||
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
||||
roomAccess = RoomAccess.Knocking,
|
||||
joinRuleItem = JoinRuleItem.PublicVisibility.AskToJoin,
|
||||
),
|
||||
),
|
||||
roomAddressValidity = RoomAddressValidity.InvalidSymbols,
|
||||
@@ -71,9 +74,9 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
||||
config = CreateRoomConfig(
|
||||
roomName = "Room 101",
|
||||
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
||||
roomVisibility = RoomVisibilityState.Public(
|
||||
visibilityState = RoomVisibilityState.Public(
|
||||
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
||||
roomAccess = RoomAccess.Knocking,
|
||||
joinRuleItem = JoinRuleItem.PublicVisibility.AskToJoin,
|
||||
),
|
||||
),
|
||||
roomAddressValidity = RoomAddressValidity.Valid,
|
||||
@@ -83,13 +86,41 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
||||
isSpace = true,
|
||||
roomName = "Space 101",
|
||||
topic = "Space topic for this space when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
||||
roomVisibility = RoomVisibilityState.Public(
|
||||
visibilityState = RoomVisibilityState.Public(
|
||||
roomAddress = RoomAddress.AutoFilled("Space-101"),
|
||||
roomAccess = RoomAccess.Anyone,
|
||||
joinRuleItem = JoinRuleItem.PublicVisibility.Public,
|
||||
),
|
||||
),
|
||||
roomAddressValidity = RoomAddressValidity.Valid,
|
||||
),
|
||||
aConfigureRoomState(
|
||||
config = CreateRoomConfig(
|
||||
isSpace = false,
|
||||
roomName = "Room 101",
|
||||
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
||||
parentSpace = null,
|
||||
visibilityState = RoomVisibilityState.Public(
|
||||
roomAddress = RoomAddress.AutoFilled("Space-101"),
|
||||
joinRuleItem = JoinRuleItem.PublicVisibility.Restricted(aSpaceRoom().roomId),
|
||||
),
|
||||
),
|
||||
spaces = listOf(aSpaceRoom()),
|
||||
roomAddressValidity = RoomAddressValidity.Valid,
|
||||
),
|
||||
aConfigureRoomState(
|
||||
config = CreateRoomConfig(
|
||||
isSpace = false,
|
||||
roomName = "Room 101",
|
||||
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
||||
parentSpace = aSpaceRoom(canonicalAlias = RoomAlias("#a-space-room:example.org")),
|
||||
visibilityState = RoomVisibilityState.Public(
|
||||
roomAddress = RoomAddress.AutoFilled("Space-101"),
|
||||
joinRuleItem = JoinRuleItem.PublicVisibility.Restricted(aSpaceRoom().roomId),
|
||||
),
|
||||
),
|
||||
spaces = listOf(aSpaceRoom()),
|
||||
roomAddressValidity = RoomAddressValidity.Valid,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -101,9 +132,20 @@ fun aConfigureRoomState(
|
||||
cameraPermissionState: PermissionsState = aPermissionsState(showDialog = false),
|
||||
homeserverName: String = "matrix.org",
|
||||
roomAddressValidity: RoomAddressValidity = RoomAddressValidity.Valid,
|
||||
availableVisibilityOptions: List<RoomVisibilityItem> = RoomVisibilityItem.entries.filter {
|
||||
if (!isKnockFeatureEnabled) it != RoomVisibilityItem.AskToJoin else true
|
||||
availableVisibilityOptions: List<JoinRuleItem> = if (config.parentSpace != null) {
|
||||
listOfNotNull(
|
||||
JoinRuleItem.PublicVisibility.Restricted(config.parentSpace.roomId),
|
||||
JoinRuleItem.PublicVisibility.AskToJoinRestricted(config.parentSpace.roomId).takeIf { isKnockFeatureEnabled },
|
||||
JoinRuleItem.Private,
|
||||
)
|
||||
} else {
|
||||
listOfNotNull(
|
||||
JoinRuleItem.PublicVisibility.Public,
|
||||
JoinRuleItem.PublicVisibility.AskToJoin.takeIf { isKnockFeatureEnabled },
|
||||
JoinRuleItem.Private,
|
||||
)
|
||||
},
|
||||
spaces: List<SpaceRoom> = emptyList(),
|
||||
eventSink: (ConfigureRoomEvents) -> Unit = { },
|
||||
) = ConfigureRoomState(
|
||||
config = config,
|
||||
@@ -112,6 +154,7 @@ fun aConfigureRoomState(
|
||||
cameraPermissionState = cameraPermissionState,
|
||||
homeserverName = homeserverName,
|
||||
roomAddressValidity = roomAddressValidity,
|
||||
availableVisibilityOptions = availableVisibilityOptions.toImmutableList(),
|
||||
availableJoinRules = availableVisibilityOptions.toImmutableList(),
|
||||
spaces = spaces.toImmutableList(),
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
@@ -35,6 +35,7 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
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.features.createroom.impl.R
|
||||
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom
|
||||
@@ -46,7 +47,6 @@ 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
|
||||
@@ -59,6 +59,7 @@ 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.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet
|
||||
import io.element.android.libraries.matrix.ui.components.AvatarPickerState
|
||||
import io.element.android.libraries.matrix.ui.components.AvatarPickerView
|
||||
@@ -120,27 +121,30 @@ fun ConfigureRoomView(
|
||||
onTopicChange = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) },
|
||||
)
|
||||
|
||||
RoomVisibilityAndAccessOptions(
|
||||
options = state.availableVisibilityOptions,
|
||||
selected = when (state.config.roomVisibility) {
|
||||
is RoomVisibilityState.Private -> RoomVisibilityItem.Private
|
||||
is RoomVisibilityState.Public -> when (state.config.roomVisibility.roomAccess) {
|
||||
RoomAccess.Knocking -> RoomVisibilityItem.AskToJoin
|
||||
RoomAccess.Anyone -> RoomVisibilityItem.Public
|
||||
}
|
||||
},
|
||||
if (!state.config.isSpace && state.spaces.isNotEmpty()) {
|
||||
SelectParentSpaceOptions(
|
||||
spaces = state.spaces,
|
||||
selectedSpace = state.config.parentSpace,
|
||||
onSelectSpace = { state.eventSink(ConfigureRoomEvents.SetParentSpace(it)) },
|
||||
)
|
||||
}
|
||||
|
||||
RoomJoinRuleOptions(
|
||||
options = state.availableJoinRules,
|
||||
selected = state.config.visibilityState.joinRuleItem,
|
||||
parentSpace = state.config.parentSpace,
|
||||
onOptionClick = {
|
||||
focusManager.clearFocus()
|
||||
state.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(it))
|
||||
state.eventSink(ConfigureRoomEvents.JoinRuleChanged(it))
|
||||
},
|
||||
)
|
||||
|
||||
if (state.config.roomVisibility !is RoomVisibilityState.Private) {
|
||||
if (state.config.visibilityState !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(),
|
||||
address = state.config.visibilityState.roomAddress().getOrNull().orEmpty(),
|
||||
homeserverName = state.homeserverName,
|
||||
addressValidity = state.roomAddressValidity,
|
||||
onAddressChange = { state.eventSink(ConfigureRoomEvents.RoomAddressChanged(it)) },
|
||||
@@ -265,7 +269,7 @@ private fun RoomTopic(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConfigureRoomOptions(
|
||||
internal fun ConfigureRoomOptions(
|
||||
title: String,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
@@ -279,10 +283,11 @@ private fun ConfigureRoomOptions(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RoomVisibilityAndAccessOptions(
|
||||
options: ImmutableList<RoomVisibilityItem>,
|
||||
selected: RoomVisibilityItem,
|
||||
onOptionClick: (RoomVisibilityItem) -> Unit,
|
||||
private fun RoomJoinRuleOptions(
|
||||
options: ImmutableList<JoinRuleItem>,
|
||||
selected: JoinRuleItem,
|
||||
onOptionClick: (JoinRuleItem) -> Unit,
|
||||
parentSpace: SpaceRoom?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ConfigureRoomOptions(
|
||||
@@ -295,10 +300,12 @@ private fun RoomVisibilityAndAccessOptions(
|
||||
leadingContent = ListItemContent.Custom {
|
||||
RoundedIconAtom(
|
||||
size = RoundedIconAtomSize.Big,
|
||||
resourceId = when (item) {
|
||||
RoomVisibilityItem.Public -> CompoundDrawables.ic_compound_public
|
||||
RoomVisibilityItem.AskToJoin -> CompoundDrawables.ic_compound_user_add
|
||||
RoomVisibilityItem.Private -> CompoundDrawables.ic_compound_lock
|
||||
imageVector = when (item) {
|
||||
JoinRuleItem.PublicVisibility.Public -> CompoundIcons.Public()
|
||||
is JoinRuleItem.PublicVisibility.Restricted -> CompoundIcons.Space()
|
||||
JoinRuleItem.PublicVisibility.AskToJoin,
|
||||
is JoinRuleItem.PublicVisibility.AskToJoinRestricted -> CompoundIcons.UserAdd()
|
||||
JoinRuleItem.Private -> CompoundIcons.Lock()
|
||||
},
|
||||
tint = if (isSelected) ElementTheme.colors.iconPrimary else ElementTheme.colors.iconSecondary,
|
||||
backgroundTint = Color.Transparent,
|
||||
@@ -306,18 +313,29 @@ private fun RoomVisibilityAndAccessOptions(
|
||||
},
|
||||
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)
|
||||
JoinRuleItem.PublicVisibility.Public -> stringResource(R.string.screen_create_room_room_access_section_public_option_title)
|
||||
is JoinRuleItem.PublicVisibility.Restricted -> stringResource(R.string.screen_create_room_room_access_section_restricted_option_title)
|
||||
JoinRuleItem.PublicVisibility.AskToJoin -> stringResource(R.string.screen_create_room_room_access_section_knocking_option_title)
|
||||
is JoinRuleItem.PublicVisibility.AskToJoinRestricted -> stringResource(
|
||||
R.string.screen_create_room_room_access_section_knocking_restricted_option_title
|
||||
)
|
||||
JoinRuleItem.Private -> stringResource(R.string.screen_create_room_room_access_section_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)
|
||||
JoinRuleItem.PublicVisibility.Public -> stringResource(R.string.screen_create_room_room_access_section_public_option_description)
|
||||
is JoinRuleItem.PublicVisibility.Restricted -> stringResource(
|
||||
R.string.screen_create_room_room_access_section_restricted_option_description,
|
||||
parentSpace?.displayName.orEmpty()
|
||||
)
|
||||
JoinRuleItem.PublicVisibility.AskToJoin -> stringResource(R.string.screen_create_room_room_access_section_knocking_option_description)
|
||||
is JoinRuleItem.PublicVisibility.AskToJoinRestricted -> stringResource(
|
||||
R.string.screen_create_room_room_access_section_knocking_restricted_option_description,
|
||||
parentSpace?.displayName.orEmpty()
|
||||
)
|
||||
JoinRuleItem.Private -> stringResource(R.string.screen_create_room_room_access_section_private_option_description)
|
||||
}
|
||||
Text(text = description)
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
package io.element.android.features.createroom.impl.configureroom
|
||||
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
@@ -18,5 +19,6 @@ data class CreateRoomConfig(
|
||||
val topic: String? = null,
|
||||
val avatarUri: String? = null,
|
||||
val invites: ImmutableList<MatrixUser> = persistentListOf(),
|
||||
val roomVisibility: RoomVisibilityState = RoomVisibilityState.Private,
|
||||
val visibilityState: RoomVisibilityState = RoomVisibilityState.Private(),
|
||||
val parentSpace: SpaceRoom? = null,
|
||||
)
|
||||
|
||||
@@ -12,6 +12,7 @@ import android.net.Uri
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.androidutils.file.safeDelete
|
||||
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.getAndUpdate
|
||||
@@ -33,23 +34,20 @@ class CreateRoomConfigStore(
|
||||
|
||||
fun setRoomName(roomName: String) {
|
||||
createRoomConfigFlow.getAndUpdate { config ->
|
||||
val newVisibility = when (config.roomVisibility) {
|
||||
is RoomVisibilityState.Public -> {
|
||||
val roomAddress = config.roomVisibility.roomAddress
|
||||
if (roomAddress is RoomAddress.AutoFilled || roomName.isEmpty()) {
|
||||
val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(roomName)
|
||||
config.roomVisibility.copy(
|
||||
roomAddress = RoomAddress.AutoFilled(roomAliasName),
|
||||
)
|
||||
} else {
|
||||
config.roomVisibility
|
||||
}
|
||||
val roomAccessWithNewAddress = if (config.visibilityState is RoomVisibilityState.Public) {
|
||||
val roomAddress = config.visibilityState.roomAddress
|
||||
if (roomAddress is RoomAddress.AutoFilled || roomName.isEmpty()) {
|
||||
val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(roomName)
|
||||
config.visibilityState.copy(roomAddress = RoomAddress.AutoFilled(roomAliasName))
|
||||
} else {
|
||||
config.visibilityState
|
||||
}
|
||||
else -> config.roomVisibility
|
||||
} else {
|
||||
config.visibilityState
|
||||
}
|
||||
config.copy(
|
||||
roomName = roomName.takeIf { it.isNotEmpty() },
|
||||
roomVisibility = newVisibility,
|
||||
visibilityState = roomAccessWithNewAddress,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -67,16 +65,19 @@ class CreateRoomConfigStore(
|
||||
}
|
||||
}
|
||||
|
||||
fun setRoomVisibility(visibility: RoomVisibilityItem) {
|
||||
/**
|
||||
* Sets both the room visibility and its access based on the provided join rule.
|
||||
*/
|
||||
fun setJoinRule(joinRule: JoinRuleItem) {
|
||||
createRoomConfigFlow.getAndUpdate { config ->
|
||||
config.copy(
|
||||
roomVisibility = when (visibility) {
|
||||
RoomVisibilityItem.Private -> RoomVisibilityState.Private
|
||||
RoomVisibilityItem.Public, RoomVisibilityItem.AskToJoin -> {
|
||||
visibilityState = when (joinRule) {
|
||||
JoinRuleItem.Private -> RoomVisibilityState.Private()
|
||||
is JoinRuleItem.PublicVisibility -> {
|
||||
val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(config.roomName.orEmpty())
|
||||
RoomVisibilityState.Public(
|
||||
roomAddress = RoomAddress.AutoFilled(roomAliasName),
|
||||
roomAccess = if (visibility == RoomVisibilityItem.AskToJoin) RoomAccess.Knocking else RoomAccess.Anyone,
|
||||
joinRuleItem = joinRule,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -87,28 +88,12 @@ class CreateRoomConfigStore(
|
||||
fun setRoomAddress(address: String) {
|
||||
createRoomConfigFlow.getAndUpdate { config ->
|
||||
config.copy(
|
||||
roomVisibility = when (config.roomVisibility) {
|
||||
visibilityState = when (config.visibilityState) {
|
||||
is RoomVisibilityState.Public -> {
|
||||
val sanitizedAddress = address.lowercase()
|
||||
config.roomVisibility.copy(roomAddress = RoomAddress.Edited(sanitizedAddress))
|
||||
config.visibilityState.copy(roomAddress = RoomAddress.Edited(sanitizedAddress))
|
||||
}
|
||||
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
|
||||
else -> config.visibilityState
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -120,6 +105,15 @@ class CreateRoomConfigStore(
|
||||
}
|
||||
}
|
||||
|
||||
fun setParentSpace(parentSpace: SpaceRoom?) {
|
||||
createRoomConfigFlow.getAndUpdate { config ->
|
||||
config.copy(
|
||||
parentSpace = parentSpace,
|
||||
visibilityState = RoomVisibilityState.Private(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearCachedData() {
|
||||
cachedAvatarUri = null
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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.features.createroom.impl.configureroom
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.join.AllowRule
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
/**
|
||||
* Join rule items to display in UI.
|
||||
*/
|
||||
@Immutable
|
||||
sealed interface JoinRuleItem {
|
||||
data object Private : JoinRuleItem
|
||||
|
||||
/**
|
||||
* Those join rule items that represent public visibility of the room/space.
|
||||
*/
|
||||
@Immutable
|
||||
sealed interface PublicVisibility : JoinRuleItem {
|
||||
data object Public : PublicVisibility
|
||||
data object AskToJoin : PublicVisibility
|
||||
data class Restricted(val parentSpaceId: RoomId) : PublicVisibility
|
||||
data class AskToJoinRestricted(val parentSpaceId: RoomId) : PublicVisibility
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a [JoinRuleItem] option into a [JoinRule].
|
||||
*/
|
||||
fun toJoinRule(): JoinRule = when (this) {
|
||||
Private -> JoinRule.Private
|
||||
PublicVisibility.Public -> JoinRule.Public
|
||||
PublicVisibility.AskToJoin -> JoinRule.Knock
|
||||
is PublicVisibility.Restricted -> JoinRule.Restricted(persistentListOf(AllowRule.RoomMembership(parentSpaceId)))
|
||||
is PublicVisibility.AskToJoinRestricted -> JoinRule.KnockRestricted(persistentListOf(AllowRule.RoomMembership(parentSpaceId)))
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 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.features.createroom.impl.configureroom
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
|
||||
enum class RoomAccess {
|
||||
Anyone,
|
||||
Knocking
|
||||
}
|
||||
|
||||
fun RoomAccess.toJoinRule(): JoinRule? {
|
||||
return when (this) {
|
||||
RoomAccess.Anyone -> null
|
||||
RoomAccess.Knocking -> JoinRule.Knock
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 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.features.createroom.impl.configureroom
|
||||
|
||||
enum class RoomAccessItem {
|
||||
Anyone,
|
||||
AskToJoin,
|
||||
}
|
||||
@@ -1,15 +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.features.createroom.impl.configureroom
|
||||
|
||||
enum class RoomVisibilityItem {
|
||||
Public,
|
||||
AskToJoin,
|
||||
Private
|
||||
}
|
||||
@@ -11,11 +11,12 @@ package io.element.android.features.createroom.impl.configureroom
|
||||
import java.util.Optional
|
||||
|
||||
sealed interface RoomVisibilityState {
|
||||
data object Private : RoomVisibilityState
|
||||
val joinRuleItem: JoinRuleItem
|
||||
data class Private(override val joinRuleItem: JoinRuleItem.Private = JoinRuleItem.Private) : RoomVisibilityState
|
||||
|
||||
data class Public(
|
||||
val roomAddress: RoomAddress,
|
||||
val roomAccess: RoomAccess,
|
||||
override val joinRuleItem: JoinRuleItem.PublicVisibility,
|
||||
) : RoomVisibilityState
|
||||
|
||||
fun roomAddress(): Optional<String> {
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* 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.features.createroom.impl.configureroom
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.createroom.impl.R
|
||||
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.components.list.ListItemContent
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||
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.ModalBottomSheet
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.hide
|
||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun SelectParentSpaceOptions(
|
||||
spaces: ImmutableList<SpaceRoom>,
|
||||
selectedSpace: SpaceRoom?,
|
||||
onSelectSpace: (SpaceRoom?) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var displaySelectSpaceBottomSheet by remember { mutableStateOf(false) }
|
||||
ConfigureRoomOptions(
|
||||
title = stringResource(CommonStrings.common_space),
|
||||
modifier = modifier
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = selectedSpace?.displayName
|
||||
?: stringResource(R.string.screen_create_room_space_selection_no_space_title),
|
||||
maxLines = 1
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
text = if (selectedSpace != null) {
|
||||
selectedSpace.canonicalAlias?.value.orEmpty()
|
||||
} else {
|
||||
stringResource(R.string.screen_create_room_space_selection_no_space_description)
|
||||
},
|
||||
maxLines = 1
|
||||
)
|
||||
},
|
||||
leadingContent = if (selectedSpace == null) {
|
||||
ListItemContent.Icon(IconSource.Vector(CompoundIcons.Home()))
|
||||
} else {
|
||||
ListItemContent.Custom({
|
||||
val avatarData = AvatarData(
|
||||
id = selectedSpace.roomId.value,
|
||||
name = selectedSpace.displayName,
|
||||
url = selectedSpace.avatarUrl,
|
||||
size = AvatarSize.SelectParentSpace,
|
||||
)
|
||||
Avatar(avatarData = avatarData, avatarType = AvatarType.Space())
|
||||
})
|
||||
},
|
||||
onClick = { displaySelectSpaceBottomSheet = true }
|
||||
)
|
||||
|
||||
if (displaySelectSpaceBottomSheet) {
|
||||
val sheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true,
|
||||
confirmValueChange = { true },
|
||||
)
|
||||
ModalBottomSheet(
|
||||
sheetState = sheetState,
|
||||
onDismissRequest = {
|
||||
sheetState.hide(coroutineScope) {
|
||||
displaySelectSpaceBottomSheet = false
|
||||
}
|
||||
}
|
||||
) {
|
||||
SelectParentSpaceBottomSheet(
|
||||
spaces = spaces,
|
||||
selectedSpace = selectedSpace,
|
||||
) {
|
||||
sheetState.hide(coroutineScope) {
|
||||
displaySelectSpaceBottomSheet = false
|
||||
}
|
||||
onSelectSpace(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.SelectParentSpaceBottomSheet(
|
||||
spaces: ImmutableList<SpaceRoom>,
|
||||
selectedSpace: SpaceRoom?,
|
||||
onSelectSpace: (SpaceRoom?) -> Unit,
|
||||
) {
|
||||
ListSectionHeader(
|
||||
title = stringResource(R.string.screen_create_room_space_selection_sheet_title),
|
||||
hasDivider = false
|
||||
)
|
||||
LazyColumn(modifier = Modifier.fillMaxWidth()) {
|
||||
item {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
stringResource(R.string.screen_create_room_space_selection_no_space_title),
|
||||
maxLines = 1
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
stringResource(R.string.screen_create_room_space_selection_no_space_description),
|
||||
maxLines = 1
|
||||
)
|
||||
},
|
||||
leadingContent = ListItemContent.Icon(
|
||||
IconSource.Vector(CompoundIcons.Home())
|
||||
),
|
||||
trailingContent = ListItemContent.RadioButton(
|
||||
selected = selectedSpace == null
|
||||
),
|
||||
onClick = { onSelectSpace(null) },
|
||||
)
|
||||
}
|
||||
for (space in spaces) {
|
||||
item {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
space.displayName,
|
||||
maxLines = 1
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
space.canonicalAlias?.value.orEmpty(),
|
||||
maxLines = 1
|
||||
)
|
||||
},
|
||||
leadingContent = ListItemContent.Custom({
|
||||
val avatarData =
|
||||
AvatarData(
|
||||
id = space.roomId.value,
|
||||
name = space.displayName,
|
||||
url = space.avatarUrl,
|
||||
size = AvatarSize.SelectParentSpace,
|
||||
)
|
||||
Avatar(
|
||||
avatarData = avatarData,
|
||||
avatarType = AvatarType.Space()
|
||||
)
|
||||
}),
|
||||
trailingContent = ListItemContent.RadioButton(
|
||||
selected = selectedSpace == space
|
||||
),
|
||||
onClick = { onSelectSpace(space) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun SelectParentSpaceBottomSheetPreview() =
|
||||
ElementPreview {
|
||||
Column {
|
||||
SelectParentSpaceBottomSheet(
|
||||
spaces = persistentListOf(
|
||||
aSpaceRoom(
|
||||
canonicalAlias = RoomAlias(
|
||||
"#a-room-alias:example.org"
|
||||
)
|
||||
)
|
||||
),
|
||||
selectedSpace = null,
|
||||
) {}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
* 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.features.startchat.impl.configureroom
|
||||
package io.element.android.features.createroom.impl
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
@@ -18,24 +17,28 @@ import io.element.android.features.createroom.impl.configureroom.ConfigureRoomPr
|
||||
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomState
|
||||
import io.element.android.features.createroom.impl.configureroom.CreateRoomConfig
|
||||
import io.element.android.features.createroom.impl.configureroom.CreateRoomConfigStore
|
||||
import io.element.android.features.createroom.impl.configureroom.RoomAccess
|
||||
import io.element.android.features.createroom.impl.configureroom.JoinRuleItem
|
||||
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.libraries.architecture.AsyncAction
|
||||
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.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.RoomInfo
|
||||
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
||||
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues
|
||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.matrix.test.A_MESSAGE
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
import io.element.android.libraries.matrix.test.room.alias.FakeRoomAliasHelper
|
||||
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
|
||||
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
|
||||
import io.element.android.libraries.mediapickers.api.PickerProvider
|
||||
@@ -47,12 +50,17 @@ import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
|
||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
||||
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.test
|
||||
import io.mockk.mockk
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
@@ -81,7 +89,7 @@ class ConfigureRoomPresenterTest {
|
||||
assertThat(initialState.config.topic).isNull()
|
||||
assertThat(initialState.config.invites).isEmpty()
|
||||
assertThat(initialState.config.avatarUri).isNull()
|
||||
assertThat(initialState.config.roomVisibility).isEqualTo(RoomVisibilityState.Private)
|
||||
assertThat(initialState.config.visibilityState).isEqualTo(RoomVisibilityState.Private())
|
||||
assertThat(initialState.createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||
assertThat(initialState.homeserverName).isEqualTo("matrix.org")
|
||||
}
|
||||
@@ -174,12 +182,12 @@ class ConfigureRoomPresenterTest {
|
||||
assertThat(newState.config).isEqualTo(expectedConfig)
|
||||
|
||||
// Room privacy
|
||||
newState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
|
||||
newState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
||||
newState = awaitItem()
|
||||
expectedConfig = expectedConfig.copy(
|
||||
roomVisibility = RoomVisibilityState.Public(
|
||||
visibilityState = RoomVisibilityState.Public(
|
||||
roomAddress = RoomAddress.AutoFilled(roomAliasHelper.roomAliasNameFromRoomDisplayName(expectedConfig.roomName ?: "")),
|
||||
roomAccess = RoomAccess.Anyone,
|
||||
joinRuleItem = JoinRuleItem.PublicVisibility.Public,
|
||||
)
|
||||
)
|
||||
assertThat(newState.config).isEqualTo(expectedConfig)
|
||||
@@ -206,6 +214,109 @@ class ConfigureRoomPresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - when creating a room in a space if the room doesn't receive the power levels value it can't be added to the space`() = runTest {
|
||||
val addChildToSpaceResult = lambdaRecorder<RoomId, RoomId, Result<Unit>> { _, _ -> Result.success(Unit) }
|
||||
val spaceService = FakeSpaceService(
|
||||
addChildToSpaceResult = addChildToSpaceResult,
|
||||
)
|
||||
val roomInfoFlow = MutableStateFlow<Optional<RoomInfo>>(Optional.empty())
|
||||
val getRoomInfoFlowLambda = lambdaRecorder<RoomId, Flow<Optional<RoomInfo>>> { roomInfoFlow }
|
||||
val matrixClient = createMatrixClient(spaceService = spaceService).apply {
|
||||
this.getRoomInfoFlowLambda = getRoomInfoFlowLambda
|
||||
}
|
||||
val presenter = createConfigureRoomPresenter(
|
||||
matrixClient = matrixClient
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = initialState()
|
||||
val createRoomResult = Result.success(RoomId("!createRoomResult:domain"))
|
||||
|
||||
matrixClient.givenCreateRoomResult(createRoomResult)
|
||||
|
||||
val parentSpace = aSpaceRoom()
|
||||
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(parentSpace))
|
||||
assertThat(awaitItem().config.parentSpace).isEqualTo(parentSpace)
|
||||
|
||||
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.AskToJoin))
|
||||
assertThat(awaitItem().config.visibilityState.joinRuleItem).isEqualTo(JoinRuleItem.PublicVisibility.AskToJoin)
|
||||
|
||||
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
|
||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
val stateAfterCreateRoom = awaitItem()
|
||||
|
||||
// getRoomInfoFlow is called, but it contains no updates
|
||||
getRoomInfoFlowLambda.assertions().isCalledOnce()
|
||||
// So adding the child room to the parent space is never done
|
||||
addChildToSpaceResult.assertions().isNeverCalled()
|
||||
|
||||
// And the operation fails
|
||||
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - creating a room and adding it into a parent space works when all the data is available`() = runTest {
|
||||
val addChildToSpaceResult = lambdaRecorder<RoomId, RoomId, Result<Unit>> { _, _ -> Result.success(Unit) }
|
||||
val spaceService = FakeSpaceService(
|
||||
addChildToSpaceResult = addChildToSpaceResult,
|
||||
)
|
||||
val roomInfoFlow = MutableStateFlow<Optional<RoomInfo>>(Optional.empty())
|
||||
val getRoomInfoFlowLambda = lambdaRecorder<RoomId, Flow<Optional<RoomInfo>>> { roomInfoFlow }
|
||||
val matrixClient = createMatrixClient(spaceService = spaceService).apply {
|
||||
this.getRoomInfoFlowLambda = getRoomInfoFlowLambda
|
||||
}
|
||||
val presenter = createConfigureRoomPresenter(
|
||||
matrixClient = matrixClient
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = initialState()
|
||||
val createRoomResult = Result.success(RoomId("!createRoomResult:domain"))
|
||||
|
||||
matrixClient.givenCreateRoomResult(createRoomResult)
|
||||
|
||||
val parentSpace = aSpaceRoom()
|
||||
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(parentSpace))
|
||||
assertThat(awaitItem().config.parentSpace).isEqualTo(parentSpace)
|
||||
|
||||
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.AskToJoin))
|
||||
assertThat(awaitItem().config.visibilityState.joinRuleItem).isEqualTo(JoinRuleItem.PublicVisibility.AskToJoin)
|
||||
|
||||
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
|
||||
|
||||
// We immediately receive the room power levels info needed for adding the child to a space
|
||||
val powerLevels = RoomPowerLevels(
|
||||
RoomPowerLevelsValues(
|
||||
ban = 0,
|
||||
invite = 0,
|
||||
kick = 0,
|
||||
eventsDefault = 0,
|
||||
stateDefault = 0,
|
||||
redactEvents = 0,
|
||||
roomName = 0,
|
||||
roomAvatar = 0,
|
||||
roomTopic = 0,
|
||||
spaceChild = 0
|
||||
),
|
||||
users = persistentMapOf(),
|
||||
)
|
||||
roomInfoFlow.value = Optional.of(aRoomInfo(roomPowerLevels = powerLevels))
|
||||
|
||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
|
||||
val stateAfterCreateRoom = awaitItem()
|
||||
|
||||
// The room info flow was read
|
||||
getRoomInfoFlowLambda.assertions().isCalledOnce()
|
||||
// Since it contained the power levels, the operation continued
|
||||
addChildToSpaceResult.assertions().isCalledOnce()
|
||||
|
||||
// And the child room was created and then added to the parent space
|
||||
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||
assertThat(stateAfterCreateRoom.createRoomAction.dataOrNull()).isEqualTo(createRoomResult.getOrNull())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - record analytics when creating room`() = runTest {
|
||||
val matrixClient = createMatrixClient()
|
||||
@@ -311,7 +422,7 @@ class ConfigureRoomPresenterTest {
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = initialState()
|
||||
initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
|
||||
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
||||
skipItems(1)
|
||||
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("invalid address"))
|
||||
skipItems(1)
|
||||
@@ -331,7 +442,7 @@ class ConfigureRoomPresenterTest {
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = initialState()
|
||||
initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
|
||||
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
||||
skipItems(1)
|
||||
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address"))
|
||||
skipItems(1)
|
||||
@@ -351,7 +462,7 @@ class ConfigureRoomPresenterTest {
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = initialState()
|
||||
initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
|
||||
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
||||
skipItems(1)
|
||||
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address"))
|
||||
skipItems(1)
|
||||
@@ -362,12 +473,51 @@ class ConfigureRoomPresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - when a space is selected, the selected join rule is reset to private`() = runTest {
|
||||
val presenter = createConfigureRoomPresenter()
|
||||
presenter.test {
|
||||
val initialState = initialState()
|
||||
|
||||
// First change the join rule to public
|
||||
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
||||
assertThat(awaitItem().config.visibilityState).isInstanceOf(RoomVisibilityState.Public::class.java)
|
||||
|
||||
// Then check changing the parent space resets it to private
|
||||
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(aSpaceRoom()))
|
||||
assertThat(awaitItem().config.visibilityState).isEqualTo(RoomVisibilityState.Private())
|
||||
|
||||
// If we change the join rule back to public
|
||||
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
||||
assertThat(awaitItem().config.visibilityState).isInstanceOf(RoomVisibilityState.Public::class.java)
|
||||
|
||||
// Then remove the parent space, it'll be private again
|
||||
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(null))
|
||||
assertThat(awaitItem().config.visibilityState).isEqualTo(RoomVisibilityState.Private())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - setting a parent space for a space currently throws an error`() = runTest {
|
||||
val presenter = createConfigureRoomPresenter(isSpace = true)
|
||||
presenter.test {
|
||||
val initialState = initialState()
|
||||
|
||||
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(aSpaceRoom()))
|
||||
|
||||
assertThat(awaitError())
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun TurbineTestContext<ConfigureRoomState>.initialState(): ConfigureRoomState {
|
||||
skipItems(1)
|
||||
return awaitItem()
|
||||
}
|
||||
|
||||
private fun createMatrixClient(isAliasAvailable: Boolean = true) = FakeMatrixClient(
|
||||
private fun createMatrixClient(
|
||||
isAliasAvailable: Boolean = true,
|
||||
spaceService: FakeSpaceService = FakeSpaceService(),
|
||||
) = FakeMatrixClient(
|
||||
userIdServerNameLambda = { "matrix.org" },
|
||||
resolveRoomAliasResult = {
|
||||
val resolvedRoomAlias = if (isAliasAvailable) {
|
||||
@@ -376,7 +526,8 @@ class ConfigureRoomPresenterTest {
|
||||
Optional.of(ResolvedRoomAlias(A_ROOM_ID, emptyList()))
|
||||
}
|
||||
Result.success(resolvedRoomAlias)
|
||||
}
|
||||
},
|
||||
spaceService = spaceService,
|
||||
)
|
||||
|
||||
private fun createConfigureRoomPresenter(
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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.features.createroom.impl
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.createroom.impl.configureroom.JoinRuleItem
|
||||
import io.element.android.libraries.matrix.api.room.join.AllowRule
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.junit.Test
|
||||
|
||||
class JoinRuleItemTest {
|
||||
@Test
|
||||
fun `toJoinRule works as expected`() {
|
||||
assertThat(JoinRuleItem.Private.toJoinRule()).isEqualTo(JoinRule.Private)
|
||||
assertThat(JoinRuleItem.PublicVisibility.Public.toJoinRule()).isEqualTo(JoinRule.Public)
|
||||
assertThat(JoinRuleItem.PublicVisibility.AskToJoin.toJoinRule()).isEqualTo(JoinRule.Knock)
|
||||
assertThat(JoinRuleItem.PublicVisibility.Restricted(A_ROOM_ID).toJoinRule())
|
||||
.isEqualTo(JoinRule.Restricted(persistentListOf(AllowRule.RoomMembership(A_ROOM_ID))))
|
||||
assertThat(JoinRuleItem.PublicVisibility.AskToJoinRestricted(A_ROOM_ID).toJoinRule())
|
||||
.isEqualTo(JoinRule.KnockRestricted(persistentListOf(AllowRule.RoomMembership(A_ROOM_ID))))
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,7 @@ enum class AvatarSize(val dp: Dp) {
|
||||
RoomPreviewInviter(56.dp),
|
||||
SpaceMember(24.dp),
|
||||
LeaveSpaceRoom(32.dp),
|
||||
SelectParentSpace(32.dp),
|
||||
|
||||
AccountItem(32.dp),
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
package io.element.android.libraries.matrix.api.spaces
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
@@ -16,6 +17,7 @@ import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
data class SpaceRoom(
|
||||
val rawName: String?,
|
||||
val displayName: String,
|
||||
|
||||
@@ -21,6 +21,11 @@ interface SpaceService {
|
||||
|
||||
fun spaceRoomList(id: RoomId): SpaceRoomList
|
||||
|
||||
/**
|
||||
* Get the list of spaces in which the current user can modify their rooms (adding or removing them).
|
||||
*/
|
||||
suspend fun editableSpaces(): Result<List<SpaceRoom>>
|
||||
|
||||
fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle
|
||||
|
||||
/**
|
||||
|
||||
@@ -87,6 +87,12 @@ class RustSpaceService(
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun editableSpaces(): Result<List<SpaceRoom>> = withContext(sessionDispatcher) {
|
||||
runCatchingExceptions {
|
||||
innerSpaceService.editableSpaces().map(spaceRoomMapper::map)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle {
|
||||
return RustLeaveSpaceHandle(
|
||||
id = spaceId,
|
||||
|
||||
@@ -23,10 +23,11 @@ class FakeSpaceService(
|
||||
private val joinedSpacesResult: () -> Result<List<SpaceRoom>> = { lambdaError() },
|
||||
private val spaceRoomListResult: (RoomId) -> SpaceRoomList = { lambdaError() },
|
||||
private val leaveSpaceHandleResult: (RoomId) -> LeaveSpaceHandle = { lambdaError() },
|
||||
private val addChildToSpaceResult: (RoomId, RoomId) -> Result<Unit> = { _, _ -> lambdaError() },
|
||||
private val removeChildFromSpaceResult: (RoomId, RoomId) -> Result<Unit> = { _, _ -> lambdaError() },
|
||||
private val joinedParentsResult: (RoomId) -> Result<List<SpaceRoom>> = { lambdaError() },
|
||||
private val getSpaceRoomResult: (RoomId) -> SpaceRoom? = { lambdaError() },
|
||||
private val editableSpacesResult: () -> Result<List<SpaceRoom>> = { lambdaError() },
|
||||
private val addChildToSpaceResult: (RoomId, RoomId) -> Result<Unit> = { _, _ -> lambdaError() },
|
||||
) : SpaceService {
|
||||
private val _spaceRoomsFlow = MutableSharedFlow<List<SpaceRoom>>()
|
||||
override val spaceRoomsFlow: SharedFlow<List<SpaceRoom>>
|
||||
@@ -52,6 +53,10 @@ class FakeSpaceService(
|
||||
return spaceRoomListResult(id)
|
||||
}
|
||||
|
||||
override suspend fun editableSpaces(): Result<List<SpaceRoom>> {
|
||||
return editableSpacesResult()
|
||||
}
|
||||
|
||||
override fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle {
|
||||
return leaveSpaceHandleResult(spaceId)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ class KonsistComposableTest {
|
||||
.functions()
|
||||
.withTopLevel()
|
||||
.withoutModifier(KoModifier.PRIVATE)
|
||||
.withoutModifier(KoModifier.INTERNAL)
|
||||
.withoutNameEndingWith("Preview")
|
||||
.withAllAnnotationsOf(Composable::class)
|
||||
.withoutReceiverType()
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user