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:
parent
5644e9225a
commit
0313fa56dd
28 changed files with 706 additions and 172 deletions
|
|
@ -38,6 +38,7 @@ dependencies {
|
||||||
implementation(projects.libraries.mediapickers.api)
|
implementation(projects.libraries.mediapickers.api)
|
||||||
implementation(projects.libraries.mediaupload.api)
|
implementation(projects.libraries.mediaupload.api)
|
||||||
implementation(projects.libraries.permissions.api)
|
implementation(projects.libraries.permissions.api)
|
||||||
|
implementation(projects.libraries.previewutils)
|
||||||
implementation(projects.libraries.usersearch.impl)
|
implementation(projects.libraries.usersearch.impl)
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,16 @@
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl.configureroom
|
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
|
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 RoomVisibilityChanged(val visibilityItem: RoomVisibilityItem) : ConfigureRoomEvents
|
data class JoinRuleChanged(val joinRuleItem: JoinRuleItem) : ConfigureRoomEvents
|
||||||
data class RoomAccessChanged(val roomAccess: RoomAccessItem) : ConfigureRoomEvents
|
|
||||||
data class RoomAddressChanged(val roomAddress: String) : ConfigureRoomEvents
|
data class RoomAddressChanged(val roomAddress: String) : ConfigureRoomEvents
|
||||||
data object CreateRoom : ConfigureRoomEvents
|
data object CreateRoom : ConfigureRoomEvents
|
||||||
data class HandleAvatarAction(val action: AvatarAction) : ConfigureRoomEvents
|
data class HandleAvatarAction(val action: AvatarAction) : ConfigureRoomEvents
|
||||||
|
data class SetParentSpace(val space: SpaceRoom?) : ConfigureRoomEvents
|
||||||
data object CancelCreateRoom : ConfigureRoomEvents
|
data object CancelCreateRoom : ConfigureRoomEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.AssistedFactory
|
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.createroom.RoomPreset
|
||||||
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
|
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.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.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.media.AvatarAction
|
||||||
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
|
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
|
||||||
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidityEffect
|
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.PermissionsEvent
|
||||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
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.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import kotlin.jvm.optionals.getOrDefault
|
import kotlin.jvm.optionals.getOrDefault
|
||||||
|
import kotlin.jvm.optionals.getOrNull
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@AssistedInject
|
@AssistedInject
|
||||||
class ConfigureRoomPresenter(
|
class ConfigureRoomPresenter(
|
||||||
|
|
@ -78,6 +87,7 @@ class ConfigureRoomPresenter(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): ConfigureRoomState {
|
override fun present(): ConfigureRoomState {
|
||||||
|
val canAddRoomToSpace by featureFlagService.isFeatureEnabledFlow(FeatureFlags.CreateSpaces).collectAsState(false)
|
||||||
val cameraPermissionState = cameraPermissionPresenter.present()
|
val cameraPermissionState = cameraPermissionPresenter.present()
|
||||||
val createRoomConfig by dataStore.getCreateRoomConfigFlow().collectAsState()
|
val createRoomConfig by dataStore.getCreateRoomConfigFlow().collectAsState()
|
||||||
val homeserverName = remember { matrixClient.userIdServerName() }
|
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) {
|
LaunchedEffect(cameraPermissionState.permissionGranted) {
|
||||||
if (cameraPermissionState.permissionGranted && pendingPermissionRequest) {
|
if (cameraPermissionState.permissionGranted && pendingPermissionRequest) {
|
||||||
pendingPermissionRequest = false
|
pendingPermissionRequest = false
|
||||||
|
|
@ -115,7 +134,7 @@ class ConfigureRoomPresenter(
|
||||||
RoomAddressValidityEffect(
|
RoomAddressValidityEffect(
|
||||||
client = matrixClient,
|
client = matrixClient,
|
||||||
roomAliasHelper = roomAliasHelper,
|
roomAliasHelper = roomAliasHelper,
|
||||||
newRoomAddress = createRoomConfig.roomVisibility.roomAddress().getOrDefault(""),
|
newRoomAddress = createRoomConfig.visibilityState.roomAddress().getOrDefault(""),
|
||||||
knownRoomAddress = null,
|
knownRoomAddress = null,
|
||||||
) { newRoomAddressValidity ->
|
) { newRoomAddressValidity ->
|
||||||
roomAddressValidity.value = newRoomAddressValidity
|
roomAddressValidity.value = newRoomAddressValidity
|
||||||
|
|
@ -124,12 +143,25 @@ class ConfigureRoomPresenter(
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
val createRoomAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
val createRoomAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||||
|
|
||||||
val availableVisibilityOptions = remember(isSpace, isKnockFeatureEnabled) {
|
// Calculate available join rules based:
|
||||||
listOfNotNull(
|
// 1. If we are creating a space.
|
||||||
RoomVisibilityItem.Public,
|
// 2. If it has a parent space.
|
||||||
RoomVisibilityItem.AskToJoin.takeIf { !isSpace && isKnockFeatureEnabled },
|
// 3. If knocking is enabled.
|
||||||
RoomVisibilityItem.Private,
|
val parentSpace = createRoomConfig.parentSpace
|
||||||
).toImmutableList()
|
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) {
|
fun createRoom(config: CreateRoomConfig) {
|
||||||
|
|
@ -141,8 +173,7 @@ class ConfigureRoomPresenter(
|
||||||
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.RoomVisibilityChanged -> dataStore.setRoomVisibility(event.visibilityItem)
|
is ConfigureRoomEvents.JoinRuleChanged -> dataStore.setJoinRule(event.joinRuleItem)
|
||||||
is ConfigureRoomEvents.RoomAccessChanged -> dataStore.setRoomAccess(event.roomAccess)
|
|
||||||
is ConfigureRoomEvents.RoomAddressChanged -> dataStore.setRoomAddress(event.roomAddress)
|
is ConfigureRoomEvents.RoomAddressChanged -> dataStore.setRoomAddress(event.roomAddress)
|
||||||
is ConfigureRoomEvents.CreateRoom -> createRoom(createRoomConfig)
|
is ConfigureRoomEvents.CreateRoom -> createRoom(createRoomConfig)
|
||||||
is ConfigureRoomEvents.HandleAvatarAction -> {
|
is ConfigureRoomEvents.HandleAvatarAction -> {
|
||||||
|
|
@ -157,8 +188,12 @@ class ConfigureRoomPresenter(
|
||||||
AvatarAction.Remove -> dataStore.setAvatarUri(uri = null)
|
AvatarAction.Remove -> dataStore.setAvatarUri(uri = null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is ConfigureRoomEvents.SetParentSpace -> {
|
||||||
ConfigureRoomEvents.CancelCreateRoom -> createRoomAction.value = AsyncAction.Uninitialized
|
dataStore.setParentSpace(event.space)
|
||||||
|
}
|
||||||
|
ConfigureRoomEvents.CancelCreateRoom -> {
|
||||||
|
createRoomAction.value = AsyncAction.Uninitialized
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -169,7 +204,8 @@ class ConfigureRoomPresenter(
|
||||||
cameraPermissionState = cameraPermissionState,
|
cameraPermissionState = cameraPermissionState,
|
||||||
homeserverName = homeserverName,
|
homeserverName = homeserverName,
|
||||||
roomAddressValidity = roomAddressValidity.value,
|
roomAddressValidity = roomAddressValidity.value,
|
||||||
availableVisibilityOptions = availableVisibilityOptions,
|
availableJoinRules = availableJoinRules,
|
||||||
|
spaces = spaces,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -180,25 +216,27 @@ class ConfigureRoomPresenter(
|
||||||
) = launch {
|
) = launch {
|
||||||
suspend {
|
suspend {
|
||||||
val avatarUrl = config.avatarUri?.let { uploadAvatar(it.toUri()) }
|
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(
|
CreateRoomParameters(
|
||||||
name = config.roomName,
|
name = config.roomName,
|
||||||
topic = config.topic,
|
topic = config.topic,
|
||||||
isEncrypted = false,
|
isEncrypted = false,
|
||||||
isDirect = false,
|
isDirect = false,
|
||||||
visibility = RoomVisibility.Public,
|
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,
|
preset = RoomPreset.PUBLIC_CHAT,
|
||||||
invite = config.invites.map { it.userId },
|
invite = config.invites.map { it.userId },
|
||||||
avatar = avatarUrl,
|
avatar = avatarUrl,
|
||||||
roomAliasName = config.roomVisibility.roomAddress(),
|
roomAliasName = config.visibilityState.roomAddress(),
|
||||||
isSpace = isSpace,
|
isSpace = isSpace,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
CreateRoomParameters(
|
CreateRoomParameters(
|
||||||
name = config.roomName,
|
name = config.roomName,
|
||||||
topic = config.topic,
|
topic = config.topic,
|
||||||
isEncrypted = config.roomVisibility is RoomVisibilityState.Private,
|
isEncrypted = config.visibilityState is RoomVisibilityState.Private,
|
||||||
isDirect = false,
|
isDirect = false,
|
||||||
visibility = RoomVisibility.Private,
|
visibility = RoomVisibility.Private,
|
||||||
historyVisibilityOverride = RoomHistoryVisibility.Invited,
|
historyVisibilityOverride = RoomHistoryVisibility.Invited,
|
||||||
|
|
@ -208,7 +246,7 @@ class ConfigureRoomPresenter(
|
||||||
isSpace = isSpace,
|
isSpace = isSpace,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
matrixClient.createRoom(params)
|
val roomId = matrixClient.createRoom(params)
|
||||||
.onFailure { failure ->
|
.onFailure { failure ->
|
||||||
Timber.e(failure, "Failed to create room")
|
Timber.e(failure, "Failed to create room")
|
||||||
}
|
}
|
||||||
|
|
@ -217,7 +255,22 @@ class ConfigureRoomPresenter(
|
||||||
analyticsService.capture(CreatedRoom(isDM = false))
|
analyticsService.capture(CreatedRoom(isDM = false))
|
||||||
}
|
}
|
||||||
.getOrThrow()
|
.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)
|
}.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 {
|
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.architecture.AsyncAction
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
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.media.AvatarAction
|
||||||
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
|
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.PermissionsState
|
||||||
|
|
@ -22,9 +23,10 @@ data class ConfigureRoomState(
|
||||||
val cameraPermissionState: PermissionsState,
|
val cameraPermissionState: PermissionsState,
|
||||||
val roomAddressValidity: RoomAddressValidity,
|
val roomAddressValidity: RoomAddressValidity,
|
||||||
val homeserverName: String,
|
val homeserverName: String,
|
||||||
val availableVisibilityOptions: ImmutableList<RoomVisibilityItem>,
|
val availableJoinRules: ImmutableList<JoinRuleItem>,
|
||||||
|
val spaces: ImmutableList<SpaceRoom>,
|
||||||
val eventSink: (ConfigureRoomEvents) -> Unit
|
val eventSink: (ConfigureRoomEvents) -> Unit
|
||||||
) {
|
) {
|
||||||
val isValid: Boolean = config.roomName?.isNotEmpty() == true &&
|
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 androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
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.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.components.aMatrixUserList
|
||||||
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
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.RoomAddressValidity
|
||||||
import io.element.android.libraries.permissions.api.PermissionsState
|
import io.element.android.libraries.permissions.api.PermissionsState
|
||||||
import io.element.android.libraries.permissions.api.aPermissionsState
|
import io.element.android.libraries.permissions.api.aPermissionsState
|
||||||
|
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomState> {
|
open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomState> {
|
||||||
|
|
@ -28,9 +31,9 @@ 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 shouldn’t be more than 3 lines",
|
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(),
|
invites = aMatrixUserList().toImmutableList(),
|
||||||
roomVisibility = RoomVisibilityState.Public(
|
visibilityState = RoomVisibilityState.Public(
|
||||||
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
||||||
roomAccess = RoomAccess.Knocking,
|
joinRuleItem = JoinRuleItem.PublicVisibility.AskToJoin,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -39,9 +42,9 @@ 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 shouldn’t be more than 3 lines",
|
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(),
|
invites = aMatrixUserList().toImmutableList(),
|
||||||
roomVisibility = RoomVisibilityState.Public(
|
visibilityState = RoomVisibilityState.Public(
|
||||||
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
||||||
roomAccess = RoomAccess.Knocking,
|
joinRuleItem = JoinRuleItem.PublicVisibility.AskToJoin,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -49,9 +52,9 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
||||||
config = CreateRoomConfig(
|
config = CreateRoomConfig(
|
||||||
roomName = "Room 101",
|
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",
|
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"),
|
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
||||||
roomAccess = RoomAccess.Knocking,
|
joinRuleItem = JoinRuleItem.PublicVisibility.AskToJoin,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
roomAddressValidity = RoomAddressValidity.NotAvailable,
|
roomAddressValidity = RoomAddressValidity.NotAvailable,
|
||||||
|
|
@ -60,9 +63,9 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
||||||
config = CreateRoomConfig(
|
config = CreateRoomConfig(
|
||||||
roomName = "Room 101",
|
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",
|
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"),
|
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
||||||
roomAccess = RoomAccess.Knocking,
|
joinRuleItem = JoinRuleItem.PublicVisibility.AskToJoin,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
roomAddressValidity = RoomAddressValidity.InvalidSymbols,
|
roomAddressValidity = RoomAddressValidity.InvalidSymbols,
|
||||||
|
|
@ -71,9 +74,9 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
||||||
config = CreateRoomConfig(
|
config = CreateRoomConfig(
|
||||||
roomName = "Room 101",
|
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",
|
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"),
|
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
||||||
roomAccess = RoomAccess.Knocking,
|
joinRuleItem = JoinRuleItem.PublicVisibility.AskToJoin,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
roomAddressValidity = RoomAddressValidity.Valid,
|
roomAddressValidity = RoomAddressValidity.Valid,
|
||||||
|
|
@ -83,13 +86,41 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
||||||
isSpace = true,
|
isSpace = true,
|
||||||
roomName = "Space 101",
|
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",
|
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"),
|
roomAddress = RoomAddress.AutoFilled("Space-101"),
|
||||||
roomAccess = RoomAccess.Anyone,
|
joinRuleItem = JoinRuleItem.PublicVisibility.Public,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
roomAddressValidity = RoomAddressValidity.Valid,
|
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),
|
cameraPermissionState: PermissionsState = aPermissionsState(showDialog = false),
|
||||||
homeserverName: String = "matrix.org",
|
homeserverName: String = "matrix.org",
|
||||||
roomAddressValidity: RoomAddressValidity = RoomAddressValidity.Valid,
|
roomAddressValidity: RoomAddressValidity = RoomAddressValidity.Valid,
|
||||||
availableVisibilityOptions: List<RoomVisibilityItem> = RoomVisibilityItem.entries.filter {
|
availableVisibilityOptions: List<JoinRuleItem> = if (config.parentSpace != null) {
|
||||||
if (!isKnockFeatureEnabled) it != RoomVisibilityItem.AskToJoin else true
|
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 = { },
|
eventSink: (ConfigureRoomEvents) -> Unit = { },
|
||||||
) = ConfigureRoomState(
|
) = ConfigureRoomState(
|
||||||
config = config,
|
config = config,
|
||||||
|
|
@ -112,6 +154,7 @@ fun aConfigureRoomState(
|
||||||
cameraPermissionState = cameraPermissionState,
|
cameraPermissionState = cameraPermissionState,
|
||||||
homeserverName = homeserverName,
|
homeserverName = homeserverName,
|
||||||
roomAddressValidity = roomAddressValidity,
|
roomAddressValidity = roomAddressValidity,
|
||||||
availableVisibilityOptions = availableVisibilityOptions.toImmutableList(),
|
availableJoinRules = availableVisibilityOptions.toImmutableList(),
|
||||||
|
spaces = spaces.toImmutableList(),
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
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.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.createroom.impl.R
|
import io.element.android.features.createroom.impl.R
|
||||||
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
|
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom
|
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.avatar.AvatarType
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
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.modifiers.clearFocusOnTap
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
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.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.api.spaces.SpaceRoom
|
||||||
import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet
|
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.AvatarPickerState
|
||||||
import io.element.android.libraries.matrix.ui.components.AvatarPickerView
|
import io.element.android.libraries.matrix.ui.components.AvatarPickerView
|
||||||
|
|
@ -120,27 +121,30 @@ fun ConfigureRoomView(
|
||||||
onTopicChange = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) },
|
onTopicChange = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) },
|
||||||
)
|
)
|
||||||
|
|
||||||
RoomVisibilityAndAccessOptions(
|
if (!state.config.isSpace && state.spaces.isNotEmpty()) {
|
||||||
options = state.availableVisibilityOptions,
|
SelectParentSpaceOptions(
|
||||||
selected = when (state.config.roomVisibility) {
|
spaces = state.spaces,
|
||||||
is RoomVisibilityState.Private -> RoomVisibilityItem.Private
|
selectedSpace = state.config.parentSpace,
|
||||||
is RoomVisibilityState.Public -> when (state.config.roomVisibility.roomAccess) {
|
onSelectSpace = { state.eventSink(ConfigureRoomEvents.SetParentSpace(it)) },
|
||||||
RoomAccess.Knocking -> RoomVisibilityItem.AskToJoin
|
)
|
||||||
RoomAccess.Anyone -> RoomVisibilityItem.Public
|
}
|
||||||
}
|
|
||||||
},
|
RoomJoinRuleOptions(
|
||||||
|
options = state.availableJoinRules,
|
||||||
|
selected = state.config.visibilityState.joinRuleItem,
|
||||||
|
parentSpace = state.config.parentSpace,
|
||||||
onOptionClick = {
|
onOptionClick = {
|
||||||
focusManager.clearFocus()
|
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 {
|
Column {
|
||||||
ListSectionHeader(title = stringResource(R.string.screen_create_room_room_address_section_title))
|
ListSectionHeader(title = stringResource(R.string.screen_create_room_room_address_section_title))
|
||||||
RoomAddressField(
|
RoomAddressField(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
address = state.config.roomVisibility.roomAddress().getOrNull().orEmpty(),
|
address = state.config.visibilityState.roomAddress().getOrNull().orEmpty(),
|
||||||
homeserverName = state.homeserverName,
|
homeserverName = state.homeserverName,
|
||||||
addressValidity = state.roomAddressValidity,
|
addressValidity = state.roomAddressValidity,
|
||||||
onAddressChange = { state.eventSink(ConfigureRoomEvents.RoomAddressChanged(it)) },
|
onAddressChange = { state.eventSink(ConfigureRoomEvents.RoomAddressChanged(it)) },
|
||||||
|
|
@ -265,7 +269,7 @@ private fun RoomTopic(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ConfigureRoomOptions(
|
internal fun ConfigureRoomOptions(
|
||||||
title: String,
|
title: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
content: @Composable ColumnScope.() -> Unit,
|
content: @Composable ColumnScope.() -> Unit,
|
||||||
|
|
@ -279,10 +283,11 @@ private fun ConfigureRoomOptions(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RoomVisibilityAndAccessOptions(
|
private fun RoomJoinRuleOptions(
|
||||||
options: ImmutableList<RoomVisibilityItem>,
|
options: ImmutableList<JoinRuleItem>,
|
||||||
selected: RoomVisibilityItem,
|
selected: JoinRuleItem,
|
||||||
onOptionClick: (RoomVisibilityItem) -> Unit,
|
onOptionClick: (JoinRuleItem) -> Unit,
|
||||||
|
parentSpace: SpaceRoom?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
ConfigureRoomOptions(
|
ConfigureRoomOptions(
|
||||||
|
|
@ -295,10 +300,12 @@ private fun RoomVisibilityAndAccessOptions(
|
||||||
leadingContent = ListItemContent.Custom {
|
leadingContent = ListItemContent.Custom {
|
||||||
RoundedIconAtom(
|
RoundedIconAtom(
|
||||||
size = RoundedIconAtomSize.Big,
|
size = RoundedIconAtomSize.Big,
|
||||||
resourceId = when (item) {
|
imageVector = when (item) {
|
||||||
RoomVisibilityItem.Public -> CompoundDrawables.ic_compound_public
|
JoinRuleItem.PublicVisibility.Public -> CompoundIcons.Public()
|
||||||
RoomVisibilityItem.AskToJoin -> CompoundDrawables.ic_compound_user_add
|
is JoinRuleItem.PublicVisibility.Restricted -> CompoundIcons.Space()
|
||||||
RoomVisibilityItem.Private -> CompoundDrawables.ic_compound_lock
|
JoinRuleItem.PublicVisibility.AskToJoin,
|
||||||
|
is JoinRuleItem.PublicVisibility.AskToJoinRestricted -> CompoundIcons.UserAdd()
|
||||||
|
JoinRuleItem.Private -> CompoundIcons.Lock()
|
||||||
},
|
},
|
||||||
tint = if (isSelected) ElementTheme.colors.iconPrimary else ElementTheme.colors.iconSecondary,
|
tint = if (isSelected) ElementTheme.colors.iconPrimary else ElementTheme.colors.iconSecondary,
|
||||||
backgroundTint = Color.Transparent,
|
backgroundTint = Color.Transparent,
|
||||||
|
|
@ -306,18 +313,29 @@ private fun RoomVisibilityAndAccessOptions(
|
||||||
},
|
},
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
val title = when (item) {
|
val title = when (item) {
|
||||||
RoomVisibilityItem.Public -> stringResource(R.string.screen_create_room_public_option_title)
|
JoinRuleItem.PublicVisibility.Public -> stringResource(R.string.screen_create_room_room_access_section_public_option_title)
|
||||||
RoomVisibilityItem.AskToJoin -> stringResource(R.string.screen_create_room_room_access_section_knocking_option_title)
|
is JoinRuleItem.PublicVisibility.Restricted -> stringResource(R.string.screen_create_room_room_access_section_restricted_option_title)
|
||||||
RoomVisibilityItem.Private -> stringResource(R.string.screen_create_room_private_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)
|
Text(text = title)
|
||||||
},
|
},
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
// TODO handle description of items in a certain space/org
|
|
||||||
val description = when (item) {
|
val description = when (item) {
|
||||||
RoomVisibilityItem.Public -> stringResource(R.string.screen_create_room_public_option_short_description)
|
JoinRuleItem.PublicVisibility.Public -> stringResource(R.string.screen_create_room_room_access_section_public_option_description)
|
||||||
RoomVisibilityItem.AskToJoin -> stringResource(R.string.screen_create_room_room_access_section_knocking_option_description)
|
is JoinRuleItem.PublicVisibility.Restricted -> stringResource(
|
||||||
RoomVisibilityItem.Private -> stringResource(R.string.screen_create_room_private_option_description)
|
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)
|
Text(text = description)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl.configureroom
|
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 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 +19,6 @@ data class CreateRoomConfig(
|
||||||
val topic: String? = null,
|
val topic: String? = null,
|
||||||
val avatarUri: String? = null,
|
val avatarUri: String? = null,
|
||||||
val invites: ImmutableList<MatrixUser> = persistentListOf(),
|
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 dev.zacsweers.metro.Inject
|
||||||
import io.element.android.libraries.androidutils.file.safeDelete
|
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.room.alias.RoomAliasHelper
|
||||||
|
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.getAndUpdate
|
import kotlinx.coroutines.flow.getAndUpdate
|
||||||
|
|
@ -33,23 +34,20 @@ class CreateRoomConfigStore(
|
||||||
|
|
||||||
fun setRoomName(roomName: String) {
|
fun setRoomName(roomName: String) {
|
||||||
createRoomConfigFlow.getAndUpdate { config ->
|
createRoomConfigFlow.getAndUpdate { config ->
|
||||||
val newVisibility = when (config.roomVisibility) {
|
val roomAccessWithNewAddress = if (config.visibilityState is RoomVisibilityState.Public) {
|
||||||
is RoomVisibilityState.Public -> {
|
val roomAddress = config.visibilityState.roomAddress
|
||||||
val roomAddress = config.roomVisibility.roomAddress
|
if (roomAddress is RoomAddress.AutoFilled || roomName.isEmpty()) {
|
||||||
if (roomAddress is RoomAddress.AutoFilled || roomName.isEmpty()) {
|
val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(roomName)
|
||||||
val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(roomName)
|
config.visibilityState.copy(roomAddress = RoomAddress.AutoFilled(roomAliasName))
|
||||||
config.roomVisibility.copy(
|
} else {
|
||||||
roomAddress = RoomAddress.AutoFilled(roomAliasName),
|
config.visibilityState
|
||||||
)
|
|
||||||
} else {
|
|
||||||
config.roomVisibility
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else -> config.roomVisibility
|
} else {
|
||||||
|
config.visibilityState
|
||||||
}
|
}
|
||||||
config.copy(
|
config.copy(
|
||||||
roomName = roomName.takeIf { it.isNotEmpty() },
|
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 ->
|
createRoomConfigFlow.getAndUpdate { config ->
|
||||||
config.copy(
|
config.copy(
|
||||||
roomVisibility = when (visibility) {
|
visibilityState = when (joinRule) {
|
||||||
RoomVisibilityItem.Private -> RoomVisibilityState.Private
|
JoinRuleItem.Private -> RoomVisibilityState.Private()
|
||||||
RoomVisibilityItem.Public, RoomVisibilityItem.AskToJoin -> {
|
is JoinRuleItem.PublicVisibility -> {
|
||||||
val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(config.roomName.orEmpty())
|
val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(config.roomName.orEmpty())
|
||||||
RoomVisibilityState.Public(
|
RoomVisibilityState.Public(
|
||||||
roomAddress = RoomAddress.AutoFilled(roomAliasName),
|
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) {
|
fun setRoomAddress(address: String) {
|
||||||
createRoomConfigFlow.getAndUpdate { config ->
|
createRoomConfigFlow.getAndUpdate { config ->
|
||||||
config.copy(
|
config.copy(
|
||||||
roomVisibility = when (config.roomVisibility) {
|
visibilityState = when (config.visibilityState) {
|
||||||
is RoomVisibilityState.Public -> {
|
is RoomVisibilityState.Public -> {
|
||||||
val sanitizedAddress = address.lowercase()
|
val sanitizedAddress = address.lowercase()
|
||||||
config.roomVisibility.copy(roomAddress = RoomAddress.Edited(sanitizedAddress))
|
config.visibilityState.copy(roomAddress = RoomAddress.Edited(sanitizedAddress))
|
||||||
}
|
}
|
||||||
else -> config.roomVisibility
|
else -> config.visibilityState
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -120,6 +105,15 @@ class CreateRoomConfigStore(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setParentSpace(parentSpace: SpaceRoom?) {
|
||||||
|
createRoomConfigFlow.getAndUpdate { config ->
|
||||||
|
config.copy(
|
||||||
|
parentSpace = parentSpace,
|
||||||
|
visibilityState = RoomVisibilityState.Private(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun clearCachedData() {
|
fun clearCachedData() {
|
||||||
cachedAvatarUri = null
|
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
|
import java.util.Optional
|
||||||
|
|
||||||
sealed interface RoomVisibilityState {
|
sealed interface RoomVisibilityState {
|
||||||
data object Private : RoomVisibilityState
|
val joinRuleItem: JoinRuleItem
|
||||||
|
data class Private(override val joinRuleItem: JoinRuleItem.Private = JoinRuleItem.Private) : RoomVisibilityState
|
||||||
|
|
||||||
data class Public(
|
data class Public(
|
||||||
val roomAddress: RoomAddress,
|
val roomAddress: RoomAddress,
|
||||||
val roomAccess: RoomAccess,
|
override val joinRuleItem: JoinRuleItem.PublicVisibility,
|
||||||
) : RoomVisibilityState
|
) : RoomVisibilityState
|
||||||
|
|
||||||
fun roomAddress(): Optional<String> {
|
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 (c) 2026 Element Creations Ltd.
|
||||||
* Copyright 2023-2025 New Vector Ltd.
|
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* 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 android.net.Uri
|
||||||
import androidx.core.net.toUri
|
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.ConfigureRoomState
|
||||||
import io.element.android.features.createroom.impl.configureroom.CreateRoomConfig
|
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.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.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.configureroom.RoomVisibilityState
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
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.ResolvedRoomAlias
|
||||||
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
|
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_AVATAR_URL
|
||||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
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_MESSAGE
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
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.A_ROOM_NAME
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
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.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.media.AvatarAction
|
||||||
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
|
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
|
||||||
import io.element.android.libraries.mediapickers.api.PickerProvider
|
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.api.PermissionsPresenter
|
||||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
|
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
|
||||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
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.api.AnalyticsService
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.test.advanceUntilIdle
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
|
@ -81,7 +89,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.roomVisibility).isEqualTo(RoomVisibilityState.Private)
|
assertThat(initialState.config.visibilityState).isEqualTo(RoomVisibilityState.Private())
|
||||||
assertThat(initialState.createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
assertThat(initialState.createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
assertThat(initialState.homeserverName).isEqualTo("matrix.org")
|
assertThat(initialState.homeserverName).isEqualTo("matrix.org")
|
||||||
}
|
}
|
||||||
|
|
@ -174,12 +182,12 @@ class ConfigureRoomPresenterTest {
|
||||||
assertThat(newState.config).isEqualTo(expectedConfig)
|
assertThat(newState.config).isEqualTo(expectedConfig)
|
||||||
|
|
||||||
// Room privacy
|
// Room privacy
|
||||||
newState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
|
newState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
||||||
newState = awaitItem()
|
newState = awaitItem()
|
||||||
expectedConfig = expectedConfig.copy(
|
expectedConfig = expectedConfig.copy(
|
||||||
roomVisibility = RoomVisibilityState.Public(
|
visibilityState = RoomVisibilityState.Public(
|
||||||
roomAddress = RoomAddress.AutoFilled(roomAliasHelper.roomAliasNameFromRoomDisplayName(expectedConfig.roomName ?: "")),
|
roomAddress = RoomAddress.AutoFilled(roomAliasHelper.roomAliasNameFromRoomDisplayName(expectedConfig.roomName ?: "")),
|
||||||
roomAccess = RoomAccess.Anyone,
|
joinRuleItem = JoinRuleItem.PublicVisibility.Public,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertThat(newState.config).isEqualTo(expectedConfig)
|
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
|
@Test
|
||||||
fun `present - record analytics when creating room`() = runTest {
|
fun `present - record analytics when creating room`() = runTest {
|
||||||
val matrixClient = createMatrixClient()
|
val matrixClient = createMatrixClient()
|
||||||
|
|
@ -311,7 +422,7 @@ class ConfigureRoomPresenterTest {
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = initialState()
|
val initialState = initialState()
|
||||||
initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
|
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("invalid address"))
|
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("invalid address"))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
@ -331,7 +442,7 @@ class ConfigureRoomPresenterTest {
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = initialState()
|
val initialState = initialState()
|
||||||
initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
|
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address"))
|
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address"))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
@ -351,7 +462,7 @@ class ConfigureRoomPresenterTest {
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = initialState()
|
val initialState = initialState()
|
||||||
initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
|
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address"))
|
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address"))
|
||||||
skipItems(1)
|
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 {
|
private suspend fun TurbineTestContext<ConfigureRoomState>.initialState(): ConfigureRoomState {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
return awaitItem()
|
return awaitItem()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createMatrixClient(isAliasAvailable: Boolean = true) = FakeMatrixClient(
|
private fun createMatrixClient(
|
||||||
|
isAliasAvailable: Boolean = true,
|
||||||
|
spaceService: FakeSpaceService = FakeSpaceService(),
|
||||||
|
) = FakeMatrixClient(
|
||||||
userIdServerNameLambda = { "matrix.org" },
|
userIdServerNameLambda = { "matrix.org" },
|
||||||
resolveRoomAliasResult = {
|
resolveRoomAliasResult = {
|
||||||
val resolvedRoomAlias = if (isAliasAvailable) {
|
val resolvedRoomAlias = if (isAliasAvailable) {
|
||||||
|
|
@ -376,7 +526,8 @@ class ConfigureRoomPresenterTest {
|
||||||
Optional.of(ResolvedRoomAlias(A_ROOM_ID, emptyList()))
|
Optional.of(ResolvedRoomAlias(A_ROOM_ID, emptyList()))
|
||||||
}
|
}
|
||||||
Result.success(resolvedRoomAlias)
|
Result.success(resolvedRoomAlias)
|
||||||
}
|
},
|
||||||
|
spaceService = spaceService,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun createConfigureRoomPresenter(
|
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),
|
RoomPreviewInviter(56.dp),
|
||||||
SpaceMember(24.dp),
|
SpaceMember(24.dp),
|
||||||
LeaveSpaceRoom(32.dp),
|
LeaveSpaceRoom(32.dp),
|
||||||
|
SelectParentSpace(32.dp),
|
||||||
|
|
||||||
AccountItem(32.dp),
|
AccountItem(32.dp),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.api.spaces
|
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.RoomAlias
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
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 io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
@Immutable
|
||||||
data class SpaceRoom(
|
data class SpaceRoom(
|
||||||
val rawName: String?,
|
val rawName: String?,
|
||||||
val displayName: String,
|
val displayName: String,
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,11 @@ interface SpaceService {
|
||||||
|
|
||||||
fun spaceRoomList(id: RoomId): SpaceRoomList
|
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
|
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 {
|
override fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle {
|
||||||
return RustLeaveSpaceHandle(
|
return RustLeaveSpaceHandle(
|
||||||
id = spaceId,
|
id = spaceId,
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,11 @@ class FakeSpaceService(
|
||||||
private val joinedSpacesResult: () -> Result<List<SpaceRoom>> = { lambdaError() },
|
private val joinedSpacesResult: () -> Result<List<SpaceRoom>> = { lambdaError() },
|
||||||
private val spaceRoomListResult: (RoomId) -> SpaceRoomList = { lambdaError() },
|
private val spaceRoomListResult: (RoomId) -> SpaceRoomList = { lambdaError() },
|
||||||
private val leaveSpaceHandleResult: (RoomId) -> LeaveSpaceHandle = { 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 removeChildFromSpaceResult: (RoomId, RoomId) -> Result<Unit> = { _, _ -> lambdaError() },
|
||||||
private val joinedParentsResult: (RoomId) -> Result<List<SpaceRoom>> = { lambdaError() },
|
private val joinedParentsResult: (RoomId) -> Result<List<SpaceRoom>> = { lambdaError() },
|
||||||
private val getSpaceRoomResult: (RoomId) -> SpaceRoom? = { lambdaError() },
|
private val getSpaceRoomResult: (RoomId) -> SpaceRoom? = { lambdaError() },
|
||||||
|
private val editableSpacesResult: () -> Result<List<SpaceRoom>> = { lambdaError() },
|
||||||
|
private val addChildToSpaceResult: (RoomId, RoomId) -> Result<Unit> = { _, _ -> lambdaError() },
|
||||||
) : SpaceService {
|
) : SpaceService {
|
||||||
private val _spaceRoomsFlow = MutableSharedFlow<List<SpaceRoom>>()
|
private val _spaceRoomsFlow = MutableSharedFlow<List<SpaceRoom>>()
|
||||||
override val spaceRoomsFlow: SharedFlow<List<SpaceRoom>>
|
override val spaceRoomsFlow: SharedFlow<List<SpaceRoom>>
|
||||||
|
|
@ -52,6 +53,10 @@ class FakeSpaceService(
|
||||||
return spaceRoomListResult(id)
|
return spaceRoomListResult(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun editableSpaces(): Result<List<SpaceRoom>> {
|
||||||
|
return editableSpacesResult()
|
||||||
|
}
|
||||||
|
|
||||||
override fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle {
|
override fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle {
|
||||||
return leaveSpaceHandleResult(spaceId)
|
return leaveSpaceHandleResult(spaceId)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ class KonsistComposableTest {
|
||||||
.functions()
|
.functions()
|
||||||
.withTopLevel()
|
.withTopLevel()
|
||||||
.withoutModifier(KoModifier.PRIVATE)
|
.withoutModifier(KoModifier.PRIVATE)
|
||||||
|
.withoutModifier(KoModifier.INTERNAL)
|
||||||
.withoutNameEndingWith("Preview")
|
.withoutNameEndingWith("Preview")
|
||||||
.withAllAnnotationsOf(Composable::class)
|
.withAllAnnotationsOf(Composable::class)
|
||||||
.withoutReceiverType()
|
.withoutReceiverType()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:c949830fae61474e63adb56f9d3b355db203a4c2902fcef4605ac6b6a5c5148a
|
||||||
|
size 45333
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:d4bf55777af9384a34c438de10887f5b6dff107b837f4bb7a2f974644c256047
|
||||||
|
size 48431
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:286ec7cfa373ebf4bcd8704895a0398f4b4fe7710c2608b325a842dae3e7a294
|
||||||
|
size 46955
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:0e95d2194cfc8b979624ce3b97250a85051d67bd8f5f61bb1e60d274c08f4649
|
||||||
|
size 50132
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a7168b3f34dbd9bdbb34682aecbc1f81e16ae7be6af7e832f6e18e0d090f38f2
|
||||||
|
size 21093
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:d03b34a155b919fd17b280448991299294a0feda120c1f1e4a4ce2d967073294
|
||||||
|
size 19942
|
||||||
Loading…
Add table
Add a link
Reference in a new issue