From b7ff884838ae2ec387e1027872e21951a8d204e2 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 13 Jan 2026 22:08:48 +0100 Subject: [PATCH 01/15] Add manage mode to space view for removing child rooms, wip. --- .../features/space/impl/root/SpaceEvents.kt | 9 + .../space/impl/root/SpacePresenter.kt | 53 ++++++ .../features/space/impl/root/SpaceState.kt | 8 + .../space/impl/root/SpaceStateProvider.kt | 29 +++- .../features/space/impl/root/SpaceView.kt | 157 +++++++++++++++--- .../impl/settings/SpaceSettingsPermissions.kt | 4 + .../space/impl/root/SpacePresenterTest.kt | 4 + .../matrix/api/spaces/SpaceService.kt | 8 + .../matrix/impl/spaces/RustSpaceService.kt | 6 + .../matrix/test/spaces/FakeSpaceService.kt | 5 + .../src/main/res/values/localazy.xml | 1 + 11 files changed, 263 insertions(+), 21 deletions(-) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt index 16a6ad1c3f..0f17a2f6f7 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt @@ -8,6 +8,7 @@ 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 sealed interface SpaceEvents { @@ -19,4 +20,12 @@ sealed interface SpaceEvents { data class ShowTopicViewer(val topic: String) : 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 } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt index 309747d2c9..abcb4b444e 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt @@ -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.spaces.SpaceRoom 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 kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -48,6 +49,8 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlin.jvm.optionals.getOrNull @@ -62,6 +65,7 @@ class SpacePresenter( private val acceptDeclineInvitePresenter: Presenter, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, private val featureFlagService: FeatureFlagService, + private val spaceService: SpaceService, ) : Presenter { private var children by mutableStateOf>(persistentListOf()) @@ -104,6 +108,11 @@ class SpacePresenter( var topicViewerState: TopicViewerState by remember { mutableStateOf(TopicViewerState.Hidden) } + // Manage mode state + var isManageMode by remember { mutableStateOf(false) } + var selectedRoomIds by remember { mutableStateOf>(emptySet()) } + var removeRoomsAction by remember { mutableStateOf>(AsyncAction.Uninitialized) } + LaunchedEffect(children) { // Remove joined children from the join actions val joinedChildren = children @@ -138,6 +147,46 @@ class SpacePresenter( } SpaceEvents.HideTopicViewer -> topicViewerState = TopicViewerState.Hidden 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( @@ -150,6 +199,10 @@ class SpacePresenter( acceptDeclineInviteState = acceptDeclineInviteState, topicViewerState = topicViewerState, canAccessSpaceSettings = canAccessSpaceSettings, + isManageMode = isManageMode, + selectedRoomIds = selectedRoomIds.toImmutableSet(), + canManageRooms = permissions.canManageRooms, + removeRoomsAction = removeRoomsAction, eventSink = ::handleEvent, ) } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt index cceda62806..e33d4730d1 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt @@ -27,12 +27,20 @@ data class SpaceState( val acceptDeclineInviteState: AcceptDeclineInviteState, val topicViewerState: TopicViewerState, val canAccessSpaceSettings: Boolean, + val isManageMode: Boolean, + val selectedRoomIds: ImmutableSet, + val canManageRooms: Boolean, + val removeRoomsAction: AsyncAction, val eventSink: (SpaceEvents) -> Unit ) { fun isJoining(spaceId: RoomId): Boolean = joinActions[spaceId] == AsyncAction.Loading val hasAnyFailure: Boolean = joinActions.values.any { it is AsyncAction.Failure } + + val showManageRoomsAction: Boolean = canManageRooms && children.isNotEmpty() + val selectedCount: Int = selectedRoomIds.size + val isRemoveButtonEnabled: Boolean = selectedRoomIds.isNotEmpty() } @Immutable diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt index 52894ad599..ea59bf6e2e 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt @@ -39,7 +39,26 @@ open class SpaceStateProvider : PreviewParameterProvider { aSpaceState( 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(), topicViewerState: TopicViewerState = TopicViewerState.Hidden, canAccessSpaceSettings: Boolean = true, + isManageMode: Boolean = false, + selectedRoomIds: Set = emptySet(), + canManageRooms: Boolean = true, + removeRoomsAction: AsyncAction = AsyncAction.Uninitialized, eventSink: (SpaceEvents) -> Unit = { }, ) = SpaceState( currentSpace = parentSpace, @@ -65,6 +88,10 @@ fun aSpaceState( acceptDeclineInviteState = acceptDeclineInviteState, topicViewerState = topicViewerState, canAccessSpaceSettings = canAccessSpaceSettings, + isManageMode = isManageMode, + selectedRoomIds = selectedRoomIds.toImmutableSet(), + canManageRooms = canManageRooms, + removeRoomsAction = removeRoomsAction, eventSink = eventSink, ) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt index 769b608e8e..56c932f968 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt @@ -40,7 +40,10 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.libraries.architecture.AsyncAction 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.SimpleModalBottomSheet 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.DropdownMenu 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.Icon 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.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar @@ -88,15 +93,26 @@ fun SpaceView( Scaffold( modifier = modifier, topBar = { - SpaceViewTopBar( - currentSpace = state.currentSpace, - canAccessSpaceSettings = state.canAccessSpaceSettings, - onBackClick = onBackClick, - onLeaveSpaceClick = onLeaveSpaceClick, - onShareSpace = onShareSpace, - onSettingsClick = onSettingsClick, - onViewMembersClick = onViewMembersClick, - ) + if (state.isManageMode) { + ManageModeTopBar( + selectedCount = state.selectedCount, + isRemoveButtonEnabled = state.isRemoveButtonEnabled, + onCancelClick = { state.eventSink(SpaceEvents.ExitManageMode) }, + onRemoveClick = { state.eventSink(SpaceEvents.RemoveSelectedRooms) }, + ) + } 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 -> Box( @@ -104,7 +120,13 @@ fun SpaceView( ) { SpaceViewContent( state = state, - onRoomClick = onRoomClick, + onRoomClick = { spaceRoom -> + if (state.isManageMode) { + state.eventSink(SpaceEvents.ToggleRoomSelection(spaceRoom.roomId)) + } else { + onRoomClick(spaceRoom) + } + }, onTopicClick = { 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 @@ -200,6 +230,7 @@ private fun SpaceViewContent( ) { index, spaceRoom -> val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED val isCurrentlyJoining = state.isJoining(spaceRoom.roomId) + val isSelected = spaceRoom.roomId in state.selectedRoomIds SpaceRoomItemView( spaceRoom = spaceRoom, showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites, @@ -210,17 +241,30 @@ private fun SpaceViewContent( onLongClick = { // TODO }, - trailingAction = spaceRoom.trailingAction(isCurrentlyJoining = isCurrentlyJoining) { - state.eventSink(SpaceEvents.Join(spaceRoom)) - }, - bottomAction = spaceRoom.inviteButtons( - onAcceptClick = { - state.eventSink(SpaceEvents.AcceptInvite(spaceRoom)) - }, - onDeclineClick = { - state.eventSink(SpaceEvents.DeclineInvite(spaceRoom)) + trailingAction = if (state.isManageMode) { + { + Checkbox( + checked = isSelected, + onCheckedChange = null, + ) } - ) + } 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) { HorizontalDivider() @@ -259,11 +303,13 @@ private fun LoadingMoreIndicator( private fun SpaceViewTopBar( currentSpace: SpaceRoom?, canAccessSpaceSettings: Boolean, + showManageRoomsAction: Boolean, onBackClick: () -> Unit, onLeaveSpaceClick: () -> Unit, onSettingsClick: () -> Unit, onShareSpace: () -> Unit, onViewMembersClick: () -> Unit, + onManageRoomsClick: () -> Unit, modifier: Modifier = Modifier, ) { TopAppBar( @@ -313,6 +359,16 @@ private fun SpaceViewTopBar( onShareSpace() } ) + if (showManageRoomsAction) { + SpaceMenuItem( + titleRes = CommonStrings.action_manage_rooms, + icon = CompoundIcons.Edit(), + onClick = { + showMenu = false + onManageRoomsClick() + } + ) + } if (canAccessSpaceSettings) { SpaceMenuItem( 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 private fun SpaceMenuItem( @StringRes titleRes: Int, @@ -425,6 +514,34 @@ private fun SpaceRoom.inviteButtons( } } +@Composable +private fun RemoveRoomsConfirmationDialog( + removeRoomsAction: AsyncAction, + 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 @Composable internal fun SpaceViewPreview( diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsPermissions.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsPermissions.kt index e3ec70a51d..8297992b49 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsPermissions.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsPermissions.kt @@ -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.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.powerlevels.RoomPermissions import io.element.android.libraries.matrix.api.room.powerlevels.canEditRolesAndPermissions @@ -19,6 +20,7 @@ data class SpaceSettingsPermissions( val editDetailsPermissions: RoomDetailsEditPermissions, val canEditRolesAndPermissions: Boolean, val securityAndPrivacyPermissions: SecurityAndPrivacyPermissions, + val canManageRooms: Boolean, ) { fun hasAny(joinRule: JoinRule?): Boolean { return editDetailsPermissions.hasAny || @@ -31,6 +33,7 @@ data class SpaceSettingsPermissions( editDetailsPermissions = RoomDetailsEditPermissions.DEFAULT, canEditRolesAndPermissions = false, securityAndPrivacyPermissions = SecurityAndPrivacyPermissions.DEFAULT, + canManageRooms = false, ) } } @@ -40,5 +43,6 @@ fun RoomPermissions.spaceSettingsPermissions(): SpaceSettingsPermissions { editDetailsPermissions = roomDetailsEditPermissions(), canEditRolesAndPermissions = canEditRolesAndPermissions(), securityAndPrivacyPermissions = securityAndPrivacyPermissions(), + canManageRooms = canOwnUserSendState(StateEventType.SpaceChild), ) } diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt index 917aceb262..65a4c76783 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt @@ -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.join.JoinRoom 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.A_ROOM_ID 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.powerlevels.FakeRoomPermissions 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.tests.testutils.EventsRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder @@ -365,6 +367,7 @@ class SpacePresenterTest { ), acceptDeclineInvitePresenter: Presenter = Presenter { anAcceptDeclineInviteState() }, spaceSettingsEnabled: Boolean = false, + spaceService: FakeSpaceService = FakeSpaceService(), ): SpacePresenter { return SpacePresenter( client = client, @@ -379,6 +382,7 @@ class SpacePresenterTest { FeatureFlags.SpaceSettings.key to spaceSettingsEnabled, ) ), + spaceService = spaceService, ) } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt index 6f5ba674ec..a89bb2da86 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt @@ -18,4 +18,12 @@ interface SpaceService { fun spaceRoomList(id: RoomId): SpaceRoomList 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 } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt index ba816c11c7..41aae086fa 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt @@ -79,6 +79,12 @@ class RustSpaceService( } } + override suspend fun removeChildFromSpace(spaceId: RoomId, childId: RoomId): Result = withContext(sessionDispatcher) { + runCatchingExceptions { + innerSpaceService.removeChildFromSpace(spaceId.value, childId.value) + } + } + init { innerSpaceService .spaceListUpdate() diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/spaces/FakeSpaceService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/spaces/FakeSpaceService.kt index eaa36ee750..539bb0ce6f 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/spaces/FakeSpaceService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/spaces/FakeSpaceService.kt @@ -23,6 +23,7 @@ class FakeSpaceService( private val joinedSpacesResult: () -> Result> = { lambdaError() }, private val spaceRoomListResult: (RoomId) -> SpaceRoomList = { lambdaError() }, private val leaveSpaceHandleResult: (RoomId) -> LeaveSpaceHandle = { lambdaError() }, + private val removeChildFromSpaceResult: (RoomId, RoomId) -> Result = { _, _ -> lambdaError() }, ) : SpaceService { private val _spaceRoomsFlow = MutableSharedFlow>() override val spaceRoomsFlow: SharedFlow> @@ -43,4 +44,8 @@ class FakeSpaceService( override fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle { return leaveSpaceHandleResult(spaceId) } + + override suspend fun removeChildFromSpace(spaceId: RoomId, childId: RoomId): Result = simulateLongTask { + removeChildFromSpaceResult(spaceId, childId) + } } diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index df47b71577..f5c3c3f12f 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -112,6 +112,7 @@ "Load more" "Manage account" "Manage devices" + "Manage rooms" "Message" "Minimise" "Next" From df3fe6d6d68cfe1cef597a51869a2b916ba9bfd2 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 14 Jan 2026 15:27:06 +0100 Subject: [PATCH 02/15] Fix wrong param order for removeChildFromSpace --- .../android/libraries/matrix/impl/spaces/RustSpaceService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt index 41aae086fa..17a9186351 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt @@ -81,7 +81,7 @@ class RustSpaceService( override suspend fun removeChildFromSpace(spaceId: RoomId, childId: RoomId): Result = withContext(sessionDispatcher) { runCatchingExceptions { - innerSpaceService.removeChildFromSpace(spaceId.value, childId.value) + innerSpaceService.removeChildFromSpace(childId = childId.value, spaceId = spaceId.value) } } From f50165807daaa7dde5550775367077b294eef137 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 14 Jan 2026 15:29:18 +0100 Subject: [PATCH 03/15] Iterate on removing space child rooms. --- .../space/impl/root/SpacePresenter.kt | 34 ++++++++++++++++--- .../features/space/impl/root/SpaceState.kt | 3 +- .../features/space/impl/root/SpaceView.kt | 27 ++++++++++++--- 3 files changed, 54 insertions(+), 10 deletions(-) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt index abcb4b444e..e18f086834 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt @@ -112,6 +112,23 @@ class SpacePresenter( var isManageMode by remember { mutableStateOf(false) } var selectedRoomIds by remember { mutableStateOf>(emptySet()) } var removeRoomsAction by remember { mutableStateOf>(AsyncAction.Uninitialized) } + var removedRoomIds by remember { mutableStateOf>(emptySet()) } + + val filteredChildren by remember { + derivedStateOf { + children + .filterNot { it.roomId in removedRoomIds } + .let { list -> + if (isManageMode) { + // In manage mode, only show rooms (not spaces) + list.filter { !it.isSpace } + } else { + list + } + } + .toImmutableList() + } + } LaunchedEffect(children) { // Remove joined children from the join actions @@ -171,10 +188,19 @@ class SpacePresenter( localCoroutineScope.launch { removeRoomsAction = AsyncAction.Loading val spaceId = spaceRoomList.roomId - val results = selectedRoomIds.map { roomId -> - async { spaceService.removeChildFromSpace(spaceId, roomId) } + val roomsToRemove = selectedRoomIds.toSet() + val successfullyRemoved = mutableSetOf() + val results = roomsToRemove.map { roomId -> + async { + spaceService.removeChildFromSpace(spaceId, roomId) + .onSuccess { successfullyRemoved.add(roomId) } + } } - val hasError = results.awaitAll().any { it.isFailure } + results.awaitAll() + if (successfullyRemoved.isNotEmpty()) { + removedRoomIds = removedRoomIds + successfullyRemoved + } + val hasError = successfullyRemoved.size < roomsToRemove.size if (hasError) { removeRoomsAction = AsyncAction.Failure(Exception("Failed to remove some rooms")) } else { @@ -191,7 +217,7 @@ class SpacePresenter( } return SpaceState( currentSpace = currentSpace.getOrNull(), - children = children, + children = filteredChildren, seenSpaceInvites = seenSpaceInvites, hideInvitesAvatar = hideInvitesAvatar, hasMoreToLoad = hasMoreToLoad, diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt index e33d4730d1..05004ddace 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt @@ -34,11 +34,12 @@ data class SpaceState( val eventSink: (SpaceEvents) -> Unit ) { fun isJoining(spaceId: RoomId): Boolean = joinActions[spaceId] == AsyncAction.Loading + fun isSelected(spaceId: RoomId): Boolean = selectedRoomIds.contains(spaceId) val hasAnyFailure: Boolean = joinActions.values.any { it is AsyncAction.Failure } - val showManageRoomsAction: Boolean = canManageRooms && children.isNotEmpty() + val showManageRoomsAction: Boolean = canManageRooms && children.any { spaceRoom -> !spaceRoom.isSpace } val selectedCount: Int = selectedRoomIds.size val isRemoveButtonEnabled: Boolean = selectedRoomIds.isNotEmpty() } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt index 56c932f968..541c225b99 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt @@ -8,6 +8,7 @@ package io.element.android.features.space.impl.root +import androidx.activity.compose.BackHandler import androidx.annotation.StringRes import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -42,10 +43,9 @@ import io.element.android.compound.theme.ElementTheme 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.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.SimpleModalBottomSheet +import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.components.async.AsyncIndicator import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost import io.element.android.libraries.designsystem.components.async.rememberAsyncIndicatorState @@ -54,18 +54,19 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Checkbox 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.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.Icon 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.Text +import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.spaces.SpaceRoom @@ -90,6 +91,15 @@ fun SpaceView( modifier: Modifier = Modifier, acceptDeclineInviteView: @Composable () -> Unit, ) { + + BackHandler { + if (state.isManageMode) { + state.eventSink(SpaceEvents.ExitManageMode) + } else { + onBackClick() + } + } + Scaffold( modifier = modifier, topBar = { @@ -230,7 +240,7 @@ private fun SpaceViewContent( ) { index, spaceRoom -> val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED val isCurrentlyJoining = state.isJoining(spaceRoom.roomId) - val isSelected = spaceRoom.roomId in state.selectedRoomIds + val isSelected = state.isSelected(spaceRoom.roomId) SpaceRoomItemView( spaceRoom = spaceRoom, showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites, @@ -530,6 +540,13 @@ private fun RemoveRoomsConfirmationDialog( onSubmitClick = onConfirm, onDismiss = onDismiss, destructiveSubmit = true, + icon = { + Icon( + imageVector = CompoundIcons.Error(), + tint = ElementTheme.colors.textCriticalPrimary, + contentDescription = null + ) + } ) } else -> { From 15c7cbade6d3ca22a548128e3e1c2f50035aabcb Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 14 Jan 2026 16:39:57 +0100 Subject: [PATCH 04/15] Animate transition from/to space manage rooms mode. --- .../features/space/impl/root/SpaceView.kt | 91 ++++++++++++------- 1 file changed, 58 insertions(+), 33 deletions(-) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt index 541c225b99..c733029d51 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt @@ -10,9 +10,17 @@ package io.element.android.features.space.impl.root import androidx.activity.compose.BackHandler import androidx.annotation.StringRes +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.animation.slideIn +import androidx.compose.animation.veilOut import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -103,25 +111,36 @@ fun SpaceView( Scaffold( modifier = modifier, topBar = { - if (state.isManageMode) { - ManageModeTopBar( - selectedCount = state.selectedCount, - isRemoveButtonEnabled = state.isRemoveButtonEnabled, - onCancelClick = { state.eventSink(SpaceEvents.ExitManageMode) }, - onRemoveClick = { state.eventSink(SpaceEvents.RemoveSelectedRooms) }, - ) - } 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) }, - ) + Box { + AnimatedVisibility( + visible = state.isManageMode, + enter = fadeIn(), + exit = fadeOut() + ) { + ManageModeTopBar( + selectedCount = state.selectedCount, + isRemoveButtonEnabled = state.isRemoveButtonEnabled, + onCancelClick = { state.eventSink(SpaceEvents.ExitManageMode) }, + onRemoveClick = { state.eventSink(SpaceEvents.RemoveSelectedRooms) }, + ) + } + AnimatedVisibility( + visible = !state.isManageMode, + enter = fadeIn(), + exit = fadeOut() + ) { + 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 -> @@ -218,20 +237,26 @@ private fun SpaceViewContent( LazyColumn(modifier.fillMaxSize()) { val currentSpace = state.currentSpace if (currentSpace != null) { - item { - SpaceHeaderView( - avatarData = currentSpace.getAvatarData(AvatarSize.SpaceHeader), - name = currentSpace.displayName, - topic = currentSpace.topic, - topicMaxLines = 2, - visibility = currentSpace.visibility, - heroes = currentSpace.heroes.toImmutableList(), - numberOfMembers = currentSpace.numJoinedMembers, - onTopicClick = onTopicClick - ) - } - item { - HorizontalDivider() + item(key = "space_header") { + AnimatedVisibility( + !state.isManageMode, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + Column { + SpaceHeaderView( + avatarData = currentSpace.getAvatarData(AvatarSize.SpaceHeader), + name = currentSpace.displayName, + topic = currentSpace.topic, + topicMaxLines = 2, + visibility = currentSpace.visibility, + heroes = currentSpace.heroes.toImmutableList(), + numberOfMembers = currentSpace.numJoinedMembers, + onTopicClick = onTopicClick + ) + HorizontalDivider() + } + } } } itemsIndexed( From 525e9b5d5078ff4e95d65c48b8e5d712d24e5c6c Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 14 Jan 2026 17:46:59 +0100 Subject: [PATCH 05/15] Hide unread count in manage space rooms mode --- .../io/element/android/features/space/impl/root/SpaceView.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt index c733029d51..31e70c5ff7 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt @@ -266,9 +266,10 @@ private fun SpaceViewContent( val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED val isCurrentlyJoining = state.isJoining(spaceRoom.roomId) val isSelected = state.isSelected(spaceRoom.roomId) + val showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites && !state.isManageMode SpaceRoomItemView( spaceRoom = spaceRoom, - showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites, + showUnreadIndicator = showUnreadIndicator, hideAvatars = isInvitation && state.hideInvitesAvatar, onClick = { onRoomClick(spaceRoom) From 8b8151722a9908dcf6fa8b4cafa7c76377f715bc Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 14 Jan 2026 17:47:20 +0100 Subject: [PATCH 06/15] Add tests for space manage rooms mode --- .../space/impl/root/SpacePresenterTest.kt | 211 +++++++++++++++++- .../space/impl/root/SpaceStateTest.kt | 78 +++++++ .../features/space/impl/root/SpaceViewTest.kt | 68 ++++++ 3 files changed, 356 insertions(+), 1 deletion(-) diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt index 65a4c76783..08fb977d1b 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt @@ -22,16 +22,18 @@ import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias 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.RoomType 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.SpaceService 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_2 +import io.element.android.libraries.matrix.test.A_ROOM_ID_3 import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom @@ -355,6 +357,213 @@ class SpacePresenterTest { } } + @Test + fun `present - enter manage mode`() = runTest { + val presenter = createSpacePresenter() + presenter.test { + val state = awaitItem() + assertThat(state.isManageMode).isFalse() + state.eventSink(SpaceEvents.EnterManageMode) + val manageModeState = awaitItem() + assertThat(manageModeState.isManageMode).isTrue() + assertThat(manageModeState.selectedRoomIds).isEmpty() + } + } + + @Test + fun `present - exit manage mode clears selection`() = runTest { + val presenter = createSpacePresenter() + presenter.test { + val initialState = awaitItem() + initialState.eventSink(SpaceEvents.EnterManageMode) + initialState.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID)) + initialState.eventSink(SpaceEvents.ExitManageMode) + val finalState = expectMostRecentItem() + assertThat(finalState.isManageMode).isFalse() + assertThat(finalState.selectedRoomIds).isEmpty() + } + } + + @Test + fun `present - toggle room selection`() = runTest { + val presenter = createSpacePresenter() + presenter.test { + val initialState = awaitItem() + initialState.eventSink(SpaceEvents.EnterManageMode) + // Select a room + initialState.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID)) + var latestState = expectMostRecentItem() + assertThat(latestState.selectedRoomIds).containsExactly(A_ROOM_ID) + // Deselect the room + latestState.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID)) + latestState = expectMostRecentItem() + assertThat(latestState.selectedRoomIds).isEmpty() + } + } + + @Test + fun `present - remove rooms success`() = runTest { + val removeChildFromSpaceResult = lambdaRecorder> { _, _ -> + Result.success(Unit) + } + val aRoom = aSpaceRoom( + roomId = A_ROOM_ID, + roomType = RoomType.Room, + ) + val fakeSpaceRoomList = FakeSpaceRoomList( + initialSpaceRoomsValue = listOf(aRoom), + paginateResult = { Result.success(Unit) }, + ) + val presenter = createSpacePresenter( + spaceRoomList = fakeSpaceRoomList, + spaceService = FakeSpaceService( + removeChildFromSpaceResult = removeChildFromSpaceResult, + ), + ) + presenter.test { + awaitItem() // Initial empty state + advanceUntilIdle() + val stateWithChildren = awaitItem() + assertThat(stateWithChildren.children).hasSize(1) + stateWithChildren.eventSink(SpaceEvents.EnterManageMode) + stateWithChildren.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID)) + stateWithChildren.eventSink(SpaceEvents.RemoveSelectedRooms) + stateWithChildren.eventSink(SpaceEvents.ConfirmRoomRemoval) + advanceUntilIdle() + val successState = expectMostRecentItem() + assertThat(successState.removeRoomsAction).isEqualTo(AsyncAction.Success(Unit)) + assertThat(successState.isManageMode).isFalse() + assertThat(successState.children).isEmpty() + removeChildFromSpaceResult.assertions().isCalledOnce() + } + } + + @Test + fun `present - remove rooms partial failure`() = runTest { + val aRoom1 = aSpaceRoom( + roomId = A_ROOM_ID, + roomType = RoomType.Room, + ) + val aRoom2 = aSpaceRoom( + roomId = A_ROOM_ID_2, + roomType = RoomType.Room, + ) + val removeChildFromSpaceResult = lambdaRecorder> { _, childId -> + if (childId == A_ROOM_ID_2) Result.failure(AN_EXCEPTION) + else Result.success(Unit) + } + val fakeSpaceRoomList = FakeSpaceRoomList( + initialSpaceRoomsValue = listOf(aRoom1, aRoom2), + paginateResult = { Result.success(Unit) }, + ) + val presenter = createSpacePresenter( + spaceRoomList = fakeSpaceRoomList, + spaceService = FakeSpaceService( + removeChildFromSpaceResult = removeChildFromSpaceResult, + ), + ) + presenter.test { + awaitItem() // Initial empty state + advanceUntilIdle() + val stateWithChildren = awaitItem() + assertThat(stateWithChildren.children).hasSize(2) + stateWithChildren.eventSink(SpaceEvents.EnterManageMode) + stateWithChildren.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID)) + stateWithChildren.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID_2)) + stateWithChildren.eventSink(SpaceEvents.RemoveSelectedRooms) + stateWithChildren.eventSink(SpaceEvents.ConfirmRoomRemoval) + advanceUntilIdle() + val failureState = expectMostRecentItem() + assertThat(failureState.removeRoomsAction.isFailure()).isTrue() + // Successfully removed room should be filtered out + assertThat(failureState.children.map { it.roomId }).doesNotContain(A_ROOM_ID) + // Failed room should still be present + assertThat(failureState.children.map { it.roomId }).contains(A_ROOM_ID_2) + removeChildFromSpaceResult.assertions().isCalledExactly(2) + } + } + + @Test + fun `present - children filtered in manage mode shows only rooms`() = runTest { + val aRoom = aSpaceRoom( + roomId = A_ROOM_ID, + roomType = RoomType.Room, + ) + val aSubSpace = aSpaceRoom( + roomId = A_ROOM_ID_2, + roomType = RoomType.Space, + ) + val fakeSpaceRoomList = FakeSpaceRoomList( + initialSpaceRoomsValue = listOf(aRoom, aSubSpace), + paginateResult = { Result.success(Unit) }, + ) + val presenter = createSpacePresenter(spaceRoomList = fakeSpaceRoomList) + presenter.test { + awaitItem() // Initial empty state + advanceUntilIdle() + val stateWithChildren = awaitItem() + // Both room and space visible initially + assertThat(stateWithChildren.children).hasSize(2) + assertThat(stateWithChildren.isManageMode).isFalse() + stateWithChildren.eventSink(SpaceEvents.EnterManageMode) + val manageModeState = expectMostRecentItem() + // Only rooms visible in manage mode + assertThat(manageModeState.children).hasSize(1) + assertThat(manageModeState.children.first().roomId).isEqualTo(A_ROOM_ID) + assertThat(manageModeState.children.first().isSpace).isFalse() + } + } + + @Test + fun `present - removed rooms persist after flow update`() = runTest { + val removeChildFromSpaceResult = lambdaRecorder> { _, _ -> + Result.success(Unit) + } + val aRoom1 = aSpaceRoom( + roomId = A_ROOM_ID, + roomType = RoomType.Room, + ) + val aRoom2 = aSpaceRoom( + roomId = A_ROOM_ID_2, + roomType = RoomType.Room, + ) + val aRoom3 = aSpaceRoom( + roomId = A_ROOM_ID_3, + roomType = RoomType.Room, + ) + val spaceRoomList = FakeSpaceRoomList( + initialSpaceRoomsValue = listOf(aRoom1, aRoom2), + paginateResult = { Result.success(Unit) }, + ) + val presenter = createSpacePresenter( + spaceRoomList = spaceRoomList, + spaceService = FakeSpaceService( + removeChildFromSpaceResult = removeChildFromSpaceResult, + ), + ) + presenter.test { + awaitItem() // Initial empty state + advanceUntilIdle() + val stateWithChildren = awaitItem() + stateWithChildren.eventSink(SpaceEvents.EnterManageMode) + stateWithChildren.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID)) + stateWithChildren.eventSink(SpaceEvents.RemoveSelectedRooms) + stateWithChildren.eventSink(SpaceEvents.ConfirmRoomRemoval) + advanceUntilIdle() + val successState = expectMostRecentItem() + assertThat(successState.children.map { it.roomId }).doesNotContain(A_ROOM_ID) + // Emit new flow update with a new room added (simulating server refresh) + spaceRoomList.emitSpaceRooms(listOf(aRoom1, aRoom2, aRoom3)) + advanceUntilIdle() + val afterFlowUpdate = awaitItem() + // A_ROOM_ID should still be filtered out even though it's in the new emission + assertThat(afterFlowUpdate.children.map { it.roomId }).doesNotContain(A_ROOM_ID) + // But the other rooms should be present + assertThat(afterFlowUpdate.children.map { it.roomId }).contains(A_ROOM_ID_2) + assertThat(afterFlowUpdate.children.map { it.roomId }).contains(A_ROOM_ID_3) + } + } + private fun TestScope.createSpacePresenter( client: MatrixClient = FakeMatrixClient(), room: BaseRoom = FakeBaseRoom(), diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceStateTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceStateTest.kt index 440ec1b6a5..a0c3635baf 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceStateTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceStateTest.kt @@ -14,6 +14,8 @@ 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_2 import io.element.android.libraries.matrix.test.A_ROOM_ID_3 +import io.element.android.libraries.matrix.api.room.RoomType +import io.element.android.libraries.previewutils.room.aSpaceRoom import org.junit.Test class SpaceStateTest { @@ -45,4 +47,80 @@ class SpaceStateTest { ) assertThat(state.isJoining(A_ROOM_ID)).isTrue() } + + @Test + fun `test isSelected returns true for selected room`() { + val state = aSpaceState( + selectedRoomIds = setOf(A_ROOM_ID) + ) + assertThat(state.isSelected(A_ROOM_ID)).isTrue() + } + + @Test + fun `test isSelected returns false for non-selected room`() { + val state = aSpaceState( + selectedRoomIds = setOf(A_ROOM_ID) + ) + assertThat(state.isSelected(A_ROOM_ID_2)).isFalse() + } + + @Test + fun `test showManageRoomsAction true when canManageRooms and has room children`() { + val state = aSpaceState( + canManageRooms = true, + children = listOf(aSpaceRoom(roomType = RoomType.Room)) + ) + assertThat(state.showManageRoomsAction).isTrue() + } + + @Test + fun `test showManageRoomsAction false when canManageRooms but children empty`() { + val state = aSpaceState( + canManageRooms = true, + children = emptyList() + ) + assertThat(state.showManageRoomsAction).isFalse() + } + + @Test + fun `test showManageRoomsAction false when canManageRooms but only space children`() { + val state = aSpaceState( + canManageRooms = true, + children = listOf(aSpaceRoom(roomType = RoomType.Space)) + ) + assertThat(state.showManageRoomsAction).isFalse() + } + + @Test + fun `test showManageRoomsAction false when has room children but canManageRooms false`() { + val state = aSpaceState( + canManageRooms = false, + children = listOf(aSpaceRoom(roomType = RoomType.Room)) + ) + assertThat(state.showManageRoomsAction).isFalse() + } + + @Test + fun `test selectedCount returns correct count`() { + val state = aSpaceState( + selectedRoomIds = setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3) + ) + assertThat(state.selectedCount).isEqualTo(3) + } + + @Test + fun `test isRemoveButtonEnabled true when selectedRoomIds not empty`() { + val state = aSpaceState( + selectedRoomIds = setOf(A_ROOM_ID) + ) + assertThat(state.isRemoveButtonEnabled).isTrue() + } + + @Test + fun `test isRemoveButtonEnabled false when selectedRoomIds empty`() { + val state = aSpaceState( + selectedRoomIds = emptySet() + ) + assertThat(state.isRemoveButtonEnabled).isFalse() + } } diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt index 406b5d17e8..3ef5151a50 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt @@ -12,9 +12,11 @@ import androidx.activity.ComponentActivity import androidx.compose.runtime.Composable import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.spaces.SpaceRoom import io.element.android.libraries.matrix.test.A_ROOM_ID @@ -29,6 +31,7 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.pressBack +import io.element.android.tests.testutils.pressBackKey import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule @@ -132,6 +135,71 @@ class SpaceViewTest { rule.onNodeWithText(A_ROOM_TOPIC).performClick() eventsRecorder.assertSingle(SpaceEvents.ShowTopicViewer(A_ROOM_TOPIC)) } + + @Test + fun `clicking back in manage mode emits ExitManageMode event`() { + val eventsRecorder = EventsRecorder() + rule.setSpaceView( + aSpaceState( + hasMoreToLoad = false, + isManageMode = true, + eventSink = eventsRecorder, + ) + ) + rule.pressBackKey() + eventsRecorder.assertSingle(SpaceEvents.ExitManageMode) + } + + @Test + fun `clicking on room in manage mode emits ToggleRoomSelection event`() { + val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, displayName = A_ROOM_NAME) + val eventsRecorder = EventsRecorder() + rule.setSpaceView( + aSpaceState( + children = listOf(aSpaceRoom), + hasMoreToLoad = false, + isManageMode = true, + eventSink = eventsRecorder, + ) + ) + rule.onNodeWithText(A_ROOM_NAME).performClick() + eventsRecorder.assertSingle(SpaceEvents.ToggleRoomSelection(A_ROOM_ID)) + } + + @Test + fun `clicking remove button emits RemoveSelectedRooms event`() { + val eventsRecorder = EventsRecorder() + rule.setSpaceView( + aSpaceState( + children = listOf(aSpaceRoom(roomId = A_ROOM_ID)), + hasMoreToLoad = false, + isManageMode = true, + selectedRoomIds = setOf(A_ROOM_ID), + eventSink = eventsRecorder, + ) + ) + rule.clickOn(CommonStrings.action_remove) + eventsRecorder.assertSingle(SpaceEvents.RemoveSelectedRooms) + } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking confirm in removal dialog emits ConfirmRoomRemoval event`() { + val eventsRecorder = EventsRecorder() + rule.setSpaceView( + aSpaceState( + children = listOf(aSpaceRoom(roomId = A_ROOM_ID)), + hasMoreToLoad = false, + isManageMode = true, + selectedRoomIds = setOf(A_ROOM_ID), + removeRoomsAction = AsyncAction.ConfirmingNoParams, + eventSink = eventsRecorder, + ) + ) + // Click on the Remove button in the confirmation dialog + rule.clickOn(CommonStrings.action_remove, inDialog = true) + eventsRecorder.assertSingle(SpaceEvents.ConfirmRoomRemoval) + } } private fun AndroidComposeTestRule.setSpaceView( From 57b89d241f928d21a0d0f60ec60671800d09205d Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 15 Jan 2026 12:14:08 +0100 Subject: [PATCH 07/15] Add proper localization for space room removal UI --- .../space/impl/root/SpacePresenter.kt | 1 + .../features/space/impl/root/SpaceState.kt | 5 +- .../space/impl/root/SpaceStateProvider.kt | 3 +- .../features/space/impl/root/SpaceView.kt | 62 ++++++++++--------- .../src/main/res/values-bg/translations.xml | 1 + .../src/main/res/values-cs/translations.xml | 1 + .../src/main/res/values-da/translations.xml | 1 + .../src/main/res/values-de/translations.xml | 6 ++ .../src/main/res/values-et/translations.xml | 1 + .../src/main/res/values-fa/translations.xml | 1 + .../src/main/res/values-fi/translations.xml | 1 + .../src/main/res/values-fr/translations.xml | 1 + .../src/main/res/values-hr/translations.xml | 1 + .../src/main/res/values-hu/translations.xml | 1 + .../src/main/res/values-it/translations.xml | 1 + .../src/main/res/values-nb/translations.xml | 1 + .../main/res/values-pt-rBR/translations.xml | 1 + .../src/main/res/values-ro/translations.xml | 1 + .../src/main/res/values-ru/translations.xml | 1 + .../src/main/res/values-sk/translations.xml | 1 + .../main/res/values-zh-rTW/translations.xml | 1 + .../src/main/res/values-zh/translations.xml | 1 + .../impl/src/main/res/values/localazy.xml | 6 ++ .../space/impl/root/SpaceStateTest.kt | 4 +- .../src/main/res/values-bg/translations.xml | 1 - .../src/main/res/values-cs/translations.xml | 1 - .../src/main/res/values-da/translations.xml | 1 - .../src/main/res/values-de/translations.xml | 6 -- .../src/main/res/values-et/translations.xml | 1 - .../src/main/res/values-fa/translations.xml | 1 - .../src/main/res/values-fi/translations.xml | 1 - .../src/main/res/values-fr/translations.xml | 1 - .../src/main/res/values-hr/translations.xml | 1 - .../src/main/res/values-hu/translations.xml | 1 - .../src/main/res/values-it/translations.xml | 1 - .../src/main/res/values-nb/translations.xml | 1 - .../main/res/values-pt-rBR/translations.xml | 1 - .../src/main/res/values-ro/translations.xml | 1 - .../src/main/res/values-ru/translations.xml | 1 - .../src/main/res/values-sk/translations.xml | 1 - .../main/res/values-zh-rTW/translations.xml | 1 - .../src/main/res/values-zh/translations.xml | 1 - .../src/main/res/values/localazy.xml | 10 ++- tools/localazy/config.json | 3 +- 44 files changed, 77 insertions(+), 63 deletions(-) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt index e18f086834..43febab8c1 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt @@ -216,6 +216,7 @@ class SpacePresenter( } } return SpaceState( + currentSpaceId = spaceRoomList.roomId, currentSpace = currentSpace.getOrNull(), children = filteredChildren, seenSpaceInvites = seenSpaceInvites, diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt index 05004ddace..28b04a9a21 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt @@ -18,6 +18,7 @@ import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.ImmutableSet data class SpaceState( + private val currentSpaceId: RoomId, val currentSpace: SpaceRoom?, val children: ImmutableList, val seenSpaceInvites: ImmutableSet, @@ -35,10 +36,12 @@ data class SpaceState( ) { fun isJoining(spaceId: RoomId): Boolean = joinActions[spaceId] == AsyncAction.Loading fun isSelected(spaceId: RoomId): Boolean = selectedRoomIds.contains(spaceId) - val hasAnyFailure: Boolean = joinActions.values.any { + val hasAnyJoinFailures: Boolean = joinActions.values.any { it is AsyncAction.Failure } + val currentSpaceDisplayName = currentSpace?.displayName ?: currentSpaceId.value + val showManageRoomsAction: Boolean = canManageRooms && children.any { spaceRoom -> !spaceRoom.isSpace } val selectedCount: Int = selectedRoomIds.size val isRemoveButtonEnabled: Boolean = selectedRoomIds.isNotEmpty() diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt index ea59bf6e2e..d70cf1f1b9 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt @@ -63,7 +63,7 @@ open class SpaceStateProvider : PreviewParameterProvider { } fun aSpaceState( - parentSpace: SpaceRoom? = aParentSpace(), + parentSpace: SpaceRoom = aParentSpace(), children: List = emptyList(), seenSpaceInvites: Set = emptySet(), joiningRooms: Set = emptySet(), @@ -79,6 +79,7 @@ fun aSpaceState( removeRoomsAction: AsyncAction = AsyncAction.Uninitialized, eventSink: (SpaceEvents) -> Unit = { }, ) = SpaceState( + currentSpaceId = parentSpace.roomId, currentSpace = parentSpace, children = children.toImmutableList(), seenSpaceInvites = seenSpaceInvites.toImmutableSet(), diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt index 31e70c5ff7..cb19636dfd 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt @@ -15,8 +15,6 @@ import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically -import androidx.compose.animation.slideIn -import androidx.compose.animation.veilOut import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box @@ -40,6 +38,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics @@ -49,6 +48,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.space.impl.R import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.atomic.molecules.InviteButtonsRowMolecule import io.element.android.libraries.designsystem.components.ClickableLinkText @@ -82,6 +82,7 @@ import io.element.android.libraries.matrix.ui.components.JoinButton import io.element.android.libraries.matrix.ui.components.SpaceHeaderView import io.element.android.libraries.matrix.ui.components.SpaceRoomItemView import io.element.android.libraries.matrix.ui.model.getAvatarData +import io.element.android.libraries.ui.strings.CommonPlurals import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay @@ -160,10 +161,17 @@ fun SpaceView( state.eventSink(SpaceEvents.ShowTopicViewer(topic)) } ) - JoinRoomFailureEffect( - hasAnyFailure = state.hasAnyFailure, + JoinFailuresEffect( + hasAnyFailure = state.hasAnyJoinFailures, eventSink = state.eventSink ) + RemoveRoomsActionView( + spaceDisplayName = state.currentSpaceDisplayName, + removeRoomsAction = state.removeRoomsAction, + selectedCount = state.selectedCount, + onConfirm = { state.eventSink(SpaceEvents.ConfirmRoomRemoval) }, + onDismiss = { state.eventSink(SpaceEvents.ClearRemoveAction) }, + ) acceptDeclineInviteView() } }, @@ -176,18 +184,10 @@ 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 -private fun JoinRoomFailureEffect( +private fun JoinFailuresEffect( hasAnyFailure: Boolean, eventSink: (SpaceEvents) -> Unit, ) { @@ -380,7 +380,7 @@ private fun SpaceViewTopBar( onDismissRequest = { showMenu = false } ) { SpaceMenuItem( - titleRes = CommonStrings.screen_space_menu_action_members, + titleRes = R.string.screen_space_menu_action_members, icon = CompoundIcons.User(), onClick = { showMenu = false @@ -448,7 +448,7 @@ private fun ManageModeTopBar( }, title = { Text( - text = "$selectedCount selected", + text = pluralStringResource(CommonPlurals.common_selected_count, selectedCount, selectedCount), style = ElementTheme.typography.fontBodyLgMedium, ) }, @@ -551,17 +551,19 @@ private fun SpaceRoom.inviteButtons( } @Composable -private fun RemoveRoomsConfirmationDialog( +private fun RemoveRoomsActionView( + spaceDisplayName: String, removeRoomsAction: AsyncAction, selectedCount: Int, onConfirm: () -> Unit, onDismiss: () -> Unit, ) { - when (removeRoomsAction) { - AsyncAction.ConfirmingNoParams -> { + AsyncActionView( + async = removeRoomsAction, + confirmationDialog = { 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.", + title = pluralStringResource(R.plurals.screen_space_remove_rooms_confirmation_title, selectedCount, selectedCount, spaceDisplayName), + content = stringResource(R.string.screen_space_remove_rooms_confirmation_content), submitText = stringResource(CommonStrings.action_remove), onSubmitClick = onConfirm, onDismiss = onDismiss, @@ -574,15 +576,17 @@ private fun RemoveRoomsConfirmationDialog( ) } ) - } - else -> { - AsyncActionView( - async = removeRoomsAction, - onSuccess = { onDismiss() }, - onErrorDismiss = onDismiss, - ) - } - } + }, + onRetry = onConfirm, + errorTitle = { + stringResource(CommonStrings.common_something_went_wrong) + }, + errorMessage = { + stringResource(CommonStrings.error_network_or_server_issue) + }, + onSuccess = { onDismiss() }, + onErrorDismiss = onDismiss, + ) } @PreviewsDayNight diff --git a/features/space/impl/src/main/res/values-bg/translations.xml b/features/space/impl/src/main/res/values-bg/translations.xml index 0759934bbd..de0870f3f4 100644 --- a/features/space/impl/src/main/res/values-bg/translations.xml +++ b/features/space/impl/src/main/res/values-bg/translations.xml @@ -1,5 +1,6 @@ + "Преглед на членовете" "Напускане на пространството" "Роли и разрешения" "Защита и поверителност" diff --git a/features/space/impl/src/main/res/values-cs/translations.xml b/features/space/impl/src/main/res/values-cs/translations.xml index d4730fc62d..d98124c714 100644 --- a/features/space/impl/src/main/res/values-cs/translations.xml +++ b/features/space/impl/src/main/res/values-cs/translations.xml @@ -11,6 +11,7 @@ "Z následujících místností nebudete odstraněni, protože jste jediným administrátorem:" "Opustit %1$s?" "Jste jediným administrátorem pro %1$s" + "Zobrazit členy" "Opustit prostor" "Role a oprávnění" "Zabezpečení a soukromí" diff --git a/features/space/impl/src/main/res/values-da/translations.xml b/features/space/impl/src/main/res/values-da/translations.xml index 6422b9635d..068712629d 100644 --- a/features/space/impl/src/main/res/values-da/translations.xml +++ b/features/space/impl/src/main/res/values-da/translations.xml @@ -10,6 +10,7 @@ "Du vil ikke blive fjernet fra følgende rum, fordi du er den eneste administrator:" "Forlad %1$s?" "Du er den eneste administrator for %1$s" + "Vis medlemmer" "Forlad gruppe" "Roller og tilladelser" "Sikkerhed og privatliv" diff --git a/features/space/impl/src/main/res/values-de/translations.xml b/features/space/impl/src/main/res/values-de/translations.xml index a001756c6c..1d0238cf7f 100644 --- a/features/space/impl/src/main/res/values-de/translations.xml +++ b/features/space/impl/src/main/res/values-de/translations.xml @@ -10,6 +10,12 @@ "Du wirst aus den folgenden Chats nicht entfernt, weil du der einzige Admin bist:" "%1$s verlassen?" "Du bist der einzige Administrator für %1$s" + "Mitglieder anzeigen" + "Das Entfernen eines Chats hat keinen Einfluss auf die Beitrittsregeln. Um die Regeln zu ändern, gehe zu \"Raum Info\" und dann zu \"Datenschutz und Sicherheit\"" + + "%1$d chat aus %2$s entfernen" + "%1$d chats aus %2$s entfernen" + "Space verlassen" "Rollen und Berechtigungen" "Sicherheit & Datenschutz" diff --git a/features/space/impl/src/main/res/values-et/translations.xml b/features/space/impl/src/main/res/values-et/translations.xml index 43eaade351..fdee05be05 100644 --- a/features/space/impl/src/main/res/values-et/translations.xml +++ b/features/space/impl/src/main/res/values-et/translations.xml @@ -10,6 +10,7 @@ "Sind ei saa järgnevatest jututubadest eemaldada, kuna oled seal/neis ainus peakasutaja:" "Kas lahkud %1$s kogukonnast?" "Sa oled siin ainus peakasutaja: %1$s" + "Vaata liikmeid" "Lahku kogukonnast" "Rollid ja õigused" "Turvalisus ja privaatsus" diff --git a/features/space/impl/src/main/res/values-fa/translations.xml b/features/space/impl/src/main/res/values-fa/translations.xml index bda53d0947..0f42a9f65f 100644 --- a/features/space/impl/src/main/res/values-fa/translations.xml +++ b/features/space/impl/src/main/res/values-fa/translations.xml @@ -5,6 +5,7 @@ "از اتاق(های) زیر برداشته نخواهید شد؛ چرا که تنها مدیر هستید:" "ترک %1$s؟" "تنها مدیر %1$s هستید" + "دیدن اعضا" "ترک فضا" "نقش‌ها و اجازه‌ها" "امنیت و محرمانگی" diff --git a/features/space/impl/src/main/res/values-fi/translations.xml b/features/space/impl/src/main/res/values-fi/translations.xml index e43a4ae7f9..77771cf383 100644 --- a/features/space/impl/src/main/res/values-fi/translations.xml +++ b/features/space/impl/src/main/res/values-fi/translations.xml @@ -10,6 +10,7 @@ "Sinua ei poisteta seuraavista huoneista, koska olet ainoa ylläpitäjä:" "Haluatko poistua tilasta %1$s?" "Olet ainoa ylläpitäjä tilassa %1$s" + "Näytä jäsenet" "Poistu tilasta" "Roolit ja oikeudet" "Turvallisuus ja yksityisyys" diff --git a/features/space/impl/src/main/res/values-fr/translations.xml b/features/space/impl/src/main/res/values-fr/translations.xml index befd4a7c92..89cc3e619f 100644 --- a/features/space/impl/src/main/res/values-fr/translations.xml +++ b/features/space/impl/src/main/res/values-fr/translations.xml @@ -10,6 +10,7 @@ "Vous ne quitterez pas le ou les salons suivants car vous y êtes le seul administrateur:" "Quitter %1$s?" "Vous êtes le seul administrateur de %1$s" + "Voir les membres" "Quitter l’espace" "Rôles & autorisations" "Sécurité & confidentialité" diff --git a/features/space/impl/src/main/res/values-hr/translations.xml b/features/space/impl/src/main/res/values-hr/translations.xml index 9babbb3d69..5bd5400124 100644 --- a/features/space/impl/src/main/res/values-hr/translations.xml +++ b/features/space/impl/src/main/res/values-hr/translations.xml @@ -11,6 +11,7 @@ "Nećete biti uklonjeni iz sljedećih soba jer ste jedini administrator:" "Želite li napustiti %1$s?" "Vi ste jedini administrator za %1$s" + "Prikaži članove" "Napusti prostor" "Uloge i dopuštenja" "Sigurnost i privatnost" diff --git a/features/space/impl/src/main/res/values-hu/translations.xml b/features/space/impl/src/main/res/values-hu/translations.xml index 3ddbe6c822..670e14cc3c 100644 --- a/features/space/impl/src/main/res/values-hu/translations.xml +++ b/features/space/impl/src/main/res/values-hu/translations.xml @@ -10,6 +10,7 @@ "Nem lesz eltávolítva a következő szobá(k)ból, mert ön az egyetlen adminisztrátor:" "Kilép innen: %1$s?" "Ön az egyetlen adminisztrátor itt: %1$s" + "Tagok megtekintése" "Tér elhagyása" "Szerepkörök és jogosultságok" "Biztonság és adatvédelem" diff --git a/features/space/impl/src/main/res/values-it/translations.xml b/features/space/impl/src/main/res/values-it/translations.xml index e483f98513..f358c96d0d 100644 --- a/features/space/impl/src/main/res/values-it/translations.xml +++ b/features/space/impl/src/main/res/values-it/translations.xml @@ -10,6 +10,7 @@ "Non verrai rimosso dalle seguenti stanze perché sei l\'unico amministratore:" "Uscire da %1$s?" "Sei l\'unico amministratore di %1$s" + "Visualizza membri" "Esci dallo spazio" "Ruoli e autorizzazioni" "Sicurezza e privacy" diff --git a/features/space/impl/src/main/res/values-nb/translations.xml b/features/space/impl/src/main/res/values-nb/translations.xml index 0e0709f80e..ebbe7be342 100644 --- a/features/space/impl/src/main/res/values-nb/translations.xml +++ b/features/space/impl/src/main/res/values-nb/translations.xml @@ -10,6 +10,7 @@ "Du vil ikke bli fjernet fra følgende rom fordi du er den eneste administratoren:" "Forlat %1$s?" "Du er den eneste administratoren for %1$s" + "Vis medlemmer" "Forlat område" "Roller og tillatelser" "Sikkerhet og personvern" diff --git a/features/space/impl/src/main/res/values-pt-rBR/translations.xml b/features/space/impl/src/main/res/values-pt-rBR/translations.xml index 3329be1097..c509b8caf8 100644 --- a/features/space/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/space/impl/src/main/res/values-pt-rBR/translations.xml @@ -10,6 +10,7 @@ "Você não será removido das seguintes salas porque você é o único administrador:" "Sair de %1$s?" "Você é o único administrador de %1$s" + "Ver membros" "Sair do espaço" "Cargos e permissões" "Segurança e privacidade" diff --git a/features/space/impl/src/main/res/values-ro/translations.xml b/features/space/impl/src/main/res/values-ro/translations.xml index 588518a249..7640d873a8 100644 --- a/features/space/impl/src/main/res/values-ro/translations.xml +++ b/features/space/impl/src/main/res/values-ro/translations.xml @@ -11,6 +11,7 @@ "Nu veți părăsi următoarele camere deoarece sunteți singurul administrator:" "Părăsiți %1$s?" "Sunteți singurul administrator pentru %1$s" + "Vizualizați membrii" "Părăsiți spațiul" "Roluri și permisiuni" "Securitate & confidențialitate" diff --git a/features/space/impl/src/main/res/values-ru/translations.xml b/features/space/impl/src/main/res/values-ru/translations.xml index 47cd467725..090c551fd5 100644 --- a/features/space/impl/src/main/res/values-ru/translations.xml +++ b/features/space/impl/src/main/res/values-ru/translations.xml @@ -11,6 +11,7 @@ "Вы не будете удалены из следующих комнат, поскольку вы являетесь единственным администратором:" "Выйти из %1$s?" "Вы единственный администратор для %1$s" + "Просмотреть участников" "Покинуть пространство" "Роли и разрешения" "Безопасность и конфиденциальность" diff --git a/features/space/impl/src/main/res/values-sk/translations.xml b/features/space/impl/src/main/res/values-sk/translations.xml index 2fd11ba58b..79b8fbfbb6 100644 --- a/features/space/impl/src/main/res/values-sk/translations.xml +++ b/features/space/impl/src/main/res/values-sk/translations.xml @@ -11,6 +11,7 @@ "Z nasledujúcich miestností nebudete odstránený/á, pretože ste jediným správcom:" "Opustiť %1$s?" "Ste jediným administrátorom pre %1$s" + "Zobraziť členov" "Opustiť priestor" "Roly a povolenia" "Bezpečnosť a súkromie" diff --git a/features/space/impl/src/main/res/values-zh-rTW/translations.xml b/features/space/impl/src/main/res/values-zh-rTW/translations.xml index abf495860f..54da45642c 100644 --- a/features/space/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/space/impl/src/main/res/values-zh-rTW/translations.xml @@ -9,6 +9,7 @@ "您不會被從以下聊天室移除,因為您是唯一的管理員:" "離開 %1$s?" "您是 %1$s 唯一的管理員" + "檢視成員" "離開空間" "角色與權限" "安全與隱私" diff --git a/features/space/impl/src/main/res/values-zh/translations.xml b/features/space/impl/src/main/res/values-zh/translations.xml index f0afff02f4..ea7011c942 100644 --- a/features/space/impl/src/main/res/values-zh/translations.xml +++ b/features/space/impl/src/main/res/values-zh/translations.xml @@ -9,6 +9,7 @@ "您不会从以下房间中被移除,因为您是唯一的管理员:" "离开%1$s?" "您是 %1$s 的唯一管理员" + "查看成员" "离开空间" "角色与权限" "安全与隐私" diff --git a/features/space/impl/src/main/res/values/localazy.xml b/features/space/impl/src/main/res/values/localazy.xml index a4df5e767d..10aa0fb28c 100644 --- a/features/space/impl/src/main/res/values/localazy.xml +++ b/features/space/impl/src/main/res/values/localazy.xml @@ -10,6 +10,12 @@ "You will not be removed from the following room(s) because you\'re the only administrator:" "Leave %1$s?" "You are the only admin for %1$s" + "View members" + "Removing a room will not affect the room access. To change the access go to Room info > Privacy & security." + + "Remove %1$d room from %2$s" + "Remove %1$d rooms from %2$s" + "Leave space" "Roles & permissions" "Security & privacy" diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceStateTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceStateTest.kt index a0c3635baf..ea9e5a2a30 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceStateTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceStateTest.kt @@ -22,7 +22,7 @@ class SpaceStateTest { @Test fun `test default state`() { val state = aSpaceState() - assertThat(state.hasAnyFailure).isFalse() + assertThat(state.hasAnyJoinFailures).isFalse() assertThat(state.isJoining(A_ROOM_ID)).isFalse() } @@ -35,7 +35,7 @@ class SpaceStateTest { A_ROOM_ID_3 to AsyncAction.Success(Unit), ) ) - assertThat(state.hasAnyFailure).isTrue() + assertThat(state.hasAnyJoinFailures).isTrue() } @Test diff --git a/libraries/ui-strings/src/main/res/values-bg/translations.xml b/libraries/ui-strings/src/main/res/values-bg/translations.xml index 483c47fbea..d6e314044b 100644 --- a/libraries/ui-strings/src/main/res/values-bg/translations.xml +++ b/libraries/ui-strings/src/main/res/values-bg/translations.xml @@ -333,7 +333,6 @@ "Споделяне на това местоположение" "%1$s пространство" "Пространства" - "Преглед на членовете" "Местоположение" "Версия: %1$s (%2$s)" "bg" diff --git a/libraries/ui-strings/src/main/res/values-cs/translations.xml b/libraries/ui-strings/src/main/res/values-cs/translations.xml index 45823e3a0d..fafb59d4fb 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -483,7 +483,6 @@ Opravdu chcete pokračovat?" "%1$s • %2$s" "%1$s prostor" "Prostory" - "Zobrazit členy" "Zpráva nebyla odeslána, protože ověřená identita uživatele %1$s se změnila." "Zpráva nebyla odeslána, protože%1$s neověřil(a) všechna zařízení." "Zpráva nebyla odeslána, protože jste neověřili jedno nebo více zařízení." diff --git a/libraries/ui-strings/src/main/res/values-da/translations.xml b/libraries/ui-strings/src/main/res/values-da/translations.xml index 6a230d4dfe..6dc36fac5a 100644 --- a/libraries/ui-strings/src/main/res/values-da/translations.xml +++ b/libraries/ui-strings/src/main/res/values-da/translations.xml @@ -469,7 +469,6 @@ Er du sikker på, at du vil fortsætte?" "%1$s•%2$s" "%1$s gruppe" "Grupper" - "Vis medlemmer" "Beskeden blev ikke sendt fordi %1$s s bekræftede identitet blev nulstillet." "Meddelelsen er ikke sendt, fordi %1$s ikke har bekræftet alle enheder." "Beskeden er ikke sendt, fordi du ikke har verificeret en eller flere af dine enheder." diff --git a/libraries/ui-strings/src/main/res/values-de/translations.xml b/libraries/ui-strings/src/main/res/values-de/translations.xml index 05650799ec..60a77143d6 100644 --- a/libraries/ui-strings/src/main/res/values-de/translations.xml +++ b/libraries/ui-strings/src/main/res/values-de/translations.xml @@ -483,12 +483,6 @@ Möchtest du wirklich fortfahren?" "Erstelle einen Space, um Chats zu organisieren" "%1$s Space" "Spaces" - "Mitglieder anzeigen" - "Das Entfernen eines Chats hat keinen Einfluss auf die Beitrittsregeln. Um die Regeln zu ändern, gehe zu \"Raum Info\" und dann zu \"Datenschutz und Sicherheit\"" - - "Chat aus %1$s entfernen" - "%1$d chats aus %2$s entfernen" - "Nachricht nicht gesendet, weil sich die verifizierte Identität von %1$s geändert hat." "Die Nachricht wurde nicht gesendet, weil %1$s nicht alle Geräte verifiziert hat." "Die Nachricht wurde nicht gesendet, weil du eines oder mehrere deiner Geräte nicht verifiziert hast." diff --git a/libraries/ui-strings/src/main/res/values-et/translations.xml b/libraries/ui-strings/src/main/res/values-et/translations.xml index 6e344cd6b2..9bd2ef325c 100644 --- a/libraries/ui-strings/src/main/res/values-et/translations.xml +++ b/libraries/ui-strings/src/main/res/values-et/translations.xml @@ -476,7 +476,6 @@ Kas sa oled kindel, et soovid jätkata?" "%1$s • %2$s" "Kogukond: %1$s" "Kogukonnad" - "Vaata liikmeid" "Sõnum on saatmata, kuna kasutaja %1$s verifitseeritud identiteet on lähtestatud." "Sõnum on saatmata, kuna %1$s pole verifitseerinud kõiki oma seadmeid." "Kuna sa pole üks või enamgi oma seadet verifitseerinud, siis sinu sõnum on saatmata." diff --git a/libraries/ui-strings/src/main/res/values-fa/translations.xml b/libraries/ui-strings/src/main/res/values-fa/translations.xml index 16872a2e91..083f84ddc6 100644 --- a/libraries/ui-strings/src/main/res/values-fa/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fa/translations.xml @@ -400,7 +400,6 @@ "%1$s • %2$s" "‏%1$s فضا" "فضاها" - "دیدن اعضا" "مکان" "نگارش : %1$s (%2$s)" "fa" diff --git a/libraries/ui-strings/src/main/res/values-fi/translations.xml b/libraries/ui-strings/src/main/res/values-fi/translations.xml index 4aa4797e3c..7d71dca9ef 100644 --- a/libraries/ui-strings/src/main/res/values-fi/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fi/translations.xml @@ -470,7 +470,6 @@ Haluatko varmasti jatkaa?" "%1$s • %2$s" "%1$s tila" "Tilat" - "Näytä jäsenet" "Viestiä ei lähetetty, koska käyttäjän %1$s vahvistettu identiteetti nollattiin." "Viestiä ei lähetetty, koska %1$s ei ole vahvistanut kaikkia laitteitaan." "Viestiä ei lähetetty, koska et ole vahvistanut yhtä tai useampaa laitettasi." diff --git a/libraries/ui-strings/src/main/res/values-fr/translations.xml b/libraries/ui-strings/src/main/res/values-fr/translations.xml index 46e0b3dfb1..be99a39c7d 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -483,7 +483,6 @@ Raison : %1$s." "Créer des espaces pour organiser les salons" "Espace %1$s" "Espaces" - "Voir les membres" "Le message n’a pas été envoyé car l’identité vérifiée de %1$s a été réinitialisée." "Le message n’a pas été envoyé car %1$s n’a pas vérifié tous ses appareils." "Message non envoyé car vous n’avez pas vérifié tous vos appareils." diff --git a/libraries/ui-strings/src/main/res/values-hr/translations.xml b/libraries/ui-strings/src/main/res/values-hr/translations.xml index 20aa4a2f66..2d9676c36a 100644 --- a/libraries/ui-strings/src/main/res/values-hr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-hr/translations.xml @@ -485,7 +485,6 @@ Jeste li sigurni da želite nastaviti?" "%1$s • %2$s" "Prostor %1$s" "Prostori" - "Prikaži članove" "Poruka nije poslana jer je poništen potvrđeni identitet korisnika %1$s." "Poruka nije poslana jer %1$s nije potvrdio sve uređaje." "Poruka nije poslana jer niste potvrdili jedan svoj uređaj ili više njih." diff --git a/libraries/ui-strings/src/main/res/values-hu/translations.xml b/libraries/ui-strings/src/main/res/values-hu/translations.xml index be1cc82207..259ef3dd15 100644 --- a/libraries/ui-strings/src/main/res/values-hu/translations.xml +++ b/libraries/ui-strings/src/main/res/values-hu/translations.xml @@ -469,7 +469,6 @@ Biztos, hogy folytatja?" "%1$s • %2$s" "%1$s tér" "Terek" - "Tagok megtekintése" "Az üzenet nem lett elküldve, mert %1$s ellenőrzött személyazonossága megváltozott." "Az üzenet nem lett elküldve, mert %1$s nem ellenőrizte az összes eszközét." "Az üzenet nem lett elküldve, mert egy vagy több eszközét nem ellenőrizte." diff --git a/libraries/ui-strings/src/main/res/values-it/translations.xml b/libraries/ui-strings/src/main/res/values-it/translations.xml index 01eb441661..c309732f11 100644 --- a/libraries/ui-strings/src/main/res/values-it/translations.xml +++ b/libraries/ui-strings/src/main/res/values-it/translations.xml @@ -470,7 +470,6 @@ Sei sicuro di voler continuare?" "%1$s • %2$s" "%1$s spazio" "Spazi" - "Visualizza membri" "Messaggio non inviato perché l\'identità verificata di %1$s è stata reimpostata." "Messaggio non inviato perché %1$s non ha verificato tutti i dispositivi." "Messaggio non inviato perché non hai verificato uno o più dispositivi." diff --git a/libraries/ui-strings/src/main/res/values-nb/translations.xml b/libraries/ui-strings/src/main/res/values-nb/translations.xml index 09d3eab84c..05673571c6 100644 --- a/libraries/ui-strings/src/main/res/values-nb/translations.xml +++ b/libraries/ui-strings/src/main/res/values-nb/translations.xml @@ -467,7 +467,6 @@ Er du sikker på at du vil fortsette?" "%1$s • %2$s" "%1$s område" "Områder" - "Vis medlemmer" "Meldingen ble ikke sendt fordi %1$ss verifiserte identitet er tilbakestilt." "Meldingen ble ikke sendt fordi %1$s ikke har verifisert alle enheter." "Meldingen ble ikke sendt fordi du ikke har verifisert en eller flere av enhetene dine." diff --git a/libraries/ui-strings/src/main/res/values-pt-rBR/translations.xml b/libraries/ui-strings/src/main/res/values-pt-rBR/translations.xml index 74acd35b3e..3c87408694 100644 --- a/libraries/ui-strings/src/main/res/values-pt-rBR/translations.xml +++ b/libraries/ui-strings/src/main/res/values-pt-rBR/translations.xml @@ -479,7 +479,6 @@ Você tem certeza de que deseja continuar?" "%1$s • %2$s" "Espaço %1$s" "Espaços" - "Ver membros" "Mensagem não enviada porque a identidade verificada de %1$s foi redefinida." "A mensagem não foi enviada porque %1$s não verificou todos os dispositivos." "Mensagem não enviada porque você não verificou um ou mais dos seus dispositivos." diff --git a/libraries/ui-strings/src/main/res/values-ro/translations.xml b/libraries/ui-strings/src/main/res/values-ro/translations.xml index e8727a98ae..0851be7648 100644 --- a/libraries/ui-strings/src/main/res/values-ro/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ro/translations.xml @@ -484,7 +484,6 @@ Sunteți sigur că doriți să continuați?" "%1$s • %2$s" "Spațiu %1$s" "Spații" - "Vizualizați membrii" "Mesajul nu a fost trimis deoarece identitatea verificată a lui %1$s s-a schimbat." "Mesajul nu a fost trimis deoarece %1$s nu a verificat toate dispozitivele." "Mesajul nu a fost trimis deoarece nu ați verificat unul sau mai multe dispozitive." diff --git a/libraries/ui-strings/src/main/res/values-ru/translations.xml b/libraries/ui-strings/src/main/res/values-ru/translations.xml index 77dde7078d..1078a92fd6 100644 --- a/libraries/ui-strings/src/main/res/values-ru/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml @@ -479,7 +479,6 @@ "%1$s • %2$s" "%1$s пространство" "Пространства" - "Просмотреть участников" "Сообщение не отправлено, потому что подтвержденная личность %1$s была сброшена." "Сообщение не отправлено, потому что %1$s не проверил одно или несколько устройств." "Сообщение не отправлено, поскольку вы не подтвердили одно или несколько своих устройств." diff --git a/libraries/ui-strings/src/main/res/values-sk/translations.xml b/libraries/ui-strings/src/main/res/values-sk/translations.xml index 747ac45132..e209bd0a21 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -481,7 +481,6 @@ Naozaj chcete pokračovať?" "%1$s • %2$s" "%1$s priestor" "Priestory" - "Zobraziť členov" "Správa nebola odoslaná, pretože sa zmenila overená totožnosť používateľa %1$s." "Správa nebola odoslaná, pretože %1$s neoveril/a všetky zariadenia." "Správa nebola odoslaná, pretože ste neoverili jedno alebo viac svojich zariadení." diff --git a/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml b/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml index 67815cfbc6..e70937357d 100644 --- a/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml +++ b/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml @@ -461,7 +461,6 @@ "%1$s • %2$s" "%1$s 空間" "空間" - "檢視成員" "因為 %1$s 的驗證身份已重設,因此未傳送訊息。" "訊息未傳送,因為 %1$s 尚未驗證所有裝置。" "因為您尚未驗證一個或多個裝置,因此未傳送訊息" diff --git a/libraries/ui-strings/src/main/res/values-zh/translations.xml b/libraries/ui-strings/src/main/res/values-zh/translations.xml index 774a3d3f89..7d63c0ccb9 100644 --- a/libraries/ui-strings/src/main/res/values-zh/translations.xml +++ b/libraries/ui-strings/src/main/res/values-zh/translations.xml @@ -460,7 +460,6 @@ "%1$s • %2$s" "%1$s空间" "空间" - "查看成员" "消息未发送,因为%1$s的已验证身份已被重置。" "消息未发送,因为%1$s尚未验证所有设备。" "消息未发送,因为您有尚未验证的设备。" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 9a86b9c203..70dd613526 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -317,6 +317,10 @@ Reason: %1$s." "Security" "Seen by" "Select an account" + + "%1$d selected" + "%1$d selected" + "Send to" "Sending…" "Sending failed" @@ -484,12 +488,6 @@ Are you sure you want to continue?" "Create spaces to organize rooms" "%1$s space" "Spaces" - "View members" - "Removing a room will not affect the room access. To change the access go to Room info > Privacy & security." - - "Remove room from %1$s" - "Remove %1$d rooms from %2$s" - "Message not sent because %1$s’s verified identity was reset." "Message not sent because %1$s has not verified all devices." "Message not sent because you have not verified one or more of your devices." diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 3894befcb0..e6ea1df3cb 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -233,7 +233,8 @@ "name" : ":features:space:impl", "includeRegex" : [ "screen\\.leave_space\\..*", - "screen\\.space_settings\\..*" + "screen\\.space_settings\\..*", + "screen\\.space\\..*" ] }, { From e5e4b18b80e22964ef3748c7ba7b552e1d62e4e0 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 15 Jan 2026 12:40:47 +0100 Subject: [PATCH 08/15] Rename canManageRooms to canEditSpaceGraph --- .../android/features/space/impl/root/SpacePresenter.kt | 2 +- .../element/android/features/space/impl/root/SpaceState.kt | 4 ++-- .../android/features/space/impl/root/SpaceStateProvider.kt | 2 +- .../space/impl/settings/SpaceSettingsPermissions.kt | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt index 43febab8c1..475920c0fc 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt @@ -228,7 +228,7 @@ class SpacePresenter( canAccessSpaceSettings = canAccessSpaceSettings, isManageMode = isManageMode, selectedRoomIds = selectedRoomIds.toImmutableSet(), - canManageRooms = permissions.canManageRooms, + canEditSpaceGraph = permissions.canEditSpaceGraph, removeRoomsAction = removeRoomsAction, eventSink = ::handleEvent, ) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt index 28b04a9a21..df384e68f2 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt @@ -30,7 +30,7 @@ data class SpaceState( val canAccessSpaceSettings: Boolean, val isManageMode: Boolean, val selectedRoomIds: ImmutableSet, - val canManageRooms: Boolean, + val canEditSpaceGraph: Boolean, val removeRoomsAction: AsyncAction, val eventSink: (SpaceEvents) -> Unit ) { @@ -42,7 +42,7 @@ data class SpaceState( val currentSpaceDisplayName = currentSpace?.displayName ?: currentSpaceId.value - val showManageRoomsAction: Boolean = canManageRooms && children.any { spaceRoom -> !spaceRoom.isSpace } + val showManageRoomsAction: Boolean = canEditSpaceGraph && children.any { spaceRoom -> !spaceRoom.isSpace } val selectedCount: Int = selectedRoomIds.size val isRemoveButtonEnabled: Boolean = selectedRoomIds.isNotEmpty() } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt index d70cf1f1b9..90095e9436 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt @@ -91,7 +91,7 @@ fun aSpaceState( canAccessSpaceSettings = canAccessSpaceSettings, isManageMode = isManageMode, selectedRoomIds = selectedRoomIds.toImmutableSet(), - canManageRooms = canManageRooms, + canEditSpaceGraph = canManageRooms, removeRoomsAction = removeRoomsAction, eventSink = eventSink, ) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsPermissions.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsPermissions.kt index 8297992b49..c02593f953 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsPermissions.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsPermissions.kt @@ -20,7 +20,7 @@ data class SpaceSettingsPermissions( val editDetailsPermissions: RoomDetailsEditPermissions, val canEditRolesAndPermissions: Boolean, val securityAndPrivacyPermissions: SecurityAndPrivacyPermissions, - val canManageRooms: Boolean, + val canEditSpaceGraph: Boolean, ) { fun hasAny(joinRule: JoinRule?): Boolean { return editDetailsPermissions.hasAny || @@ -33,7 +33,7 @@ data class SpaceSettingsPermissions( editDetailsPermissions = RoomDetailsEditPermissions.DEFAULT, canEditRolesAndPermissions = false, securityAndPrivacyPermissions = SecurityAndPrivacyPermissions.DEFAULT, - canManageRooms = false, + canEditSpaceGraph = false, ) } } @@ -43,6 +43,6 @@ fun RoomPermissions.spaceSettingsPermissions(): SpaceSettingsPermissions { editDetailsPermissions = roomDetailsEditPermissions(), canEditRolesAndPermissions = canEditRolesAndPermissions(), securityAndPrivacyPermissions = securityAndPrivacyPermissions(), - canManageRooms = canOwnUserSendState(StateEventType.SpaceChild), + canEditSpaceGraph = canOwnUserSendState(StateEventType.SpaceChild) || canOwnUserSendState(StateEventType.SpaceParent), ) } From 8dc7caa737ed8a0a2f6e58bc17973bd08a690a4f Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 15 Jan 2026 12:41:11 +0100 Subject: [PATCH 09/15] Move manage rooms menu item to top of space menu --- .../features/space/impl/root/SpaceView.kt | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt index cb19636dfd..5dcb57dfdb 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt @@ -136,8 +136,8 @@ fun SpaceView( showManageRoomsAction = state.showManageRoomsAction, onBackClick = onBackClick, onLeaveSpaceClick = onLeaveSpaceClick, - onShareSpace = onShareSpace, onSettingsClick = onSettingsClick, + onShareSpace = onShareSpace, onViewMembersClick = onViewMembersClick, onManageRoomsClick = { state.eventSink(SpaceEvents.EnterManageMode) }, ) @@ -379,6 +379,17 @@ private fun SpaceViewTopBar( expanded = showMenu, onDismissRequest = { showMenu = false } ) { + if (showManageRoomsAction) { + SpaceMenuItem( + titleRes = CommonStrings.action_manage_rooms, + icon = CompoundIcons.Edit(), + onClick = { + showMenu = false + onManageRoomsClick() + } + ) + HorizontalDivider() + } SpaceMenuItem( titleRes = R.string.screen_space_menu_action_members, icon = CompoundIcons.User(), @@ -395,16 +406,6 @@ private fun SpaceViewTopBar( onShareSpace() } ) - if (showManageRoomsAction) { - SpaceMenuItem( - titleRes = CommonStrings.action_manage_rooms, - icon = CompoundIcons.Edit(), - onClick = { - showMenu = false - onManageRoomsClick() - } - ) - } if (canAccessSpaceSettings) { SpaceMenuItem( titleRes = CommonStrings.common_settings, @@ -415,6 +416,7 @@ private fun SpaceViewTopBar( } ) } + HorizontalDivider() SpaceMenuItem( titleRes = CommonStrings.action_leave_space, icon = CompoundIcons.Leave(), From e7789ef8696a599ca01537bd6a0cc93d4e2d9bef Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 15 Jan 2026 16:41:33 +0100 Subject: [PATCH 10/15] Move canEditSpaceGraph out of SettingsPermissions to his own Permissions --- .../space/impl/root/SpacePermissions.kt | 33 +++++++++++++++++++ .../space/impl/root/SpacePresenter.kt | 6 ++-- .../impl/settings/SpaceSettingsPermissions.kt | 4 --- 3 files changed, 36 insertions(+), 7 deletions(-) create mode 100644 features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePermissions.kt diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePermissions.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePermissions.kt new file mode 100644 index 0000000000..90ee8e27f1 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePermissions.kt @@ -0,0 +1,33 @@ +/* + * 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.space.impl.root + +import io.element.android.features.space.impl.settings.SpaceSettingsPermissions +import io.element.android.features.space.impl.settings.spaceSettingsPermissions +import io.element.android.libraries.matrix.api.room.StateEventType +import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions + +data class SpacePermissions( + val settingsPermissions: SpaceSettingsPermissions, + val canEditSpaceGraph: Boolean, +) { + companion object { + val DEFAULT = SpacePermissions( + settingsPermissions = SpaceSettingsPermissions.DEFAULT, + canEditSpaceGraph = false, + ) + } +} + +fun RoomPermissions.spacePermissions(): SpacePermissions { + return SpacePermissions( + settingsPermissions = spaceSettingsPermissions(), + canEditSpaceGraph = canOwnUserSendState(StateEventType.SpaceChild) || canOwnUserSendState(StateEventType.SpaceParent), + ) +} + diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt index 475920c0fc..9ca68e866f 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt @@ -92,8 +92,8 @@ class SpacePresenter( } }.collectAsState() - val permissions by room.permissionsAsState(SpaceSettingsPermissions.DEFAULT) { perms -> - perms.spaceSettingsPermissions() + val permissions by room.permissionsAsState(SpacePermissions.DEFAULT) { perms -> + perms.spacePermissions() } val isSpaceSettingsEnabled by remember { featureFlagService.isFeatureEnabledFlow(FeatureFlags.SpaceSettings) @@ -101,7 +101,7 @@ class SpacePresenter( val roomInfo by room.roomInfoFlow.collectAsState() val canAccessSpaceSettings by remember { - derivedStateOf { isSpaceSettingsEnabled && permissions.hasAny(roomInfo.joinRule) } + derivedStateOf { isSpaceSettingsEnabled && permissions.settingsPermissions.hasAny(roomInfo.joinRule) } } val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState() val (joinActions, setJoinActions) = remember { mutableStateOf(emptyMap>()) } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsPermissions.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsPermissions.kt index c02593f953..e3ec70a51d 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsPermissions.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsPermissions.kt @@ -11,7 +11,6 @@ import io.element.android.features.roomdetailsedit.api.RoomDetailsEditPermission 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.libraries.matrix.api.room.StateEventType 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.canEditRolesAndPermissions @@ -20,7 +19,6 @@ data class SpaceSettingsPermissions( val editDetailsPermissions: RoomDetailsEditPermissions, val canEditRolesAndPermissions: Boolean, val securityAndPrivacyPermissions: SecurityAndPrivacyPermissions, - val canEditSpaceGraph: Boolean, ) { fun hasAny(joinRule: JoinRule?): Boolean { return editDetailsPermissions.hasAny || @@ -33,7 +31,6 @@ data class SpaceSettingsPermissions( editDetailsPermissions = RoomDetailsEditPermissions.DEFAULT, canEditRolesAndPermissions = false, securityAndPrivacyPermissions = SecurityAndPrivacyPermissions.DEFAULT, - canEditSpaceGraph = false, ) } } @@ -43,6 +40,5 @@ fun RoomPermissions.spaceSettingsPermissions(): SpaceSettingsPermissions { editDetailsPermissions = roomDetailsEditPermissions(), canEditRolesAndPermissions = canEditRolesAndPermissions(), securityAndPrivacyPermissions = securityAndPrivacyPermissions(), - canEditSpaceGraph = canOwnUserSendState(StateEventType.SpaceChild) || canOwnUserSendState(StateEventType.SpaceParent), ) } From 3757ac144caefbf98f632072edd94b3b0b011d16 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 15 Jan 2026 17:37:34 +0100 Subject: [PATCH 11/15] Replace SpaceState.currentSpace with spaceInfo (RoomInfo) --- .../space/impl/root/SpacePresenter.kt | 5 +- .../features/space/impl/root/SpaceState.kt | 6 +- .../space/impl/root/SpaceStateProvider.kt | 64 +++++++++++++----- .../features/space/impl/root/SpaceView.kt | 66 +++++++++---------- .../space/impl/root/SpacePresenterTest.kt | 20 +----- .../features/space/impl/root/SpaceViewTest.kt | 3 +- 6 files changed, 86 insertions(+), 78 deletions(-) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt index 9ca68e866f..60db576df8 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt @@ -53,7 +53,6 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import kotlin.jvm.optionals.getOrNull @Inject class SpacePresenter( @@ -103,7 +102,6 @@ class SpacePresenter( val canAccessSpaceSettings by remember { derivedStateOf { isSpaceSettingsEnabled && permissions.settingsPermissions.hasAny(roomInfo.joinRule) } } - val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState() val (joinActions, setJoinActions) = remember { mutableStateOf(emptyMap>()) } var topicViewerState: TopicViewerState by remember { mutableStateOf(TopicViewerState.Hidden) } @@ -216,8 +214,7 @@ class SpacePresenter( } } return SpaceState( - currentSpaceId = spaceRoomList.roomId, - currentSpace = currentSpace.getOrNull(), + spaceInfo = roomInfo, children = filteredChildren, seenSpaceInvites = seenSpaceInvites, hideInvitesAvatar = hideInvitesAvatar, diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt index df384e68f2..a669c294b5 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt @@ -12,14 +12,14 @@ import androidx.compose.runtime.Immutable import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState import io.element.android.libraries.architecture.AsyncAction 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.spaces.SpaceRoom import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.ImmutableSet data class SpaceState( - private val currentSpaceId: RoomId, - val currentSpace: SpaceRoom?, + val spaceInfo: RoomInfo, val children: ImmutableList, val seenSpaceInvites: ImmutableSet, val hideInvitesAvatar: Boolean, @@ -40,8 +40,6 @@ data class SpaceState( it is AsyncAction.Failure } - val currentSpaceDisplayName = currentSpace?.displayName ?: currentSpaceId.value - val showManageRoomsAction: Boolean = canEditSpaceGraph && children.any { spaceRoom -> !spaceRoom.isSpace } val selectedCount: Int = selectedRoomIds.size val isRemoveButtonEnabled: Boolean = selectedRoomIds.isNotEmpty() diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt index 90095e9436..9641e17625 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt @@ -15,6 +15,8 @@ import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInvit import io.element.android.libraries.architecture.AsyncAction 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.RoomInfo +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.spaces.SpaceRoom import io.element.android.libraries.previewutils.room.aSpaceRoom @@ -27,11 +29,11 @@ open class SpaceStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aSpaceState(), - aSpaceState(parentSpace = aParentSpace(joinRule = JoinRule.Public)), - aSpaceState(parentSpace = aParentSpace(joinRule = JoinRule.Restricted(persistentListOf()))), + aSpaceState(spaceInfo = aSpaceInfo(joinRule = JoinRule.Public)), + aSpaceState(spaceInfo = aSpaceInfo(joinRule = JoinRule.Restricted(persistentListOf()))), aSpaceState(children = aListOfSpaceRooms()), aSpaceState( - parentSpace = aParentSpace(), + spaceInfo = aSpaceInfo(), children = aListOfSpaceRooms(), joiningRooms = setOf(RoomId("!spaceId0:example.com")), hasMoreToLoad = false @@ -41,19 +43,19 @@ open class SpaceStateProvider : PreviewParameterProvider { ), // Manage mode states aSpaceState( - parentSpace = aParentSpace(), + spaceInfo = aSpaceInfo(), children = aListOfSpaceRooms(), isManageMode = true, selectedRoomIds = emptySet(), ), aSpaceState( - parentSpace = aParentSpace(), + spaceInfo = aSpaceInfo(), children = aListOfSpaceRooms(), isManageMode = true, selectedRoomIds = setOf(RoomId("!spaceId0:example.com"), RoomId("!spaceId1:example.com")), ), aSpaceState( - parentSpace = aParentSpace(), + spaceInfo = aSpaceInfo(), children = aListOfSpaceRooms(), isManageMode = true, selectedRoomIds = setOf(RoomId("!spaceId0:example.com")), @@ -63,7 +65,7 @@ open class SpaceStateProvider : PreviewParameterProvider { } fun aSpaceState( - parentSpace: SpaceRoom = aParentSpace(), + spaceInfo: RoomInfo = aSpaceInfo(), children: List = emptyList(), seenSpaceInvites: Set = emptySet(), joiningRooms: Set = emptySet(), @@ -79,8 +81,7 @@ fun aSpaceState( removeRoomsAction: AsyncAction = AsyncAction.Uninitialized, eventSink: (SpaceEvents) -> Unit = { }, ) = SpaceState( - currentSpaceId = parentSpace.roomId, - currentSpace = parentSpace, + spaceInfo = spaceInfo, children = children.toImmutableList(), seenSpaceInvites = seenSpaceInvites.toImmutableSet(), hideInvitesAvatar = hideInvitesAvatar, @@ -96,16 +97,45 @@ fun aSpaceState( eventSink = eventSink, ) -private fun aParentSpace( +private fun aSpaceInfo( joinRule: JoinRule? = null, -): SpaceRoom { - return aSpaceRoom( - numJoinedMembers = 5, - childrenCount = 10, - worldReadable = true, - joinRule = joinRule, - roomId = RoomId("!spaceId0:example.com"), +): RoomInfo { + return RoomInfo( + id = RoomId("!spaceId0:example.com"), + name = "A Space", + rawName = "A Space", topic = "Space description goes here. " + LoremIpsum(20).values.first(), + avatarUrl = null, + isPublic = true, + isDirect = false, + isEncrypted = false, + joinRule = joinRule, + isSpace = true, + isFavorite = false, + canonicalAlias = null, + alternativeAliases = persistentListOf(), + currentUserMembership = CurrentUserMembership.JOINED, + inviter = null, + activeMembersCount = 5, + invitedMembersCount = 0, + joinedMembersCount = 5, + roomPowerLevels = null, + highlightCount = 0, + notificationCount = 0, + userDefinedNotificationMode = null, + hasRoomCall = false, + activeRoomCallParticipants = persistentListOf(), + isMarkedUnread = false, + numUnreadMessages = 0, + numUnreadNotifications = 0, + numUnreadMentions = 0, + heroes = persistentListOf(), + pinnedEventIds = persistentListOf(), + creators = persistentListOf(), + historyVisibility = RoomHistoryVisibility.Joined, + successorRoom = null, + roomVersion = "11", + privilegedCreatorRole = false, ) } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt index 5dcb57dfdb..0887e69e7f 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt @@ -77,7 +77,9 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.api.room.CurrentUserMembership +import io.element.android.libraries.matrix.api.room.RoomInfo import io.element.android.libraries.matrix.api.spaces.SpaceRoom +import io.element.android.libraries.matrix.api.spaces.SpaceRoomVisibility import io.element.android.libraries.matrix.ui.components.JoinButton import io.element.android.libraries.matrix.ui.components.SpaceHeaderView import io.element.android.libraries.matrix.ui.components.SpaceRoomItemView @@ -131,7 +133,7 @@ fun SpaceView( exit = fadeOut() ) { SpaceViewTopBar( - currentSpace = state.currentSpace, + spaceInfo = state.spaceInfo, canAccessSpaceSettings = state.canAccessSpaceSettings, showManageRoomsAction = state.showManageRoomsAction, onBackClick = onBackClick, @@ -166,7 +168,7 @@ fun SpaceView( eventSink = state.eventSink ) RemoveRoomsActionView( - spaceDisplayName = state.currentSpaceDisplayName, + spaceDisplayName = state.spaceInfo.name ?: state.spaceInfo.id.value, removeRoomsAction = state.removeRoomsAction, selectedCount = state.selectedCount, onConfirm = { state.eventSink(SpaceEvents.ConfirmRoomRemoval) }, @@ -235,27 +237,25 @@ private fun SpaceViewContent( modifier: Modifier = Modifier, ) { LazyColumn(modifier.fillMaxSize()) { - val currentSpace = state.currentSpace - if (currentSpace != null) { - item(key = "space_header") { - AnimatedVisibility( - !state.isManageMode, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() - ) { - Column { - SpaceHeaderView( - avatarData = currentSpace.getAvatarData(AvatarSize.SpaceHeader), - name = currentSpace.displayName, - topic = currentSpace.topic, - topicMaxLines = 2, - visibility = currentSpace.visibility, - heroes = currentSpace.heroes.toImmutableList(), - numberOfMembers = currentSpace.numJoinedMembers, - onTopicClick = onTopicClick - ) - HorizontalDivider() - } + val spaceInfo = state.spaceInfo + item(key = "space_header") { + AnimatedVisibility( + !state.isManageMode, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + Column { + SpaceHeaderView( + avatarData = spaceInfo.getAvatarData(AvatarSize.SpaceHeader), + name = spaceInfo.name, + topic = spaceInfo.topic, + topicMaxLines = 2, + visibility = SpaceRoomVisibility.fromJoinRule(spaceInfo.joinRule), + heroes = spaceInfo.heroes, + numberOfMembers = spaceInfo.joinedMembersCount.toInt(), + onTopicClick = onTopicClick + ) + HorizontalDivider() } } } @@ -337,7 +337,7 @@ private fun LoadingMoreIndicator( @OptIn(ExperimentalMaterial3Api::class) @Composable private fun SpaceViewTopBar( - currentSpace: SpaceRoom?, + spaceInfo: RoomInfo, canAccessSpaceSettings: Boolean, showManageRoomsAction: Boolean, onBackClick: () -> Unit, @@ -354,16 +354,14 @@ private fun SpaceViewTopBar( BackButton(onClick = onBackClick) }, title = { - if (currentSpace != null) { - val roundedCornerShape = RoundedCornerShape(8.dp) - SpaceAvatarAndNameRow( - name = currentSpace.displayName, - avatarData = currentSpace.getAvatarData(AvatarSize.TimelineRoom), - modifier = Modifier - .clip(roundedCornerShape) - .clickable(enabled = canAccessSpaceSettings, onClick = onSettingsClick) - ) - } + val roundedCornerShape = RoundedCornerShape(8.dp) + SpaceAvatarAndNameRow( + name = spaceInfo.name, + avatarData = spaceInfo.getAvatarData(AvatarSize.TimelineRoom), + modifier = Modifier + .clip(roundedCornerShape) + .clickable(enabled = canAccessSpaceSettings, onClick = onSettingsClick) + ) }, actions = { var showMenu by remember { mutableStateOf(false) } diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt index 08fb977d1b..f5d9f96e27 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt @@ -36,6 +36,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID_2 import io.element.android.libraries.matrix.test.A_ROOM_ID_3 import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.libraries.matrix.test.room.aRoomInfo 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.spaces.FakeSpaceRoomList @@ -63,7 +64,7 @@ class SpacePresenterTest { val presenter = createSpacePresenter(spaceRoomList = spaceRoomList) presenter.test { val state = awaitItem() - assertThat(state.currentSpace).isNull() + assertThat(state.spaceInfo).isNotNull() assertThat(state.children).isEmpty() assertThat(state.seenSpaceInvites).isEmpty() assertThat(state.hideInvitesAvatar).isFalse() @@ -143,23 +144,6 @@ class SpacePresenterTest { } } - @Test - fun `present - current space value`() = runTest { - val paginateResult = lambdaRecorder> { - Result.success(Unit) - } - val spaceRoomList = FakeSpaceRoomList(paginateResult = paginateResult) - val presenter = createSpacePresenter(spaceRoomList = spaceRoomList) - presenter.test { - val state = awaitItem() - advanceUntilIdle() - assertThat(state.currentSpace).isNull() - val aSpace = aSpaceRoom() - spaceRoomList.emitCurrentSpace(aSpace) - assertThat(awaitItem().currentSpace).isEqualTo(aSpace) - } - } - @Test fun `present - children value`() = runTest { val paginateResult = lambdaRecorder> { diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt index 3ef5151a50..57e14b05e5 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt @@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.api.spaces.SpaceRoom 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_TOPIC +import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.previewutils.room.aSpaceRoom import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled @@ -127,7 +128,7 @@ class SpaceViewTest { val eventsRecorder = EventsRecorder() rule.setSpaceView( aSpaceState( - parentSpace = aSpaceRoom(topic = A_ROOM_TOPIC), + spaceInfo = aRoomInfo(topic = A_ROOM_TOPIC), hasMoreToLoad = false, eventSink = eventsRecorder, ) From b4f15e595d0c2360295d5038efe24597e7bb7199 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 15 Jan 2026 17:43:44 +0100 Subject: [PATCH 12/15] Change canEditSpaceGraph to observe space settings feature flag --- .../android/features/space/impl/root/SpacePresenter.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt index 60db576df8..753dcae72a 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt @@ -102,6 +102,9 @@ class SpacePresenter( val canAccessSpaceSettings by remember { derivedStateOf { isSpaceSettingsEnabled && permissions.settingsPermissions.hasAny(roomInfo.joinRule) } } + val canEditSpaceGraph by remember { + derivedStateOf { isSpaceSettingsEnabled && permissions.canEditSpaceGraph } + } val (joinActions, setJoinActions) = remember { mutableStateOf(emptyMap>()) } var topicViewerState: TopicViewerState by remember { mutableStateOf(TopicViewerState.Hidden) } @@ -225,7 +228,7 @@ class SpacePresenter( canAccessSpaceSettings = canAccessSpaceSettings, isManageMode = isManageMode, selectedRoomIds = selectedRoomIds.toImmutableSet(), - canEditSpaceGraph = permissions.canEditSpaceGraph, + canEditSpaceGraph = canEditSpaceGraph, removeRoomsAction = removeRoomsAction, eventSink = ::handleEvent, ) From 9d5b2c57bcbf66b75a2ffe70be371f42491cc9b3 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 15 Jan 2026 18:18:27 +0100 Subject: [PATCH 13/15] Remove unused imports and fix code style issues --- .../android/features/space/impl/root/SpacePermissions.kt | 1 - .../android/features/space/impl/root/SpacePresenter.kt | 2 -- .../element/android/features/space/impl/root/SpaceView.kt | 2 -- .../features/space/impl/root/SpacePresenterTest.kt | 8 +++++--- .../android/features/space/impl/root/SpaceStateTest.kt | 2 +- .../android/features/space/impl/root/SpaceViewTest.kt | 1 - 6 files changed, 6 insertions(+), 10 deletions(-) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePermissions.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePermissions.kt index 90ee8e27f1..4a43aceb86 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePermissions.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePermissions.kt @@ -30,4 +30,3 @@ fun RoomPermissions.spacePermissions(): SpacePermissions { canEditSpaceGraph = canOwnUserSendState(StateEventType.SpaceChild) || canOwnUserSendState(StateEventType.SpaceParent), ) } - diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt index 753dcae72a..a5bc2ec52e 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt @@ -23,8 +23,6 @@ import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState import io.element.android.features.invite.api.toInviteData -import io.element.android.features.space.impl.settings.SpaceSettingsPermissions -import io.element.android.features.space.impl.settings.spaceSettingsPermissions import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.mapState diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt index 0887e69e7f..695b887496 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt @@ -86,7 +86,6 @@ import io.element.android.libraries.matrix.ui.components.SpaceRoomItemView import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.ui.strings.CommonPlurals import io.element.android.libraries.ui.strings.CommonStrings -import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay @OptIn(ExperimentalMaterial3Api::class) @@ -102,7 +101,6 @@ fun SpaceView( modifier: Modifier = Modifier, acceptDeclineInviteView: @Composable () -> Unit, ) { - BackHandler { if (state.isManageMode) { state.eventSink(SpaceEvents.ExitManageMode) diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt index f5d9f96e27..811e9158b9 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt @@ -36,7 +36,6 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID_2 import io.element.android.libraries.matrix.test.A_ROOM_ID_3 import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeBaseRoom -import io.element.android.libraries.matrix.test.room.aRoomInfo 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.spaces.FakeSpaceRoomList @@ -433,8 +432,11 @@ class SpacePresenterTest { roomType = RoomType.Room, ) val removeChildFromSpaceResult = lambdaRecorder> { _, childId -> - if (childId == A_ROOM_ID_2) Result.failure(AN_EXCEPTION) - else Result.success(Unit) + if (childId == A_ROOM_ID_2) { + Result.failure(AN_EXCEPTION) + } else { + Result.success(Unit) + } } val fakeSpaceRoomList = FakeSpaceRoomList( initialSpaceRoomsValue = listOf(aRoom1, aRoom2), diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceStateTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceStateTest.kt index ea9e5a2a30..65bb740541 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceStateTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceStateTest.kt @@ -10,11 +10,11 @@ package io.element.android.features.space.impl.root import com.google.common.truth.Truth.assertThat import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.room.RoomType 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_2 import io.element.android.libraries.matrix.test.A_ROOM_ID_3 -import io.element.android.libraries.matrix.api.room.RoomType import io.element.android.libraries.previewutils.room.aSpaceRoom import org.junit.Test diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt index 57e14b05e5..27970e93f8 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt @@ -12,7 +12,6 @@ import androidx.activity.ComponentActivity import androidx.compose.runtime.Composable import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 From d7c771f431f34790bba129bc0109cbfaaa58d815 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 15 Jan 2026 17:56:01 +0000 Subject: [PATCH 14/15] Update screenshots --- .../images/features.space.impl.root_SpaceView_Day_0_en.png | 4 ++-- .../images/features.space.impl.root_SpaceView_Day_1_en.png | 4 ++-- .../images/features.space.impl.root_SpaceView_Day_2_en.png | 4 ++-- .../images/features.space.impl.root_SpaceView_Day_3_en.png | 4 ++-- .../images/features.space.impl.root_SpaceView_Day_4_en.png | 4 ++-- .../images/features.space.impl.root_SpaceView_Day_5_en.png | 4 ++-- .../images/features.space.impl.root_SpaceView_Day_6_en.png | 3 +++ .../images/features.space.impl.root_SpaceView_Day_7_en.png | 3 +++ .../images/features.space.impl.root_SpaceView_Day_8_en.png | 3 +++ .../images/features.space.impl.root_SpaceView_Night_0_en.png | 4 ++-- .../images/features.space.impl.root_SpaceView_Night_1_en.png | 4 ++-- .../images/features.space.impl.root_SpaceView_Night_2_en.png | 4 ++-- .../images/features.space.impl.root_SpaceView_Night_3_en.png | 4 ++-- .../images/features.space.impl.root_SpaceView_Night_4_en.png | 4 ++-- .../images/features.space.impl.root_SpaceView_Night_5_en.png | 4 ++-- .../images/features.space.impl.root_SpaceView_Night_6_en.png | 3 +++ .../images/features.space.impl.root_SpaceView_Night_7_en.png | 3 +++ .../images/features.space.impl.root_SpaceView_Night_8_en.png | 3 +++ 18 files changed, 42 insertions(+), 24 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_6_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_7_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_8_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_6_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_7_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_8_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_0_en.png index 0f62df3d20..f69878ffd5 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c55277089a4447b618a0e8c058718ecf9d3da6d437322f0e23e5fd70019f6b00 -size 34585 +oid sha256:e59a9e2ae6ef36f28e61534b5639314cc840953df51bb1660e77e8d565865357 +size 32998 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_1_en.png index bb1c9d1947..7f66fa5a85 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:241f5500cb7212fac174466bbe7855ccf39de3e3764a83202388b947d90ae807 -size 34770 +oid sha256:b854ce2b0618ebcbc88eff9952d6869bceeb8b43f9eccb8f8e5feef225e0a4c2 +size 33181 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_2_en.png index 6f624546ab..b0f6f289a0 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:92785cf3a4010779b0fbcd58be3437a22808b0a2f02a19a5cfd50eb3bd58ed26 -size 35058 +oid sha256:e1cf063ee5c9fbc50a53445050780ba239d4fb0fd1e9903a578eab8dd3bfc257 +size 33496 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_3_en.png index 8b5b2f5f27..330b4d2dca 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de26882f13bac98b2cb5365d98e06e781d516d179adb8328cf22cf524e6fd79e -size 62568 +oid sha256:669287557e8effe8154682d33de45430fc852ddefa25ed7399aa3917730e2893 +size 61083 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_4_en.png index cfa8381d77..53464e9cd5 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a121fdb9473512b0264e48294df1799a7a6bf9b469df973fbe41f31bbf98f1d0 -size 63248 +oid sha256:d2ef07a1af8f872a5b7bb708314bc180d4eefe9545c763a0860f895af6e3ce37 +size 61755 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_5_en.png index f20b7c4048..97dbb10528 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3506b4f646408262450ae51b612f86c1171ed972c1d7ea8871c4dc090556c7a -size 59702 +oid sha256:525059397001897f705630b8ac5a661439a502f2e623fdca252f9e86f97133e4 +size 58181 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_6_en.png new file mode 100644 index 0000000000..bc44bb4ce4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59af097a2152b235c7df83aa76eea880ceef65edebebecb86f00745ec39712f4 +size 35937 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_7_en.png new file mode 100644 index 0000000000..68f915afff --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5151bbd3effcd6a46724a0cfb13730e845a3b8ba489826146c76d0a797403e0 +size 36502 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_8_en.png new file mode 100644 index 0000000000..64ce2a5fe0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b445ae9e0ff01dea22b8ffb98aaaf4c5593093e2b2bea2ff9508cc43832e687 +size 48283 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_0_en.png index e8f00148a3..9d406ab88a 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f2407444889af236ef21a90c47a5d3e05df8b15b9cc9483e84377e3af8794772 -size 33996 +oid sha256:47c8bdf4d153ecebe749ae3cacfe4c9a1c59ac836106fb0903dca80305aedac2 +size 32412 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_1_en.png index 6f012ad603..d178c45fec 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fee41efefc2ca1d6670d8455ac756c6b314aab54510eab8a4e597f1cc1edf3f8 -size 34141 +oid sha256:33917297cb1c6d38e5b955757de50f4ff6b73844bb87e8e88d0885daa01b266b +size 32556 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_2_en.png index 24f916eac9..35ca1a2937 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea955839cbd1aeba5de2780cee413628c7d46383398b10125cd3a900fb41d5a5 -size 34459 +oid sha256:930f763051533ea3aa4032e483e15e0c1ccb1d213d2e59bebe7f934517b500ba +size 32868 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_3_en.png index 153b68c3d0..86af1703e4 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cba2c99744aeb2a869ae2ed700d7241b1d0b6ed979b16d2be9774ddbc5f8f28a -size 61381 +oid sha256:e38c4a6ff464f77da95b2aa0eaba647bc54ee590c2c4319478fcd54a2400b886 +size 59877 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_4_en.png index f36d90e33c..b2628c4761 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b32d65accabc357208efeb2ec61374182479541299ade28184f82938e59bfdd5 -size 61932 +oid sha256:8d91c27ee6dc7938ce905774868b66157b06944ae7c230935c30d6be8ce189be +size 60430 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_5_en.png index 66a7762467..5f2f43a920 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c161ff55e8a235fe403e53ee179b299fd2563d85ef64bfe6d0dd9295228685b -size 57925 +oid sha256:dd673c2cf628285836848a732d3972a17421a2394464a3a77e7a49e4c5a862f1 +size 56636 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_6_en.png new file mode 100644 index 0000000000..3078485999 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b459fa8aa195dcf85995ac51c2f079e9d2505efede138bcef94c0a5049e1f271 +size 35266 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_7_en.png new file mode 100644 index 0000000000..7741306594 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36fd123fcff07169723857c99e4e375fe9bbb8193b2a6e97508343ea025d5787 +size 35782 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_8_en.png new file mode 100644 index 0000000000..5db2323ea6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5eed7b1690ac5fbd24312e37029ca880b5cf4424e3796b116829074ef891f2d +size 45433 From f999898b92e47e1999673a9591e0eb4b5026d9a1 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 16 Jan 2026 11:18:16 +0100 Subject: [PATCH 15/15] Add doc to SpacePermissions data class --- .../android/features/space/impl/root/SpacePermissions.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePermissions.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePermissions.kt index 4a43aceb86..fc9f5c9b0c 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePermissions.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePermissions.kt @@ -12,6 +12,11 @@ import io.element.android.features.space.impl.settings.spaceSettingsPermissions import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions +/** + * Permissions needed for different actions in the Space screen. + * @param settingsPermissions Permissions related to space settings. + * @param canEditSpaceGraph Whether the user can edit the space graph (add/remove children). + */ data class SpacePermissions( val settingsPermissions: SpaceSettingsPermissions, val canEditSpaceGraph: Boolean,