Add manage mode to space view for removing child rooms, wip.

This commit is contained in:
ganfra 2026-01-13 22:08:48 +01:00
parent 168ea3e0e4
commit 78b4895254
11 changed files with 263 additions and 21 deletions

View file

@ -8,6 +8,7 @@
package io.element.android.features.space.impl.root package io.element.android.features.space.impl.root
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceRoom import io.element.android.libraries.matrix.api.spaces.SpaceRoom
sealed interface SpaceEvents { sealed interface SpaceEvents {
@ -19,4 +20,12 @@ sealed interface SpaceEvents {
data class ShowTopicViewer(val topic: String) : SpaceEvents data class ShowTopicViewer(val topic: String) : SpaceEvents
data object HideTopicViewer : SpaceEvents data object HideTopicViewer : SpaceEvents
// Manage mode events
data object EnterManageMode : SpaceEvents
data object ExitManageMode : SpaceEvents
data class ToggleRoomSelection(val roomId: RoomId) : SpaceEvents
data object ConfirmRoomRemoval : SpaceEvents
data object RemoveSelectedRooms : SpaceEvents
data object ClearRemoveAction : SpaceEvents
} }

View file

@ -40,6 +40,7 @@ import io.element.android.libraries.matrix.api.room.join.JoinRoom
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
import io.element.android.libraries.matrix.api.spaces.SpaceRoom import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
import io.element.android.libraries.matrix.api.spaces.SpaceService
import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -48,6 +49,8 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toImmutableMap
import kotlinx.collections.immutable.toImmutableSet import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.jvm.optionals.getOrNull import kotlin.jvm.optionals.getOrNull
@ -62,6 +65,7 @@ class SpacePresenter(
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>, private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
private val featureFlagService: FeatureFlagService, private val featureFlagService: FeatureFlagService,
private val spaceService: SpaceService,
) : Presenter<SpaceState> { ) : Presenter<SpaceState> {
private var children by mutableStateOf<ImmutableList<SpaceRoom>>(persistentListOf()) private var children by mutableStateOf<ImmutableList<SpaceRoom>>(persistentListOf())
@ -104,6 +108,11 @@ class SpacePresenter(
var topicViewerState: TopicViewerState by remember { mutableStateOf(TopicViewerState.Hidden) } var topicViewerState: TopicViewerState by remember { mutableStateOf(TopicViewerState.Hidden) }
// Manage mode state
var isManageMode by remember { mutableStateOf(false) }
var selectedRoomIds by remember { mutableStateOf<Set<RoomId>>(emptySet()) }
var removeRoomsAction by remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
LaunchedEffect(children) { LaunchedEffect(children) {
// Remove joined children from the join actions // Remove joined children from the join actions
val joinedChildren = children val joinedChildren = children
@ -138,6 +147,46 @@ class SpacePresenter(
} }
SpaceEvents.HideTopicViewer -> topicViewerState = TopicViewerState.Hidden SpaceEvents.HideTopicViewer -> topicViewerState = TopicViewerState.Hidden
is SpaceEvents.ShowTopicViewer -> topicViewerState = TopicViewerState.Shown(event.topic) is SpaceEvents.ShowTopicViewer -> topicViewerState = TopicViewerState.Shown(event.topic)
// Manage mode events
SpaceEvents.EnterManageMode -> {
isManageMode = true
selectedRoomIds = emptySet()
}
SpaceEvents.ExitManageMode -> {
isManageMode = false
selectedRoomIds = emptySet()
}
is SpaceEvents.ToggleRoomSelection -> {
selectedRoomIds = if (event.roomId in selectedRoomIds) {
selectedRoomIds - event.roomId
} else {
selectedRoomIds + event.roomId
}
}
SpaceEvents.RemoveSelectedRooms -> {
removeRoomsAction = AsyncAction.ConfirmingNoParams
}
SpaceEvents.ConfirmRoomRemoval -> {
localCoroutineScope.launch {
removeRoomsAction = AsyncAction.Loading
val spaceId = spaceRoomList.roomId
val results = selectedRoomIds.map { roomId ->
async { spaceService.removeChildFromSpace(spaceId, roomId) }
}
val hasError = results.awaitAll().any { it.isFailure }
if (hasError) {
removeRoomsAction = AsyncAction.Failure(Exception("Failed to remove some rooms"))
} else {
removeRoomsAction = AsyncAction.Success(Unit)
isManageMode = false
selectedRoomIds = emptySet()
}
}
}
SpaceEvents.ClearRemoveAction -> {
removeRoomsAction = AsyncAction.Uninitialized
}
} }
} }
return SpaceState( return SpaceState(
@ -150,6 +199,10 @@ class SpacePresenter(
acceptDeclineInviteState = acceptDeclineInviteState, acceptDeclineInviteState = acceptDeclineInviteState,
topicViewerState = topicViewerState, topicViewerState = topicViewerState,
canAccessSpaceSettings = canAccessSpaceSettings, canAccessSpaceSettings = canAccessSpaceSettings,
isManageMode = isManageMode,
selectedRoomIds = selectedRoomIds.toImmutableSet(),
canManageRooms = permissions.canManageRooms,
removeRoomsAction = removeRoomsAction,
eventSink = ::handleEvent, eventSink = ::handleEvent,
) )
} }

View file

@ -27,12 +27,20 @@ data class SpaceState(
val acceptDeclineInviteState: AcceptDeclineInviteState, val acceptDeclineInviteState: AcceptDeclineInviteState,
val topicViewerState: TopicViewerState, val topicViewerState: TopicViewerState,
val canAccessSpaceSettings: Boolean, val canAccessSpaceSettings: Boolean,
val isManageMode: Boolean,
val selectedRoomIds: ImmutableSet<RoomId>,
val canManageRooms: Boolean,
val removeRoomsAction: AsyncAction<Unit>,
val eventSink: (SpaceEvents) -> Unit val eventSink: (SpaceEvents) -> Unit
) { ) {
fun isJoining(spaceId: RoomId): Boolean = joinActions[spaceId] == AsyncAction.Loading fun isJoining(spaceId: RoomId): Boolean = joinActions[spaceId] == AsyncAction.Loading
val hasAnyFailure: Boolean = joinActions.values.any { val hasAnyFailure: Boolean = joinActions.values.any {
it is AsyncAction.Failure it is AsyncAction.Failure
} }
val showManageRoomsAction: Boolean = canManageRooms && children.isNotEmpty()
val selectedCount: Int = selectedRoomIds.size
val isRemoveButtonEnabled: Boolean = selectedRoomIds.isNotEmpty()
} }
@Immutable @Immutable

View file

@ -39,7 +39,26 @@ open class SpaceStateProvider : PreviewParameterProvider<SpaceState> {
aSpaceState( aSpaceState(
topicViewerState = TopicViewerState.Shown(topic = "Space description goes here." + LoremIpsum(20).values.first()), topicViewerState = TopicViewerState.Shown(topic = "Space description goes here." + LoremIpsum(20).values.first()),
), ),
// Add other states here // Manage mode states
aSpaceState(
parentSpace = aParentSpace(),
children = aListOfSpaceRooms(),
isManageMode = true,
selectedRoomIds = emptySet(),
),
aSpaceState(
parentSpace = aParentSpace(),
children = aListOfSpaceRooms(),
isManageMode = true,
selectedRoomIds = setOf(RoomId("!spaceId0:example.com"), RoomId("!spaceId1:example.com")),
),
aSpaceState(
parentSpace = aParentSpace(),
children = aListOfSpaceRooms(),
isManageMode = true,
selectedRoomIds = setOf(RoomId("!spaceId0:example.com")),
removeRoomsAction = AsyncAction.ConfirmingNoParams,
),
) )
} }
@ -54,6 +73,10 @@ fun aSpaceState(
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(), acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
topicViewerState: TopicViewerState = TopicViewerState.Hidden, topicViewerState: TopicViewerState = TopicViewerState.Hidden,
canAccessSpaceSettings: Boolean = true, canAccessSpaceSettings: Boolean = true,
isManageMode: Boolean = false,
selectedRoomIds: Set<RoomId> = emptySet(),
canManageRooms: Boolean = true,
removeRoomsAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
eventSink: (SpaceEvents) -> Unit = { }, eventSink: (SpaceEvents) -> Unit = { },
) = SpaceState( ) = SpaceState(
currentSpace = parentSpace, currentSpace = parentSpace,
@ -65,6 +88,10 @@ fun aSpaceState(
acceptDeclineInviteState = acceptDeclineInviteState, acceptDeclineInviteState = acceptDeclineInviteState,
topicViewerState = topicViewerState, topicViewerState = topicViewerState,
canAccessSpaceSettings = canAccessSpaceSettings, canAccessSpaceSettings = canAccessSpaceSettings,
isManageMode = isManageMode,
selectedRoomIds = selectedRoomIds.toImmutableSet(),
canManageRooms = canManageRooms,
removeRoomsAction = removeRoomsAction,
eventSink = eventSink, eventSink = eventSink,
) )

View file

@ -40,7 +40,10 @@ 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.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.atomic.molecules.InviteButtonsRowMolecule import io.element.android.libraries.designsystem.atomic.molecules.InviteButtonsRowMolecule
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.components.ClickableLinkText import io.element.android.libraries.designsystem.components.ClickableLinkText
import io.element.android.libraries.designsystem.components.SimpleModalBottomSheet import io.element.android.libraries.designsystem.components.SimpleModalBottomSheet
import io.element.android.libraries.designsystem.components.async.AsyncIndicator import io.element.android.libraries.designsystem.components.async.AsyncIndicator
@ -56,9 +59,11 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.DropdownMenu import io.element.android.libraries.designsystem.theme.components.DropdownMenu
import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem
import io.element.android.libraries.designsystem.theme.components.Checkbox
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
@ -88,15 +93,26 @@ fun SpaceView(
Scaffold( Scaffold(
modifier = modifier, modifier = modifier,
topBar = { topBar = {
SpaceViewTopBar( if (state.isManageMode) {
currentSpace = state.currentSpace, ManageModeTopBar(
canAccessSpaceSettings = state.canAccessSpaceSettings, selectedCount = state.selectedCount,
onBackClick = onBackClick, isRemoveButtonEnabled = state.isRemoveButtonEnabled,
onLeaveSpaceClick = onLeaveSpaceClick, onCancelClick = { state.eventSink(SpaceEvents.ExitManageMode) },
onShareSpace = onShareSpace, onRemoveClick = { state.eventSink(SpaceEvents.RemoveSelectedRooms) },
onSettingsClick = onSettingsClick, )
onViewMembersClick = onViewMembersClick, } else {
) SpaceViewTopBar(
currentSpace = state.currentSpace,
canAccessSpaceSettings = state.canAccessSpaceSettings,
showManageRoomsAction = state.showManageRoomsAction,
onBackClick = onBackClick,
onLeaveSpaceClick = onLeaveSpaceClick,
onShareSpace = onShareSpace,
onSettingsClick = onSettingsClick,
onViewMembersClick = onViewMembersClick,
onManageRoomsClick = { state.eventSink(SpaceEvents.EnterManageMode) },
)
}
}, },
content = { padding -> content = { padding ->
Box( Box(
@ -104,7 +120,13 @@ fun SpaceView(
) { ) {
SpaceViewContent( SpaceViewContent(
state = state, state = state,
onRoomClick = onRoomClick, onRoomClick = { spaceRoom ->
if (state.isManageMode) {
state.eventSink(SpaceEvents.ToggleRoomSelection(spaceRoom.roomId))
} else {
onRoomClick(spaceRoom)
}
},
onTopicClick = { topic -> onTopicClick = { topic ->
state.eventSink(SpaceEvents.ShowTopicViewer(topic)) state.eventSink(SpaceEvents.ShowTopicViewer(topic))
} }
@ -125,6 +147,14 @@ fun SpaceView(
} }
) )
} }
// Confirmation dialog for removing rooms
RemoveRoomsConfirmationDialog(
removeRoomsAction = state.removeRoomsAction,
selectedCount = state.selectedCount,
onConfirm = { state.eventSink(SpaceEvents.ConfirmRoomRemoval) },
onDismiss = { state.eventSink(SpaceEvents.ClearRemoveAction) },
)
} }
@Composable @Composable
@ -200,6 +230,7 @@ private fun SpaceViewContent(
) { index, spaceRoom -> ) { index, spaceRoom ->
val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED
val isCurrentlyJoining = state.isJoining(spaceRoom.roomId) val isCurrentlyJoining = state.isJoining(spaceRoom.roomId)
val isSelected = spaceRoom.roomId in state.selectedRoomIds
SpaceRoomItemView( SpaceRoomItemView(
spaceRoom = spaceRoom, spaceRoom = spaceRoom,
showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites, showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites,
@ -210,17 +241,30 @@ private fun SpaceViewContent(
onLongClick = { onLongClick = {
// TODO // TODO
}, },
trailingAction = spaceRoom.trailingAction(isCurrentlyJoining = isCurrentlyJoining) { trailingAction = if (state.isManageMode) {
state.eventSink(SpaceEvents.Join(spaceRoom)) {
}, Checkbox(
bottomAction = spaceRoom.inviteButtons( checked = isSelected,
onAcceptClick = { onCheckedChange = null,
state.eventSink(SpaceEvents.AcceptInvite(spaceRoom)) )
},
onDeclineClick = {
state.eventSink(SpaceEvents.DeclineInvite(spaceRoom))
} }
) } else {
spaceRoom.trailingAction(isCurrentlyJoining = isCurrentlyJoining) {
state.eventSink(SpaceEvents.Join(spaceRoom))
}
},
bottomAction = if (state.isManageMode) {
null
} else {
spaceRoom.inviteButtons(
onAcceptClick = {
state.eventSink(SpaceEvents.AcceptInvite(spaceRoom))
},
onDeclineClick = {
state.eventSink(SpaceEvents.DeclineInvite(spaceRoom))
}
)
}
) )
if (index != state.children.lastIndex) { if (index != state.children.lastIndex) {
HorizontalDivider() HorizontalDivider()
@ -259,11 +303,13 @@ private fun LoadingMoreIndicator(
private fun SpaceViewTopBar( private fun SpaceViewTopBar(
currentSpace: SpaceRoom?, currentSpace: SpaceRoom?,
canAccessSpaceSettings: Boolean, canAccessSpaceSettings: Boolean,
showManageRoomsAction: Boolean,
onBackClick: () -> Unit, onBackClick: () -> Unit,
onLeaveSpaceClick: () -> Unit, onLeaveSpaceClick: () -> Unit,
onSettingsClick: () -> Unit, onSettingsClick: () -> Unit,
onShareSpace: () -> Unit, onShareSpace: () -> Unit,
onViewMembersClick: () -> Unit, onViewMembersClick: () -> Unit,
onManageRoomsClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
TopAppBar( TopAppBar(
@ -313,6 +359,16 @@ private fun SpaceViewTopBar(
onShareSpace() onShareSpace()
} }
) )
if (showManageRoomsAction) {
SpaceMenuItem(
titleRes = CommonStrings.action_manage_rooms,
icon = CompoundIcons.Edit(),
onClick = {
showMenu = false
onManageRoomsClick()
}
)
}
if (canAccessSpaceSettings) { if (canAccessSpaceSettings) {
SpaceMenuItem( SpaceMenuItem(
titleRes = CommonStrings.common_settings, titleRes = CommonStrings.common_settings,
@ -337,6 +393,39 @@ private fun SpaceViewTopBar(
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ManageModeTopBar(
selectedCount: Int,
isRemoveButtonEnabled: Boolean,
onCancelClick: () -> Unit,
onRemoveClick: () -> Unit,
modifier: Modifier = Modifier,
) {
TopAppBar(
modifier = modifier,
navigationIcon = {
BackButton(
onClick = onCancelClick,
imageVector = CompoundIcons.Close()
)
},
title = {
Text(
text = "$selectedCount selected",
style = ElementTheme.typography.fontBodyLgMedium,
)
},
actions = {
TextButton(
text = stringResource(CommonStrings.action_remove),
onClick = onRemoveClick,
enabled = isRemoveButtonEnabled,
)
},
)
}
@Composable @Composable
private fun SpaceMenuItem( private fun SpaceMenuItem(
@StringRes titleRes: Int, @StringRes titleRes: Int,
@ -425,6 +514,34 @@ private fun SpaceRoom.inviteButtons(
} }
} }
@Composable
private fun RemoveRoomsConfirmationDialog(
removeRoomsAction: AsyncAction<Unit>,
selectedCount: Int,
onConfirm: () -> Unit,
onDismiss: () -> Unit,
) {
when (removeRoomsAction) {
AsyncAction.ConfirmingNoParams -> {
ConfirmationDialog(
title = "Remove $selectedCount rooms from space?",
content = "Removing a room will not affect the room access. To change the access go to Room info > Privacy & security.",
submitText = stringResource(CommonStrings.action_remove),
onSubmitClick = onConfirm,
onDismiss = onDismiss,
destructiveSubmit = true,
)
}
else -> {
AsyncActionView(
async = removeRoomsAction,
onSuccess = { onDismiss() },
onErrorDismiss = onDismiss,
)
}
}
}
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun SpaceViewPreview( internal fun SpaceViewPreview(

View file

@ -11,6 +11,7 @@ import io.element.android.features.roomdetailsedit.api.RoomDetailsEditPermission
import io.element.android.features.roomdetailsedit.api.roomDetailsEditPermissions import io.element.android.features.roomdetailsedit.api.roomDetailsEditPermissions
import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyPermissions import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyPermissions
import io.element.android.features.securityandprivacy.api.securityAndPrivacyPermissions import io.element.android.features.securityandprivacy.api.securityAndPrivacyPermissions
import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions
import io.element.android.libraries.matrix.api.room.powerlevels.canEditRolesAndPermissions import io.element.android.libraries.matrix.api.room.powerlevels.canEditRolesAndPermissions
@ -19,6 +20,7 @@ data class SpaceSettingsPermissions(
val editDetailsPermissions: RoomDetailsEditPermissions, val editDetailsPermissions: RoomDetailsEditPermissions,
val canEditRolesAndPermissions: Boolean, val canEditRolesAndPermissions: Boolean,
val securityAndPrivacyPermissions: SecurityAndPrivacyPermissions, val securityAndPrivacyPermissions: SecurityAndPrivacyPermissions,
val canManageRooms: Boolean,
) { ) {
fun hasAny(joinRule: JoinRule?): Boolean { fun hasAny(joinRule: JoinRule?): Boolean {
return editDetailsPermissions.hasAny || return editDetailsPermissions.hasAny ||
@ -31,6 +33,7 @@ data class SpaceSettingsPermissions(
editDetailsPermissions = RoomDetailsEditPermissions.DEFAULT, editDetailsPermissions = RoomDetailsEditPermissions.DEFAULT,
canEditRolesAndPermissions = false, canEditRolesAndPermissions = false,
securityAndPrivacyPermissions = SecurityAndPrivacyPermissions.DEFAULT, securityAndPrivacyPermissions = SecurityAndPrivacyPermissions.DEFAULT,
canManageRooms = false,
) )
} }
} }
@ -40,5 +43,6 @@ fun RoomPermissions.spaceSettingsPermissions(): SpaceSettingsPermissions {
editDetailsPermissions = roomDetailsEditPermissions(), editDetailsPermissions = roomDetailsEditPermissions(),
canEditRolesAndPermissions = canEditRolesAndPermissions(), canEditRolesAndPermissions = canEditRolesAndPermissions(),
securityAndPrivacyPermissions = securityAndPrivacyPermissions(), securityAndPrivacyPermissions = securityAndPrivacyPermissions(),
canManageRooms = canOwnUserSendState(StateEventType.SpaceChild),
) )
} }

View file

@ -28,6 +28,7 @@ import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.join.JoinRoom import io.element.android.libraries.matrix.api.room.join.JoinRoom
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
import io.element.android.libraries.matrix.api.spaces.SpaceService
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_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID_2 import io.element.android.libraries.matrix.test.A_ROOM_ID_2
@ -36,6 +37,7 @@ import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions
import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
import io.element.android.libraries.previewutils.room.aSpaceRoom import io.element.android.libraries.previewutils.room.aSpaceRoom
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
@ -365,6 +367,7 @@ class SpacePresenterTest {
), ),
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() }, acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
spaceSettingsEnabled: Boolean = false, spaceSettingsEnabled: Boolean = false,
spaceService: FakeSpaceService = FakeSpaceService(),
): SpacePresenter { ): SpacePresenter {
return SpacePresenter( return SpacePresenter(
client = client, client = client,
@ -379,6 +382,7 @@ class SpacePresenterTest {
FeatureFlags.SpaceSettings.key to spaceSettingsEnabled, FeatureFlags.SpaceSettings.key to spaceSettingsEnabled,
) )
), ),
spaceService = spaceService,
) )
} }
} }

View file

@ -18,4 +18,12 @@ interface SpaceService {
fun spaceRoomList(id: RoomId): SpaceRoomList fun spaceRoomList(id: RoomId): SpaceRoomList
fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle
/**
* Remove a child room from a space.
* @param spaceId The space ID from which to remove the child.
* @param childId The room ID of the child to remove.
* @return A result indicating success or failure.
*/
suspend fun removeChildFromSpace(spaceId: RoomId, childId: RoomId): Result<Unit>
} }

View file

@ -79,6 +79,12 @@ class RustSpaceService(
} }
} }
override suspend fun removeChildFromSpace(spaceId: RoomId, childId: RoomId): Result<Unit> = withContext(sessionDispatcher) {
runCatchingExceptions {
innerSpaceService.removeChildFromSpace(spaceId.value, childId.value)
}
}
init { init {
innerSpaceService innerSpaceService
.spaceListUpdate() .spaceListUpdate()

View file

@ -23,6 +23,7 @@ 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 removeChildFromSpaceResult: (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>>
@ -43,4 +44,8 @@ class FakeSpaceService(
override fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle { override fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle {
return leaveSpaceHandleResult(spaceId) return leaveSpaceHandleResult(spaceId)
} }
override suspend fun removeChildFromSpace(spaceId: RoomId, childId: RoomId): Result<Unit> = simulateLongTask {
removeChildFromSpaceResult(spaceId, childId)
}
} }

View file

@ -112,6 +112,7 @@
<string name="action_load_more">"Load more"</string> <string name="action_load_more">"Load more"</string>
<string name="action_manage_account">"Manage account"</string> <string name="action_manage_account">"Manage account"</string>
<string name="action_manage_devices">"Manage devices"</string> <string name="action_manage_devices">"Manage devices"</string>
<string name="action_manage_rooms">"Manage rooms"</string>
<string name="action_message">"Message"</string> <string name="action_message">"Message"</string>
<string name="action_minimize">"Minimise"</string> <string name="action_minimize">"Minimise"</string>
<string name="action_next">"Next"</string> <string name="action_next">"Next"</string>