From 74c2ee46d89709d591b11ac9f0b7ffd46ead3077 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 12 Sep 2025 12:43:51 +0200 Subject: [PATCH 01/12] Leave space - Add screen to leave a space. --- .../android/appnav/LoggedInEventProcessor.kt | 14 +- .../impl/src/main/res/values/localazy.xml | 2 + .../space/impl/DefaultSpaceEntryPoint.kt | 2 +- .../features/space/impl/SpaceFlowNode.kt | 81 +++++ .../space/impl/leave/LeaveSpaceEvents.kt | 18 + .../{SpaceNode.kt => leave/LeaveSpaceNode.kt} | 14 +- .../space/impl/leave/LeaveSpacePresenter.kt | 135 ++++++++ .../space/impl/leave/LeaveSpaceState.kt | 35 ++ .../impl/leave/LeaveSpaceStateProvider.kt | 113 +++++++ .../space/impl/leave/LeaveSpaceView.kt | 312 ++++++++++++++++++ .../space/impl/leave/SelectableSpaceRoom.kt | 16 + .../space/impl/{ => root}/SpaceEvents.kt | 2 +- .../features/space/impl/root/SpaceNode.kt | 85 +++++ .../space/impl/{ => root}/SpacePresenter.kt | 2 +- .../space/impl/{ => root}/SpaceState.kt | 2 +- .../impl/{ => root}/SpaceStateProvider.kt | 4 +- .../space/impl/{ => root}/SpaceView.kt | 70 +++- .../impl/src/main/res/values/localazy.xml | 13 + .../space/impl/DefaultSpaceEntryPointTest.kt | 28 +- .../impl/leave/LeaveSpacePresenterTest.kt | 213 ++++++++++++ .../impl/{ => root}/SpacePresenterTest.kt | 2 +- .../components/avatar/AvatarSize.kt | 1 + .../matrix/api/room/RoomMembershipObserver.kt | 16 +- .../matrix/impl/room/RustBaseRoom.kt | 6 +- .../matrix/impl/room/RustBaseRoomTest.kt | 3 + .../android/libraries/matrix/test/TestData.kt | 2 + .../src/main/res/values/localazy.xml | 6 +- tools/localazy/config.json | 6 + 28 files changed, 1153 insertions(+), 50 deletions(-) create mode 100644 features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt create mode 100644 features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceEvents.kt rename features/space/impl/src/main/kotlin/io/element/android/features/space/impl/{SpaceNode.kt => leave/LeaveSpaceNode.kt} (75%) create mode 100644 features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenter.kt create mode 100644 features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceState.kt create mode 100644 features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateProvider.kt create mode 100644 features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceView.kt create mode 100644 features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/SelectableSpaceRoom.kt rename features/space/impl/src/main/kotlin/io/element/android/features/space/impl/{ => root}/SpaceEvents.kt (83%) create mode 100644 features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceNode.kt rename features/space/impl/src/main/kotlin/io/element/android/features/space/impl/{ => root}/SpacePresenter.kt (98%) rename features/space/impl/src/main/kotlin/io/element/android/features/space/impl/{ => root}/SpaceState.kt (92%) rename features/space/impl/src/main/kotlin/io/element/android/features/space/impl/{ => root}/SpaceStateProvider.kt (97%) rename features/space/impl/src/main/kotlin/io/element/android/features/space/impl/{ => root}/SpaceView.kt (71%) create mode 100644 features/space/impl/src/main/res/values/localazy.xml create mode 100644 features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenterTest.kt rename features/space/impl/src/test/kotlin/io/element/android/features/space/impl/{ => root}/SpacePresenterTest.kt (99%) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt index 5444b5c465..c557d6e1c2 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt @@ -31,9 +31,17 @@ class LoggedInEventProcessor( observingJob = roomMembershipObserver.updates .filter { !it.isUserInRoom } .distinctUntilChanged() - .onEach { - when (it.change) { - MembershipChange.LEFT -> displayMessage(CommonStrings.common_current_user_left_room) + .onEach { roomMemberShipUpdate -> + when (roomMemberShipUpdate.change) { + MembershipChange.LEFT -> { + displayMessage( + if (roomMemberShipUpdate.isSpace) { + CommonStrings.common_current_user_left_space + } else { + CommonStrings.common_current_user_left_room + } + ) + } MembershipChange.INVITATION_REJECTED -> displayMessage(CommonStrings.common_current_user_rejected_invite) MembershipChange.KNOCK_RETRACTED -> displayMessage(CommonStrings.common_current_user_canceled_knock) else -> Unit diff --git a/features/rageshake/impl/src/main/res/values/localazy.xml b/features/rageshake/impl/src/main/res/values/localazy.xml index 9c18d37a3b..f6d93c4114 100644 --- a/features/rageshake/impl/src/main/res/values/localazy.xml +++ b/features/rageshake/impl/src/main/res/values/localazy.xml @@ -14,5 +14,7 @@ "Send screenshot" "Logs will be included with your message to make sure that everything is working properly. To send your message without logs, turn off this setting." "%1$s crashed the last time it was used. Would you like to share a crash report with us?" + "If you are having issues with notifications, uploading the notification settings can help us pinpoint the root cause." + "Send notification settings" "View logs" diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPoint.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPoint.kt index 1ef8275b27..8591978417 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPoint.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPoint.kt @@ -33,7 +33,7 @@ class DefaultSpaceEntryPoint : SpaceEntryPoint { } override fun build(): Node { - return parentNode.createNode(buildContext, plugins = plugins.toList()) + return parentNode.createNode(buildContext, plugins = plugins.toList()) } } } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt new file mode 100644 index 0000000000..3fec810cb2 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +@file:OptIn(ExperimentalMaterial3Api::class) + +package io.element.android.features.space.impl + +import android.os.Parcelable +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.push +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.Inject +import io.element.android.annotations.ContributesNode +import io.element.android.features.space.api.SpaceEntryPoint +import io.element.android.features.space.impl.leave.LeaveSpaceNode +import io.element.android.features.space.impl.root.SpaceNode +import io.element.android.libraries.architecture.BackstackView +import io.element.android.libraries.architecture.BaseFlowNode +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.core.RoomId +import kotlinx.parcelize.Parcelize + +@ContributesNode(SessionScope::class) +@Inject +class SpaceFlowNode( + @Assisted val buildContext: BuildContext, + @Assisted plugins: List, +) : BaseFlowNode( + backstack = BackStack( + initialElement = NavTarget.Root, + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins, +) { + private val inputs: SpaceEntryPoint.Inputs = inputs() + private val callback = plugins.filterIsInstance().single() + + sealed interface NavTarget : Parcelable { + @Parcelize + data object Root : NavTarget + + @Parcelize + data object Leave : NavTarget + } + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return when (navTarget) { + NavTarget.Leave -> { + createNode(buildContext, listOf(inputs)) + } + NavTarget.Root -> { + val callback = object : SpaceNode.Callback { + override fun onOpenRoom(roomId: RoomId, viaParameters: List) { + callback.onOpenRoom(roomId, viaParameters) + } + + override fun onLeaveSpace() { + backstack.push(NavTarget.Leave) + } + } + createNode(buildContext, listOf(inputs, callback)) + } + } + } + + @Composable + override fun View(modifier: Modifier) = BackstackView() +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceEvents.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceEvents.kt new file mode 100644 index 0000000000..3c963a0bf5 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceEvents.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.space.impl.leave + +import io.element.android.libraries.matrix.api.core.RoomId + +sealed interface LeaveSpaceEvents { + data object SelectAllRooms : LeaveSpaceEvents + data object DeselectAllRooms : LeaveSpaceEvents + data class ToggleRoomSelection(val roomId: RoomId) : LeaveSpaceEvents + data object LeaveSpace : LeaveSpaceEvents + data object CloseError : LeaveSpaceEvents +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceNode.kt similarity index 75% rename from features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceNode.kt rename to features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceNode.kt index db49b7eb49..0973092994 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceNode.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceNode.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.space.impl +package io.element.android.features.space.impl.leave import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -21,24 +21,20 @@ import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) @AssistedInject -class SpaceNode( +class LeaveSpaceNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, - presenterFactory: SpacePresenter.Factory, + presenterFactory: LeaveSpacePresenter.Factory, ) : Node(buildContext, plugins = plugins) { private val inputs: SpaceEntryPoint.Inputs = inputs() - private val callback = plugins.filterIsInstance().single() private val presenter = presenterFactory.create(inputs) @Composable override fun View(modifier: Modifier) { val state = presenter.present() - SpaceView( + LeaveSpaceView( state = state, - onBackClick = ::navigateUp, - onRoomClick = { spaceRoom -> - callback.onOpenRoom(spaceRoom.roomId, spaceRoom.via) - }, + onCancel = ::navigateUp, modifier = modifier ) } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenter.kt new file mode 100644 index 0000000000..21e87e95e3 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenter.kt @@ -0,0 +1,135 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.space.impl.leave + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.Inject +import io.element.android.features.space.api.SpaceEntryPoint +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.runUpdatingState +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.previewutils.room.aSpaceRoom +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlin.jvm.optionals.getOrNull + +@Inject +class LeaveSpacePresenter( + @Assisted private val inputs: SpaceEntryPoint.Inputs, + private val matrixClient: MatrixClient, +) : Presenter { + @AssistedFactory + fun interface Factory { + fun create(inputs: SpaceEntryPoint.Inputs): LeaveSpacePresenter + } + + private val spaceRoomList = matrixClient.spaceService.spaceRoomList(inputs.roomId) + + @Composable + override fun present(): LeaveSpaceState { + val coroutineScope = rememberCoroutineScope() + val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState() + val leaveSpaceAction = remember { + mutableStateOf>(AsyncAction.Uninitialized) + } + val selectedRoomIds = remember { + mutableStateOf>(emptySet()) + } + val joinedSpaceRooms by produceState(emptyList()) { + // TODO Get the joined room from the SDK, should also have the + val rooms = listOf( + aSpaceRoom( + roomId = RoomId("!roomId1:example.com"), + ), + aSpaceRoom( + roomId = RoomId("!roomId2:example.com"), + ), + ) + value = rooms + } + val selectableSpaceRooms by produceState>>( + initialValue = AsyncData.Uninitialized, + key1 = joinedSpaceRooms, + key2 = selectedRoomIds.value, + ) { + value = AsyncData.Success( + joinedSpaceRooms.map { + SelectableSpaceRoom( + it, + // TODO Get this value from the SDK + isLastAdmin = false, + selectedRoomIds.value.contains(it.roomId), + ) + }.toPersistentList() + ) + } + + fun handleEvents(event: LeaveSpaceEvents) { + when (event) { + LeaveSpaceEvents.DeselectAllRooms -> selectedRoomIds.value = emptySet() + LeaveSpaceEvents.SelectAllRooms -> { + selectedRoomIds.value = selectableSpaceRooms.dataOrNull() + .orEmpty() + .filter { it.isLastAdmin.not() } + .map { it.spaceRoom.roomId } + .toSet() + } + is LeaveSpaceEvents.ToggleRoomSelection -> { + val currentSet = selectedRoomIds.value + selectedRoomIds.value = if (currentSet.contains(event.roomId)) { + currentSet - event.roomId + } else { + currentSet + event.roomId + } + } + LeaveSpaceEvents.LeaveSpace -> coroutineScope.leaveSpace( + leaveSpaceAction = leaveSpaceAction, + selectedRoomIds = selectedRoomIds.value, + ) + LeaveSpaceEvents.CloseError -> { + leaveSpaceAction.value = AsyncAction.Uninitialized + } + } + } + + return LeaveSpaceState( + spaceName = currentSpace.getOrNull()?.name, + selectableSpaceRooms = selectableSpaceRooms, + leaveSpaceAction = leaveSpaceAction.value, + eventSink = ::handleEvents, + ) + } + + private fun CoroutineScope.leaveSpace( + leaveSpaceAction: MutableState>, + @Suppress("unused") selectedRoomIds: Set, + ) = launch { + runUpdatingState(leaveSpaceAction) { + // TODO SDK API call to leave all the rooms and space + delay(1000) + val room = matrixClient.getRoom(inputs.roomId) + ?: return@runUpdatingState Result.failure(Exception("Room not found")) + room.leave() + } + } +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceState.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceState.kt new file mode 100644 index 0000000000..e0862bae49 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceState.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.space.impl.leave + +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.core.bool.orFalse +import kotlinx.collections.immutable.ImmutableList + +data class LeaveSpaceState( + val spaceName: String?, + val selectableSpaceRooms: AsyncData>, + val leaveSpaceAction: AsyncAction, + val eventSink: (LeaveSpaceEvents) -> Unit, +) { + val showQuickAction = selectableSpaceRooms.dataOrNull().orEmpty().any { !it.isLastAdmin } + val hasOnlyLastAdminRoom = selectableSpaceRooms.dataOrNull() + ?.let { rooms -> + rooms.isNotEmpty() && rooms.all { it.isLastAdmin } + } + .orFalse() + val numberOfSelectRooms = selectableSpaceRooms.dataOrNull().orEmpty().count { it.isSelected } + + val areAllSelected = selectableSpaceRooms.dataOrNull() + ?.filter { !it.isLastAdmin } + ?.let { rooms -> + rooms.isNotEmpty() && rooms.all { it.isSelected } + } + .orFalse() +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateProvider.kt new file mode 100644 index 0000000000..0e1424c7b8 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateProvider.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.space.impl.leave + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.AsyncData +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 +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList + +class LeaveSpaceStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aLeaveSpaceState(), + aLeaveSpaceState( + spaceName = null, + selectableSpaceRooms = AsyncData.Success(persistentListOf()), + ), + aLeaveSpaceState( + selectableSpaceRooms = AsyncData.Success( + persistentListOf( + aSelectableSpaceRoom( + spaceRoom = aSpaceRoom( + worldReadable = true, + ), + isLastAdmin = true, + ), + aSelectableSpaceRoom( + spaceRoom = aSpaceRoom( + joinRule = JoinRule.Private, + ), + isSelected = false, + ), + ) + ) + ), + aLeaveSpaceState( + selectableSpaceRooms = AsyncData.Success( + persistentListOf( + aSelectableSpaceRoom( + spaceRoom = aSpaceRoom( + worldReadable = true, + ), + isLastAdmin = true, + ), + aSelectableSpaceRoom( + spaceRoom = aSpaceRoom( + joinRule = JoinRule.Private, + ), + isSelected = true, + ), + ) + ) + ), + aLeaveSpaceState( + selectableSpaceRooms = AsyncData.Success( + persistentListOf( + aSelectableSpaceRoom( + spaceRoom = aSpaceRoom( + worldReadable = true, + ), + isLastAdmin = true, + ), + ) + ), + ), + aLeaveSpaceState( + selectableSpaceRooms = AsyncData.Success( + List(10) { aSelectableSpaceRoom() }.toPersistentList() + ), + leaveSpaceAction = AsyncAction.Loading, + ), + aLeaveSpaceState( + selectableSpaceRooms = AsyncData.Success( + List(10) { aSelectableSpaceRoom() }.toPersistentList() + ), + leaveSpaceAction = AsyncAction.Failure(Exception("An error")), + ), + aLeaveSpaceState( + selectableSpaceRooms = AsyncData.Failure(Exception("An error")), + ), + ) +} + +fun aLeaveSpaceState( + spaceName: String? = "Space name", + selectableSpaceRooms: AsyncData> = AsyncData.Uninitialized, + leaveSpaceAction: AsyncAction = AsyncAction.Uninitialized, +) = LeaveSpaceState( + spaceName = spaceName, + selectableSpaceRooms = selectableSpaceRooms, + leaveSpaceAction = leaveSpaceAction, + eventSink = { } +) + +fun aSelectableSpaceRoom( + spaceRoom: SpaceRoom = aSpaceRoom(), + isLastAdmin: Boolean = false, + isSelected: Boolean = false, +) = SelectableSpaceRoom( + spaceRoom = spaceRoom, + isLastAdmin = isLastAdmin, + isSelected = isSelected, +) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceView.kt new file mode 100644 index 0000000000..bccacd0e47 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceView.kt @@ -0,0 +1,312 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.space.impl.leave + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.selection.toggleable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +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.AsyncData +import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule +import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule +import io.element.android.libraries.designsystem.components.BigIcon +import io.element.android.libraries.designsystem.components.async.AsyncActionView +import io.element.android.libraries.designsystem.components.async.AsyncFailure +import io.element.android.libraries.designsystem.components.async.AsyncLoading +import io.element.android.libraries.designsystem.components.avatar.Avatar +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.components.avatar.AvatarType +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.Checkbox +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconSource +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.matrix.api.room.join.JoinRule +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 + +/** + * https://www.figma.com/design/kcnHxunG1LDWXsJhaNuiHz/ER-145--Spaces-on-Element-X?node-id=3947-68767&t=GTf1cLkAf6UCQDan-0 + */ +@Composable +fun LeaveSpaceView( + state: LeaveSpaceState, + onCancel: () -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + containerColor = ElementTheme.colors.bgCanvasDefault, + ) { padding -> + Column( + modifier = Modifier + .padding(padding) + .imePadding() + .consumeWindowInsets(padding) + .fillMaxSize() + .padding(16.dp) + ) { + LeaveSpaceHeader(state) + LazyColumn( + modifier = Modifier + .weight(1f), + ) { + when (state.selectableSpaceRooms) { + is AsyncData.Success -> { + // List rooms where the user is the only admin + state.selectableSpaceRooms.data.forEach { selectableSpaceRoom -> + item { + SpaceItem( + selectableSpaceRoom = selectableSpaceRoom, + onClick = { + state.eventSink(LeaveSpaceEvents.ToggleRoomSelection(selectableSpaceRoom.spaceRoom.roomId)) + } + ) + } + } + } + is AsyncData.Failure -> item { + AsyncFailure( + throwable = state.selectableSpaceRooms.error, + onRetry = null, + ) + } + is AsyncData.Loading, + AsyncData.Uninitialized -> item { + AsyncLoading() + } + } + } + LeaveSpaceButtons( + showLeaveButton = state.selectableSpaceRooms is AsyncData.Success, + nbOfSelectedRooms = state.numberOfSelectRooms, + onLeaveSpace = { + state.eventSink(LeaveSpaceEvents.LeaveSpace) + }, + onCancel = onCancel, + ) + } + } + + AsyncActionView( + async = state.leaveSpaceAction, + onSuccess = { /* Nothing to do, the screen will be dismissed automatically */ }, + onErrorDismiss = { state.eventSink(LeaveSpaceEvents.CloseError) }, + ) +} + +@Composable +private fun LeaveSpaceHeader(state: LeaveSpaceState) { + Column { + IconTitleSubtitleMolecule( + modifier = Modifier.padding(top = 24.dp, bottom = 8.dp, start = 24.dp, end = 24.dp), + iconStyle = BigIcon.Style.AlertSolid, + title = stringResource( + R.string.screen_leave_space_title, + state.spaceName ?: stringResource(CommonStrings.common_space) + ), + subTitle = + if (state.selectableSpaceRooms is AsyncData.Success && state.selectableSpaceRooms.data.isNotEmpty()) { + val count = state.selectableSpaceRooms.data.size + if (state.hasOnlyLastAdminRoom) { + pluralStringResource(R.plurals.screen_leave_space_subtitle_only_last_admin, count, count) + } else { + stringResource(R.string.screen_leave_space_subtitle) + } + } else { + null + }, + ) + if (state.showQuickAction) { + if (state.areAllSelected) { + Text( + modifier = Modifier + .align(Alignment.End) + .clickable { + state.eventSink(LeaveSpaceEvents.DeselectAllRooms) + } + .padding(vertical = 8.dp, horizontal = 8.dp), + text = stringResource(CommonStrings.common_deselect_all), + color = ElementTheme.colors.textActionPrimary, + style = ElementTheme.typography.fontBodyMdMedium, + ) + } else { + Text( + modifier = Modifier + .align(Alignment.End) + .clickable { + state.eventSink(LeaveSpaceEvents.SelectAllRooms) + } + .padding(vertical = 8.dp, horizontal = 8.dp), + text = stringResource(CommonStrings.common_select_all), + color = ElementTheme.colors.textActionPrimary, + style = ElementTheme.typography.fontBodyMdMedium, + ) + } + } + } +} + +@Composable +private fun LeaveSpaceButtons( + showLeaveButton: Boolean, + nbOfSelectedRooms: Int, + onLeaveSpace: () -> Unit, + onCancel: () -> Unit, +) { + ButtonColumnMolecule( + modifier = Modifier.padding(top = 16.dp) + ) { + if (showLeaveButton) { + val text = if (nbOfSelectedRooms > 0) { + pluralStringResource(R.plurals.screen_leave_space_submit, nbOfSelectedRooms, nbOfSelectedRooms) + } else { + stringResource(CommonStrings.action_leave_space) + } + Button( + modifier = Modifier.fillMaxWidth(), + text = text, + leadingIcon = IconSource.Vector(CompoundIcons.Leave()), + onClick = onLeaveSpace, + destructive = true, + ) + } + TextButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(CommonStrings.action_cancel), + onClick = onCancel, + ) + } +} + +@Composable +private fun SpaceItem( + selectableSpaceRoom: SelectableSpaceRoom, + onClick: () -> Unit, +) { + val room = selectableSpaceRoom.spaceRoom + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 66.dp) + .toggleable( + value = selectableSpaceRoom.isSelected, + role = Role.Checkbox, + enabled = selectableSpaceRoom.isLastAdmin.not(), + onValueChange = { onClick() } + ) + .clickable( + enabled = selectableSpaceRoom.isLastAdmin.not(), + // TODO + onClickLabel = null, + role = Role.Checkbox, + onClick = onClick, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Avatar( + modifier = Modifier.padding(horizontal = 16.dp), + avatarData = room.getAvatarData(AvatarSize.LeaveSpaceRoom), + avatarType = if (room.isSpace) AvatarType.Space() else AvatarType.Room(), + ) + Column( + modifier = Modifier.weight(1f), + ) { + Text( + modifier = Modifier + .padding(end = 16.dp), + text = room.name ?: stringResource( + if (room.isSpace) { + CommonStrings.common_no_space_name + } else { + CommonStrings.common_no_room_name + }, + ), + color = ElementTheme.colors.textPrimary, + style = ElementTheme.typography.fontBodyLgMedium, + maxLines = 1, + ) + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + if (room.joinRule == JoinRule.Private) { + // Picto for private + Icon( + modifier = Modifier + .size(16.dp) + .padding(end = 4.dp), + imageVector = CompoundIcons.LockSolid(), + contentDescription = null, + tint = ElementTheme.colors.iconTertiary, + ) + } else if (room.worldReadable) { + // Picto for world readable + Icon( + modifier = Modifier + .size(16.dp) + .padding(end = 4.dp), + imageVector = CompoundIcons.Public(), + contentDescription = null, + tint = ElementTheme.colors.iconTertiary, + ) + } + // Number of members + Text( + modifier = Modifier.padding(end = 16.dp), + text = pluralStringResource( + CommonPlurals.common_member_count, + room.numJoinedMembers, + room.numJoinedMembers + ), + color = ElementTheme.colors.textSecondary, + style = ElementTheme.typography.fontBodyMdRegular, + ) + } + } + Checkbox( + checked = selectableSpaceRoom.isSelected, + onCheckedChange = null, + enabled = selectableSpaceRoom.isLastAdmin.not(), + ) + } +} + +@PreviewsDayNight +@Composable +internal fun LeaveSpaceViewPreview( + @PreviewParameter(LeaveSpaceStateProvider::class) state: LeaveSpaceState, +) = ElementPreview { + LeaveSpaceView( + state = state, + onCancel = {}, + ) +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/SelectableSpaceRoom.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/SelectableSpaceRoom.kt new file mode 100644 index 0000000000..6247a9e48f --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/SelectableSpaceRoom.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.space.impl.leave + +import io.element.android.libraries.matrix.api.spaces.SpaceRoom + +data class SelectableSpaceRoom( + val spaceRoom: SpaceRoom, + val isLastAdmin: Boolean, + val isSelected: Boolean, +) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceEvents.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt similarity index 83% rename from features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceEvents.kt rename to features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt index 848dac3ebc..b978ae8010 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceEvents.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.space.impl +package io.element.android.features.space.impl.root sealed interface SpaceEvents { data object LoadMore : SpaceEvents diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceNode.kt new file mode 100644 index 0000000000..706777bfa7 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceNode.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.space.impl.root + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.lifecycleScope +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.Inject +import io.element.android.annotations.ContributesNode +import io.element.android.features.space.api.SpaceEntryPoint +import io.element.android.libraries.androidutils.R +import io.element.android.libraries.androidutils.system.startSharePlainTextIntent +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.coroutines.launch +import timber.log.Timber + +@ContributesNode(SessionScope::class) +@Inject +class SpaceNode( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: SpacePresenter.Factory, + private val matrixClient: MatrixClient, +) : Node(buildContext, plugins = plugins) { + interface Callback : Plugin { + fun onOpenRoom(roomId: RoomId, viaParameters: List) + fun onLeaveSpace() + } + + private val inputs: SpaceEntryPoint.Inputs = inputs() + private val callback = plugins.filterIsInstance().single() + private val presenter = presenterFactory.create(inputs) + + private fun onShareRoom(context: Context) = lifecycleScope.launch { + matrixClient.getRoom(inputs.roomId)?.use { room -> + room.getPermalink() + .onSuccess { permalink -> + context.startSharePlainTextIntent( + activityResultLauncher = null, + chooserTitle = context.getString(CommonStrings.common_share_space), + text = permalink, + noActivityFoundMessage = context.getString(R.string.error_no_compatible_app_found) + ) + } + .onFailure { + Timber.e(it) + } + } + } + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + val context = LocalContext.current + SpaceView( + state = state, + onBackClick = ::navigateUp, + onLeaveSpaceClick = { + callback.onLeaveSpace() + }, + onRoomClick = { spaceRoom -> + callback.onOpenRoom(spaceRoom.roomId, spaceRoom.via) + }, + onShareSpace = { + onShareRoom(context) + }, + modifier = modifier + ) + } +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt similarity index 98% rename from features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpacePresenter.kt rename to features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt index b46dd41e5c..3d4bcc8fdd 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpacePresenter.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.space.impl +package io.element.android.features.space.impl.root import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceState.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt similarity index 92% rename from features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceState.kt rename to features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt index ad822283ca..820123e32f 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceState.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.space.impl +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 diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt similarity index 97% rename from features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceStateProvider.kt rename to features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt index cf2fcf92b5..881fc20af2 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.space.impl +package io.element.android.features.space.impl.root import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.matrix.api.core.RoomId @@ -34,7 +34,7 @@ open class SpaceStateProvider : PreviewParameterProvider { aSpaceState( hasMoreToLoad = false, children = aListOfSpaceRooms() - ) + ), // Add other states here ) } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt similarity index 71% rename from features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceView.kt rename to features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt index f1f8356701..e8d5a8f2c3 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.space.impl +package io.element.android.features.space.impl.root import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row @@ -17,7 +17,10 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -28,6 +31,7 @@ import androidx.compose.ui.text.style.TextOverflow 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.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize @@ -36,6 +40,10 @@ import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreview 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.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton 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 @@ -51,13 +59,20 @@ import kotlinx.collections.immutable.toImmutableList fun SpaceView( state: SpaceState, onBackClick: () -> Unit, + onLeaveSpaceClick: () -> Unit, onRoomClick: (spaceRoom: SpaceRoom) -> Unit, + onShareSpace: () -> Unit, modifier: Modifier = Modifier, ) { Scaffold( modifier = modifier, topBar = { - SpaceViewTopBar(currentSpace = state.currentSpace, onBackClick = onBackClick) + SpaceViewTopBar( + state = state, + onBackClick = onBackClick, + onLeaveSpaceClick = onLeaveSpaceClick, + onShareSpace = onShareSpace, + ) }, content = { padding -> Box( @@ -140,10 +155,13 @@ private fun LoadingMoreIndicator( @OptIn(ExperimentalMaterial3Api::class) @Composable private fun SpaceViewTopBar( - currentSpace: SpaceRoom?, + state: SpaceState, onBackClick: () -> Unit, + onLeaveSpaceClick: () -> Unit, + onShareSpace: () -> Unit, modifier: Modifier = Modifier, ) { + val currentSpace = state.currentSpace TopAppBar( modifier = modifier, navigationIcon = { @@ -158,6 +176,48 @@ private fun SpaceViewTopBar( } }, actions = { + var showMenu by remember { mutableStateOf(false) } + IconButton( + onClick = { showMenu = !showMenu } + ) { + Icon( + imageVector = CompoundIcons.OverflowVertical(), + contentDescription = null, + ) + } + DropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false } + ) { + DropdownMenuItem( + onClick = { + showMenu = false + onShareSpace() + }, + text = { Text(stringResource(id = CommonStrings.action_share)) }, + leadingIcon = { + Icon( + imageVector = CompoundIcons.ShareAndroid(), + tint = ElementTheme.colors.iconSecondary, + contentDescription = null, + ) + } + ) + DropdownMenuItem( + onClick = { + showMenu = false + onLeaveSpaceClick() + }, + text = { Text(stringResource(id = CommonStrings.action_leave)) }, + leadingIcon = { + Icon( + imageVector = CompoundIcons.Leave(), + tint = ElementTheme.colors.iconSecondary, + contentDescription = null, + ) + } + ) + } }, ) } @@ -198,7 +258,9 @@ internal fun SpaceViewPreview( ) = ElementPreview { SpaceView( state = state, - onRoomClick = {}, onBackClick = {}, + onLeaveSpaceClick = {}, + onRoomClick = {}, + onShareSpace = {}, ) } diff --git a/features/space/impl/src/main/res/values/localazy.xml b/features/space/impl/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..016682b46c --- /dev/null +++ b/features/space/impl/src/main/res/values/localazy.xml @@ -0,0 +1,13 @@ + + + + "Leave %1$d room and space" + "Leave %1$d rooms and space" + + "Select the rooms you’d like to leave which you\'re not the only administrator for:" + + "You will not be removed from the following room because you\'re the only administrator:" + "You will not be removed from the following rooms because you\'re the only administrator:" + + "Leave %1$s?" + diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPointTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPointTest.kt index cafc825f6a..0410944dbb 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPointTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPointTest.kt @@ -9,14 +9,11 @@ package io.element.android.features.space.impl import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.testing.junit4.util.MainDispatcherRule import com.google.common.truth.Truth.assertThat -import io.element.android.features.invite.test.InMemorySeenInvitesStore import io.element.android.features.space.api.SpaceEntryPoint import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.test.A_ROOM_ID -import io.element.android.libraries.matrix.test.FakeMatrixClient -import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList -import io.element.android.libraries.matrix.test.spaces.FakeSpaceService import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.node.TestParentNode import org.junit.Rule @@ -26,38 +23,27 @@ class DefaultSpaceEntryPointTest { @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + @Test fun `test node builder`() { val entryPoint = DefaultSpaceEntryPoint() val nodeInputs = SpaceEntryPoint.Inputs(A_ROOM_ID) val parentNode = TestParentNode.create { buildContext, plugins -> - SpaceNode( + SpaceFlowNode( buildContext = buildContext, plugins = plugins, - presenterFactory = { inputs -> - assertThat(inputs).isEqualTo(nodeInputs) - SpacePresenter( - inputs = inputs, - client = FakeMatrixClient( - spaceService = FakeSpaceService( - spaceRoomListResult = { FakeSpaceRoomList() }, - ) - ), - seenInvitesStore = InMemorySeenInvitesStore(), - ) - }, ) } val callback = object : SpaceEntryPoint.Callback { - override fun onOpenRoom(roomId: RoomId, viaParameters: List) { - lambdaError() - } + override fun onOpenRoom(roomId: RoomId, viaParameters: List) = lambdaError() } val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) .inputs(nodeInputs) .callback(callback) .build() - assertThat(result).isInstanceOf(SpaceNode::class.java) + assertThat(result).isInstanceOf(SpaceFlowNode::class.java) assertThat(result.plugins).contains(nodeInputs) assertThat(result.plugins).contains(callback) } diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenterTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenterTest.kt new file mode 100644 index 0000000000..81866001cc --- /dev/null +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenterTest.kt @@ -0,0 +1,213 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.space.impl.leave + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.space.api.SpaceEntryPoint +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.MatrixClient +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_SPACE_NAME +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +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.lambda.lambdaRecorder +import io.element.android.tests.testutils.test +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class LeaveSpacePresenterTest { + @Test + fun `present - initial state`() = runTest { + val paginateResult = lambdaRecorder> { + Result.success(Unit) + } + val presenter = createLeaveSpacePresenter( + matrixClient = FakeMatrixClient( + spaceService = FakeSpaceService( + spaceRoomListResult = { + FakeSpaceRoomList( + paginateResult = paginateResult, + ) + }, + ), + ), + ) + presenter.test { + val state = awaitItem() + assertThat(state.spaceName).isNull() + assertThat(state.selectableSpaceRooms).isEqualTo(AsyncAction.Uninitialized) + assertThat(state.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized) + advanceUntilIdle() + paginateResult.assertions().isCalledOnce() + } + } + + @Test + fun `present - current space name`() = runTest { + val fakeSpaceRoomList = FakeSpaceRoomList() + val presenter = createLeaveSpacePresenter( + matrixClient = FakeMatrixClient( + spaceService = FakeSpaceService( + spaceRoomListResult = { fakeSpaceRoomList }, + ), + ), + ) + presenter.test { + val state = awaitItem() + advanceUntilIdle() + assertThat(state.spaceName).isNull() + val aSpace = aSpaceRoom( + name = A_SPACE_NAME + ) + fakeSpaceRoomList.emitCurrentSpace(aSpace) + assertThat(awaitItem().spaceName).isEqualTo(A_SPACE_NAME) + } + } + + @Test + fun `present - leave space and cancel`() = runTest { + val fakeSpaceRoomList = FakeSpaceRoomList( + paginateResult = { Result.success(Unit) }, + ) + val presenter = createLeaveSpacePresenter( + matrixClient = FakeMatrixClient( + spaceService = FakeSpaceService( + spaceRoomListResult = { fakeSpaceRoomList }, + ), + ), + ) + presenter.test { + val state = awaitItem() + advanceUntilIdle() + assertThat(state.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized) + state.eventSink(LeaveSpaceEvents.LeaveSpace) + val stateAfterStarting = awaitItem() + assertThat(stateAfterStarting.leaveSpaceAction).isInstanceOf(LeaveSpaceState::class.java) + val shown = stateAfterStarting.leaveSpaceAction as LeaveSpaceState + assertThat(shown.spaceName).isNull() + assertThat(shown.selectableSpaceRooms).isInstanceOf(AsyncData.Loading::class.java) + val stateAfterLoading = awaitItem() + val shownLoaded = stateAfterLoading.leaveSpaceAction as LeaveSpaceState + assertThat(shownLoaded.selectableSpaceRooms.dataOrNull()!!).isEmpty() + stateAfterLoading.eventSink(LeaveSpaceEvents.CloseError) + val stateAfterCancel = awaitItem() + assertThat(stateAfterCancel.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized) + } + } + + @Test + fun `present - leave space and confirm`() = runTest { + val fakeSpaceRoomList = FakeSpaceRoomList( + paginateResult = { Result.success(Unit) }, + ) + val leaveRoomLambda = lambdaRecorder> { + Result.success(Unit) + } + val presenter = createLeaveSpacePresenter( + matrixClient = FakeMatrixClient( + spaceService = FakeSpaceService( + spaceRoomListResult = { fakeSpaceRoomList }, + ), + ).apply { + givenGetRoomResult( + roomId = A_ROOM_ID, + result = FakeBaseRoom( + leaveRoomLambda = leaveRoomLambda, + ) + ) + }, + ) + presenter.test { + val state = awaitItem() + advanceUntilIdle() + assertThat(state.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized) + state.eventSink(LeaveSpaceEvents.LeaveSpace) + val stateAfterStarting = awaitItem() + assertThat(stateAfterStarting.leaveSpaceAction).isInstanceOf(LeaveSpaceState::class.java) + val shown = stateAfterStarting.leaveSpaceAction as LeaveSpaceState + assertThat(shown.spaceName).isNull() + assertThat(shown.selectableSpaceRooms).isInstanceOf(AsyncData.Loading::class.java) + val stateAfterLoading = awaitItem() + val shownLoaded = stateAfterLoading.leaveSpaceAction as LeaveSpaceState + assertThat(shownLoaded.selectableSpaceRooms.dataOrNull()!!).isEmpty() + stateAfterLoading.eventSink(LeaveSpaceEvents.LeaveSpace) + val stateLoading = awaitItem() + assertThat(stateLoading.leaveSpaceAction).isEqualTo(AsyncAction.Loading) + val stateFinal = awaitItem() + assertThat(stateFinal.leaveSpaceAction).isEqualTo(AsyncAction.Success(Unit)) + leaveRoomLambda.assertions().isCalledOnce() + } + } + + @Test + fun `present - leave space, confirm then failure`() = runTest { + val fakeSpaceRoomList = FakeSpaceRoomList( + paginateResult = { Result.success(Unit) }, + ) + val leaveRoomLambda = lambdaRecorder> { + Result.failure(AN_EXCEPTION) + } + val presenter = createLeaveSpacePresenter( + matrixClient = FakeMatrixClient( + spaceService = FakeSpaceService( + spaceRoomListResult = { fakeSpaceRoomList }, + ), + ).apply { + givenGetRoomResult( + roomId = A_ROOM_ID, + result = FakeBaseRoom( + leaveRoomLambda = leaveRoomLambda, + ) + ) + }, + ) + presenter.test { + val state = awaitItem() + advanceUntilIdle() + assertThat(state.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized) + state.eventSink(LeaveSpaceEvents.LeaveSpace) + val stateAfterStarting = awaitItem() + assertThat(stateAfterStarting.leaveSpaceAction).isInstanceOf(LeaveSpaceState::class.java) + val shown = stateAfterStarting.leaveSpaceAction as LeaveSpaceState + assertThat(shown.spaceName).isNull() + assertThat(shown.selectableSpaceRooms).isInstanceOf(AsyncData.Loading::class.java) + val stateAfterLoading = awaitItem() + val shownLoaded = stateAfterLoading.leaveSpaceAction as LeaveSpaceState + assertThat(shownLoaded.selectableSpaceRooms.dataOrNull()!!).isEmpty() + stateAfterLoading.eventSink(LeaveSpaceEvents.LeaveSpace) + val stateLoading = awaitItem() + assertThat(stateLoading.leaveSpaceAction).isEqualTo(AsyncAction.Loading) + val stateError = awaitItem() + assertThat(stateError.leaveSpaceAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION)) + leaveRoomLambda.assertions().isCalledOnce() + // Close error + stateError.eventSink(LeaveSpaceEvents.CloseError) + val stateFinal = awaitItem() + assertThat(stateFinal.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized) + } + } + + private fun createLeaveSpacePresenter( + inputs: SpaceEntryPoint.Inputs = SpaceEntryPoint.Inputs(A_ROOM_ID), + matrixClient: MatrixClient = FakeMatrixClient(), + ): LeaveSpacePresenter { + return LeaveSpacePresenter( + inputs = inputs, + matrixClient = matrixClient, + ) + } +} diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/SpacePresenterTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt similarity index 99% rename from features/space/impl/src/test/kotlin/io/element/android/features/space/impl/SpacePresenterTest.kt rename to features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt index 0bcd1303ae..1f096ff2b6 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/SpacePresenterTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt @@ -7,7 +7,7 @@ @file:OptIn(ExperimentalCoroutinesApi::class) -package io.element.android.features.space.impl +package io.element.android.features.space.impl.root import com.google.common.truth.Truth.assertThat import io.element.android.features.invite.api.SeenInvitesStore diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt index 72cf62c76a..c142fa0d39 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt @@ -72,4 +72,5 @@ enum class AvatarSize(val dp: Dp) { RoomPreviewHeader(64.dp), RoomPreviewInviter(56.dp), SpaceMember(24.dp), + LeaveSpaceRoom(32.dp), } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembershipObserver.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembershipObserver.kt index 19d7fdaaf2..a4a718b219 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembershipObserver.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembershipObserver.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.asSharedFlow class RoomMembershipObserver { data class RoomMembershipUpdate( val roomId: RoomId, + val isSpace: Boolean, val isUserInRoom: Boolean, val change: MembershipChange, ) @@ -22,12 +23,23 @@ class RoomMembershipObserver { private val _updates = MutableSharedFlow(extraBufferCapacity = 10) val updates = _updates.asSharedFlow() - suspend fun notifyUserLeftRoom(roomId: RoomId, membershipBeforeLeft: CurrentUserMembership) { + suspend fun notifyUserLeftRoom( + roomId: RoomId, + isSpace: Boolean, + membershipBeforeLeft: CurrentUserMembership, + ) { val membershipChange = when (membershipBeforeLeft) { CurrentUserMembership.INVITED -> MembershipChange.INVITATION_REJECTED CurrentUserMembership.KNOCKED -> MembershipChange.KNOCK_RETRACTED else -> MembershipChange.LEFT } - _updates.emit(RoomMembershipUpdate(roomId, false, membershipChange)) + _updates.emit( + RoomMembershipUpdate( + roomId = roomId, + isSpace = isSpace, + isUserInRoom = false, + change = membershipChange, + ) + ) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt index 1ca5915c71..8697818f0a 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt @@ -157,7 +157,11 @@ class RustBaseRoom( runCatchingExceptions { innerRoom.leave() }.onSuccess { - roomMembershipObserver.notifyUserLeftRoom(roomId, membershipBeforeLeft) + roomMembershipObserver.notifyUserLeftRoom( + roomId = roomId, + isSpace = roomInfoFlow.value.isSpace, + membershipBeforeLeft = membershipBeforeLeft, + ) } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoomTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoomTest.kt index e24eca53cb..50d6c348b5 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoomTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoomTest.kt @@ -57,6 +57,7 @@ class RustBaseRoomTest { leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) { val membershipUpdate = awaitItem() assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId) + assertThat(membershipUpdate.isSpace).isFalse() assertThat(membershipUpdate.isUserInRoom).isFalse() assertThat(membershipUpdate.change).isEqualTo(MembershipChange.LEFT) } @@ -77,6 +78,7 @@ class RustBaseRoomTest { leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) { val membershipUpdate = awaitItem() assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId) + assertThat(membershipUpdate.isSpace).isFalse() assertThat(membershipUpdate.isUserInRoom).isFalse() assertThat(membershipUpdate.change).isEqualTo(MembershipChange.KNOCK_RETRACTED) } @@ -97,6 +99,7 @@ class RustBaseRoomTest { leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) { val membershipUpdate = awaitItem() assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId) + assertThat(membershipUpdate.isSpace).isFalse() assertThat(membershipUpdate.isUserInRoom).isFalse() assertThat(membershipUpdate.change).isEqualTo(MembershipChange.INVITATION_REJECTED) } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt index ca0db64285..ed18a5ebd9 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt @@ -65,6 +65,8 @@ const val ANOTHER_MESSAGE = "Hello universe!" const val A_CAPTION = "A media caption" const val A_REASON = "A reason" +const val A_SPACE_NAME = "A space name" + const val A_REDACTION_REASON = "A redaction reason" const val A_HOMESERVER_URL = "matrix.org" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 2e678e6c28..9e3d8f6035 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -190,6 +190,7 @@ "Dark" "Decryption error" "Description" + "Deselect all" "Developer options" "Device ID" "Direct chat" @@ -301,6 +302,7 @@ Reason: %1$s." "Security" "Seen by" "Select an account" + "Select all" "Send to" "Sending…" "Sending failed" @@ -414,9 +416,6 @@ Are you sure you want to continue?" "Hey, talk to me on %1$s: %2$s" "%1$s Android" "Rageshake to report bug" - "This will also remove you from all rooms in this space." - "This will also remove you from all rooms in this space, including those you’re the only administrator for:" - "Leave %1$s?" "Screenshot" "%1$s: %2$s" "Options" @@ -460,6 +459,7 @@ Are you sure you want to continue?" "Share this location" "Spaces you have created or joined." "%1$s • %2$s" + "%1$s space" "Spaces" "Message not sent because %1$s’s verified identity was reset." "Message not sent because %1$s has not verified all devices." diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 4264b35229..2afebbef4d 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -200,6 +200,12 @@ "screen\\.security_and_privacy\\..*" ] }, + { + "name" : ":features:space:impl", + "includeRegex" : [ + "screen\\.leave_space\\..*" + ] + }, { "name" : ":features:userprofile:shared", "includeRegex" : [ From cbd591e102899caf447db0511c1b0aef7bd13d52 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 25 Sep 2025 22:54:40 +0200 Subject: [PATCH 02/12] Remove translations (key values have changed). Translations will be back during the next Localazy sync. --- libraries/ui-strings/src/main/res/values-cs/translations.xml | 3 --- libraries/ui-strings/src/main/res/values-cy/translations.xml | 3 --- libraries/ui-strings/src/main/res/values-de/translations.xml | 3 --- libraries/ui-strings/src/main/res/values-et/translations.xml | 3 --- libraries/ui-strings/src/main/res/values-fi/translations.xml | 3 --- .../ui-strings/src/main/res/values-zh-rTW/translations.xml | 3 --- 6 files changed, 18 deletions(-) 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 f3aa72c290..62dbd75f25 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -419,9 +419,6 @@ Opravdu chcete pokračovat?" "Ahoj, ozvi se mi na %1$s: %2$s" "%1$s Android" "Zatřeste zařízením pro nahlášení chyby" - "Tím budete také odstraněni ze všech místností v tomto prostoru." - "Tímto budete také odstraněni ze všech místností v tomto prostoru, včetně těch, jejichž jediným správcem jste:" - "Opustit %1$s?" "Snímek obrazovky" "%1$s: %2$s" "Možnosti" diff --git a/libraries/ui-strings/src/main/res/values-cy/translations.xml b/libraries/ui-strings/src/main/res/values-cy/translations.xml index a709e010d9..6c0788070f 100644 --- a/libraries/ui-strings/src/main/res/values-cy/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cy/translations.xml @@ -445,9 +445,6 @@ Ydych chi\'n siŵr eich bod am barhau?" "Hei, siaradwch â mi ar %1$s: %2$s" "Android %1$s" "Rageshake i adrodd gwall" - "Bydd hyn hefyd yn eich tynnu o bob ystafell yn y gofod hwn." - "Bydd hyn hefyd yn eich tynnu o bob ystafell yn y gofod hwn, gan gynnwys y rhai rydych chi\'n unig weinyddwr ar eu cyfer:" - "Gadael %1$s ?" "Llun sgrin" "%1$s: %2$s" "Dewisiadau" 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 39396928eb..842fc2cc57 100644 --- a/libraries/ui-strings/src/main/res/values-de/translations.xml +++ b/libraries/ui-strings/src/main/res/values-de/translations.xml @@ -412,9 +412,6 @@ Möchtest du wirklich fortfahren?" "Hey, sprich mit mir auf %1$s: %2$s" "%1$s Android" "Heftiges Schütteln um Fehler zu melden" - "Dadurch wirst du auch aus allen Chats in diesem Space entfernt." - "Dadurch wirst du auch aus allen Chats in diesem Space entfernt, auch aus denen, für die du der einzige Admin bist:" - "%1$s verlassen?" "Bildschirmfoto" "%1$s: %2$s" "Optionen" 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 e02bba3558..1a341ee1de 100644 --- a/libraries/ui-strings/src/main/res/values-et/translations.xml +++ b/libraries/ui-strings/src/main/res/values-et/translations.xml @@ -411,9 +411,6 @@ Kas sa oled kindel, et soovid jätkata?" "Hei, suhtle minuga %1$s võrgus: %2$s" "%1$s Android" "Veast teatamiseks raputa nutiseadet ägedalt" - "Sellega eemaldad end ka kõikidest antud kogukonna jututubadest." - "Sellega eemaldad end ka kõikidest antud kogukonna jututubadest, sealhulgast järgnevaist, kus oled ainus peakasutaja:" - "Kas lahkud %1$s kogukonnast?" "Ekraanitõmmis" "%1$s: %2$s" "Valikud" 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 326d67c2d9..f819a17bfa 100644 --- a/libraries/ui-strings/src/main/res/values-fi/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fi/translations.xml @@ -413,9 +413,6 @@ Haluatko varmasti jatkaa?" "Hei, keskustele kanssani %1$s -sovelluksessa: %2$s" "%1$s Android" "Raivostunut ravistaminen ilmoittaa virheestä" - "Tämä poistaa sinut myös kaikista tämän tilan huoneista." - "Tämä poistaa sinut myös kaikista tämän tilan huoneista, mukaan lukien ne, joissa olet ainoa ylläpitäjä:" - "Haluatko poistua tilasta %1$s?" "Näyttökuva" "%1$s: %2$s" "Vaihtoehdot" 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 93b486e173..a093b237fa 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 @@ -404,9 +404,6 @@ "嘿,來 %1$s 和我聊天:%2$s" "%1$s Android" "憤怒搖晃以回報臭蟲" - "這也會將您從此空間中的所有聊天室移除。" - "這也會將您從此空間中的所有聊天室移除,包含您是唯一管理員的聊天室:" - "離開 %1$s?" "螢幕截圖" "%1$s:%2$s" "選項" From 9732d55a203374e1b5aa839f5e6e5917d78eefc8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 25 Sep 2025 23:02:36 +0200 Subject: [PATCH 03/12] Add the (Admin) info. --- .../impl/leave/LeaveSpaceStateProvider.kt | 1 + .../space/impl/leave/LeaveSpaceView.kt | 23 +++++++++++++++---- .../impl/src/main/res/values/localazy.xml | 1 + 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateProvider.kt index 0e1424c7b8..9f70a24939 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateProvider.kt @@ -30,6 +30,7 @@ class LeaveSpaceStateProvider : PreviewParameterProvider { persistentListOf( aSelectableSpaceRoom( spaceRoom = aSpaceRoom( + name = "A long space name that should be truncated", worldReadable = true, ), isLastAdmin = true, diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceView.kt index bccacd0e47..8dea64c73e 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceView.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme @@ -254,6 +255,7 @@ private fun SpaceItem( color = ElementTheme.colors.textPrimary, style = ElementTheme.typography.fontBodyLgMedium, maxLines = 1, + overflow = TextOverflow.Ellipsis, ) Row( verticalAlignment = Alignment.CenterVertically, @@ -280,15 +282,26 @@ private fun SpaceItem( ) } // Number of members + val subTitle = buildString { + append( + pluralStringResource( + CommonPlurals.common_member_count, + room.numJoinedMembers, + room.numJoinedMembers + ) + ) + if (selectableSpaceRoom.isLastAdmin) { + append(" ") + append(stringResource(R.string.screen_leave_space_last_admin_info)) + } + } Text( modifier = Modifier.padding(end = 16.dp), - text = pluralStringResource( - CommonPlurals.common_member_count, - room.numJoinedMembers, - room.numJoinedMembers - ), + text = subTitle, color = ElementTheme.colors.textSecondary, style = ElementTheme.typography.fontBodyMdRegular, + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) } } diff --git a/features/space/impl/src/main/res/values/localazy.xml b/features/space/impl/src/main/res/values/localazy.xml index 016682b46c..ac031defa3 100644 --- a/features/space/impl/src/main/res/values/localazy.xml +++ b/features/space/impl/src/main/res/values/localazy.xml @@ -1,5 +1,6 @@ + "(Admin)" "Leave %1$d room and space" "Leave %1$d rooms and space" From de6f52c72a12d11c9fa55d6bf4f0fe414b4c401e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 26 Sep 2025 09:39:22 +0200 Subject: [PATCH 04/12] Add unit test on LeaveSpaceState --- .../space/impl/leave/LeaveSpaceState.kt | 39 +++++-- .../space/impl/leave/LeaveSpaceStateTest.kt | 105 ++++++++++++++++++ 2 files changed, 132 insertions(+), 12 deletions(-) create mode 100644 features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateTest.kt diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceState.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceState.kt index e0862bae49..6aa29925c2 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceState.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceState.kt @@ -9,7 +9,6 @@ package io.element.android.features.space.impl.leave import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.core.bool.orFalse import kotlinx.collections.immutable.ImmutableList data class LeaveSpaceState( @@ -18,18 +17,34 @@ data class LeaveSpaceState( val leaveSpaceAction: AsyncAction, val eventSink: (LeaveSpaceEvents) -> Unit, ) { - val showQuickAction = selectableSpaceRooms.dataOrNull().orEmpty().any { !it.isLastAdmin } - val hasOnlyLastAdminRoom = selectableSpaceRooms.dataOrNull() - ?.let { rooms -> - rooms.isNotEmpty() && rooms.all { it.isLastAdmin } - } - .orFalse() - val numberOfSelectRooms = selectableSpaceRooms.dataOrNull().orEmpty().count { it.isSelected } + private val rooms = selectableSpaceRooms.dataOrNull().orEmpty() - val areAllSelected = selectableSpaceRooms.dataOrNull() - ?.filter { !it.isLastAdmin } - ?.let { rooms -> + /** + * True if we should show the quick action to select/deselect all rooms. + */ + val showQuickAction = rooms + .any { !it.isLastAdmin } + + /** + * True if there are rooms and they are all selected. + */ + val areAllSelected = rooms + .filter { !it.isLastAdmin } + .let { rooms -> rooms.isNotEmpty() && rooms.all { it.isSelected } } - .orFalse() + + /** + * True if there are rooms but the user is the last admin in all of them. + */ + val hasOnlyLastAdminRoom = rooms + .let { rooms -> + rooms.isNotEmpty() && rooms.all { it.isLastAdmin } + } + + /** + * Number of selected rooms. + */ + val numberOfSelectRooms = rooms + .count { it.isSelected } } diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateTest.kt new file mode 100644 index 0000000000..060b5043a3 --- /dev/null +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateTest.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.space.impl.leave + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.architecture.AsyncData +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import org.junit.Test + +class LeaveSpaceStateTest { + @Test + fun `test loading`() { + val sut = aLeaveSpaceState( + selectableSpaceRooms = AsyncData.Loading() + ) + assertThat(sut.showQuickAction).isFalse() + assertThat(sut.areAllSelected).isFalse() + assertThat(sut.hasOnlyLastAdminRoom).isFalse() + assertThat(sut.numberOfSelectRooms).isEqualTo(0) + } + + @Test + fun `test no rooms`() { + val sut = aLeaveSpaceState( + selectableSpaceRooms = AsyncData.Success( + persistentListOf() + ) + ) + assertThat(sut.showQuickAction).isFalse() + assertThat(sut.areAllSelected).isFalse() + assertThat(sut.hasOnlyLastAdminRoom).isFalse() + assertThat(sut.numberOfSelectRooms).isEqualTo(0) + } + + @Test + fun `test no last admin, 1 selected, 1 not selected`() { + val sut = aLeaveSpaceState( + selectableSpaceRooms = AsyncData.Success( + listOf( + aSelectableSpaceRoom(isLastAdmin = false, isSelected = true), + aSelectableSpaceRoom(isLastAdmin = false, isSelected = false), + ).toPersistentList() + ) + ) + assertThat(sut.showQuickAction).isTrue() + assertThat(sut.areAllSelected).isFalse() + assertThat(sut.hasOnlyLastAdminRoom).isFalse() + assertThat(sut.numberOfSelectRooms).isEqualTo(1) + } + + @Test + fun `test no last admin, 2 selected`() { + val sut = aLeaveSpaceState( + selectableSpaceRooms = AsyncData.Success( + listOf( + aSelectableSpaceRoom(isLastAdmin = false, isSelected = true), + aSelectableSpaceRoom(isLastAdmin = false, isSelected = true), + ).toPersistentList() + ) + ) + assertThat(sut.showQuickAction).isTrue() + assertThat(sut.areAllSelected).isTrue() + assertThat(sut.hasOnlyLastAdminRoom).isFalse() + assertThat(sut.numberOfSelectRooms).isEqualTo(2) + } + + @Test + fun `test 1 last admin, 2 selected`() { + val sut = aLeaveSpaceState( + selectableSpaceRooms = AsyncData.Success( + listOf( + aSelectableSpaceRoom(isLastAdmin = true, isSelected = false), + aSelectableSpaceRoom(isLastAdmin = false, isSelected = true), + aSelectableSpaceRoom(isLastAdmin = false, isSelected = true), + ).toPersistentList() + ) + ) + assertThat(sut.showQuickAction).isTrue() + assertThat(sut.areAllSelected).isTrue() + assertThat(sut.hasOnlyLastAdminRoom).isFalse() + assertThat(sut.numberOfSelectRooms).isEqualTo(2) + } + + @Test + fun `test only last admin`() { + val sut = aLeaveSpaceState( + selectableSpaceRooms = AsyncData.Success( + listOf( + aSelectableSpaceRoom(isLastAdmin = true, isSelected = false), + aSelectableSpaceRoom(isLastAdmin = true, isSelected = false), + ).toPersistentList() + ) + ) + assertThat(sut.showQuickAction).isFalse() + assertThat(sut.areAllSelected).isFalse() + assertThat(sut.hasOnlyLastAdminRoom).isTrue() + assertThat(sut.numberOfSelectRooms).isEqualTo(0) + } +} From f503bc401ef5906fe58f20d8f448b190c3ab3340 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 26 Sep 2025 09:39:40 +0200 Subject: [PATCH 05/12] Select all rooms by default --- .../features/space/impl/leave/LeaveSpacePresenter.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenter.kt index 21e87e95e3..59713f2f82 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenter.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenter.kt @@ -65,6 +65,8 @@ class LeaveSpacePresenter( roomId = RoomId("!roomId2:example.com"), ), ) + // By default select all rooms + selectedRoomIds.value = rooms.map { it.roomId }.toSet() value = rooms } val selectableSpaceRooms by produceState>>( @@ -86,7 +88,9 @@ class LeaveSpacePresenter( fun handleEvents(event: LeaveSpaceEvents) { when (event) { - LeaveSpaceEvents.DeselectAllRooms -> selectedRoomIds.value = emptySet() + LeaveSpaceEvents.DeselectAllRooms -> { + selectedRoomIds.value = emptySet() + } LeaveSpaceEvents.SelectAllRooms -> { selectedRoomIds.value = selectableSpaceRooms.dataOrNull() .orEmpty() From 5afe213fe4d91470379175cdadf30a27e117a2e4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 26 Sep 2025 11:38:07 +0200 Subject: [PATCH 06/12] Update UI --- .../impl/leave/LeaveSpaceStateProvider.kt | 16 ++++++++ .../space/impl/leave/LeaveSpaceView.kt | 37 +++++++++++++++---- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateProvider.kt index 9f70a24939..6795cba3a7 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateProvider.kt @@ -74,6 +74,22 @@ class LeaveSpaceStateProvider : PreviewParameterProvider { ) ), ), + aLeaveSpaceState( + selectableSpaceRooms = AsyncData.Success( + persistentListOf( + aSelectableSpaceRoom( + spaceRoom = aSpaceRoom( + worldReadable = true, + ), + isLastAdmin = true, + ), + aSelectableSpaceRoom( + spaceRoom = aSpaceRoom(), + isLastAdmin = true, + ), + ) + ), + ), aLeaveSpaceState( selectableSpaceRooms = AsyncData.Success( List(10) { aSelectableSpaceRoom() }.toPersistentList() diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceView.kt index 8dea64c73e..5a894e0e7a 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceView.kt @@ -5,6 +5,8 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalMaterial3Api::class) + package io.element.android.features.space.impl.leave import androidx.compose.foundation.clickable @@ -19,6 +21,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.selection.toggleable +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -41,6 +44,7 @@ import io.element.android.libraries.designsystem.components.async.AsyncLoading import io.element.android.libraries.designsystem.components.avatar.Avatar 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.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Button @@ -50,6 +54,7 @@ import io.element.android.libraries.designsystem.theme.components.IconSource 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.join.JoinRule import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.ui.strings.CommonPlurals @@ -76,7 +81,10 @@ fun LeaveSpaceView( .fillMaxSize() .padding(16.dp) ) { - LeaveSpaceHeader(state) + LeaveSpaceHeader( + state = state, + onBackClick = onCancel, + ) LazyColumn( modifier = Modifier .weight(1f), @@ -88,6 +96,7 @@ fun LeaveSpaceView( item { SpaceItem( selectableSpaceRoom = selectableSpaceRoom, + showCheckBox = state.hasOnlyLastAdminRoom.not(), onClick = { state.eventSink(LeaveSpaceEvents.ToggleRoomSelection(selectableSpaceRoom.spaceRoom.roomId)) } @@ -126,10 +135,19 @@ fun LeaveSpaceView( } @Composable -private fun LeaveSpaceHeader(state: LeaveSpaceState) { +private fun LeaveSpaceHeader( + state: LeaveSpaceState, + onBackClick: () -> Unit, +) { Column { + TopAppBar( + navigationIcon = { + BackButton(onClick = onBackClick) + }, + title = {}, + ) IconTitleSubtitleMolecule( - modifier = Modifier.padding(top = 24.dp, bottom = 8.dp, start = 24.dp, end = 24.dp), + modifier = Modifier.padding(top = 0.dp, bottom = 8.dp, start = 24.dp, end = 24.dp), iconStyle = BigIcon.Style.AlertSolid, title = stringResource( R.string.screen_leave_space_title, @@ -212,6 +230,7 @@ private fun LeaveSpaceButtons( @Composable private fun SpaceItem( selectableSpaceRoom: SelectableSpaceRoom, + showCheckBox: Boolean, onClick: () -> Unit, ) { val room = selectableSpaceRoom.spaceRoom @@ -305,11 +324,13 @@ private fun SpaceItem( ) } } - Checkbox( - checked = selectableSpaceRoom.isSelected, - onCheckedChange = null, - enabled = selectableSpaceRoom.isLastAdmin.not(), - ) + if (showCheckBox) { + Checkbox( + checked = selectableSpaceRoom.isSelected, + onCheckedChange = null, + enabled = selectableSpaceRoom.isLastAdmin.not(), + ) + } } } From b4232a83fd4b38ce6bbac34d54083e131c1353e8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 26 Sep 2025 11:59:29 +0200 Subject: [PATCH 07/12] Update tests --- .../impl/leave/LeaveSpacePresenterTest.kt | 139 +----------------- 1 file changed, 4 insertions(+), 135 deletions(-) diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenterTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenterTest.kt index 81866001cc..5129181306 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenterTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenterTest.kt @@ -14,15 +14,12 @@ import io.element.android.features.space.api.SpaceEntryPoint import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.MatrixClient -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_SPACE_NAME import io.element.android.libraries.matrix.test.FakeMatrixClient -import io.element.android.libraries.matrix.test.room.FakeBaseRoom 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.lambda.lambdaRecorder import io.element.android.tests.testutils.test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceUntilIdle @@ -32,16 +29,11 @@ import org.junit.Test class LeaveSpacePresenterTest { @Test fun `present - initial state`() = runTest { - val paginateResult = lambdaRecorder> { - Result.success(Unit) - } val presenter = createLeaveSpacePresenter( matrixClient = FakeMatrixClient( spaceService = FakeSpaceService( spaceRoomListResult = { - FakeSpaceRoomList( - paginateResult = paginateResult, - ) + FakeSpaceRoomList() }, ), ), @@ -49,10 +41,9 @@ class LeaveSpacePresenterTest { presenter.test { val state = awaitItem() assertThat(state.spaceName).isNull() - assertThat(state.selectableSpaceRooms).isEqualTo(AsyncAction.Uninitialized) + assertThat(state.selectableSpaceRooms).isEqualTo(AsyncData.Uninitialized) assertThat(state.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized) - advanceUntilIdle() - paginateResult.assertions().isCalledOnce() + skipItems(1) } } @@ -74,133 +65,11 @@ class LeaveSpacePresenterTest { name = A_SPACE_NAME ) fakeSpaceRoomList.emitCurrentSpace(aSpace) + skipItems(1) assertThat(awaitItem().spaceName).isEqualTo(A_SPACE_NAME) } } - @Test - fun `present - leave space and cancel`() = runTest { - val fakeSpaceRoomList = FakeSpaceRoomList( - paginateResult = { Result.success(Unit) }, - ) - val presenter = createLeaveSpacePresenter( - matrixClient = FakeMatrixClient( - spaceService = FakeSpaceService( - spaceRoomListResult = { fakeSpaceRoomList }, - ), - ), - ) - presenter.test { - val state = awaitItem() - advanceUntilIdle() - assertThat(state.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized) - state.eventSink(LeaveSpaceEvents.LeaveSpace) - val stateAfterStarting = awaitItem() - assertThat(stateAfterStarting.leaveSpaceAction).isInstanceOf(LeaveSpaceState::class.java) - val shown = stateAfterStarting.leaveSpaceAction as LeaveSpaceState - assertThat(shown.spaceName).isNull() - assertThat(shown.selectableSpaceRooms).isInstanceOf(AsyncData.Loading::class.java) - val stateAfterLoading = awaitItem() - val shownLoaded = stateAfterLoading.leaveSpaceAction as LeaveSpaceState - assertThat(shownLoaded.selectableSpaceRooms.dataOrNull()!!).isEmpty() - stateAfterLoading.eventSink(LeaveSpaceEvents.CloseError) - val stateAfterCancel = awaitItem() - assertThat(stateAfterCancel.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized) - } - } - - @Test - fun `present - leave space and confirm`() = runTest { - val fakeSpaceRoomList = FakeSpaceRoomList( - paginateResult = { Result.success(Unit) }, - ) - val leaveRoomLambda = lambdaRecorder> { - Result.success(Unit) - } - val presenter = createLeaveSpacePresenter( - matrixClient = FakeMatrixClient( - spaceService = FakeSpaceService( - spaceRoomListResult = { fakeSpaceRoomList }, - ), - ).apply { - givenGetRoomResult( - roomId = A_ROOM_ID, - result = FakeBaseRoom( - leaveRoomLambda = leaveRoomLambda, - ) - ) - }, - ) - presenter.test { - val state = awaitItem() - advanceUntilIdle() - assertThat(state.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized) - state.eventSink(LeaveSpaceEvents.LeaveSpace) - val stateAfterStarting = awaitItem() - assertThat(stateAfterStarting.leaveSpaceAction).isInstanceOf(LeaveSpaceState::class.java) - val shown = stateAfterStarting.leaveSpaceAction as LeaveSpaceState - assertThat(shown.spaceName).isNull() - assertThat(shown.selectableSpaceRooms).isInstanceOf(AsyncData.Loading::class.java) - val stateAfterLoading = awaitItem() - val shownLoaded = stateAfterLoading.leaveSpaceAction as LeaveSpaceState - assertThat(shownLoaded.selectableSpaceRooms.dataOrNull()!!).isEmpty() - stateAfterLoading.eventSink(LeaveSpaceEvents.LeaveSpace) - val stateLoading = awaitItem() - assertThat(stateLoading.leaveSpaceAction).isEqualTo(AsyncAction.Loading) - val stateFinal = awaitItem() - assertThat(stateFinal.leaveSpaceAction).isEqualTo(AsyncAction.Success(Unit)) - leaveRoomLambda.assertions().isCalledOnce() - } - } - - @Test - fun `present - leave space, confirm then failure`() = runTest { - val fakeSpaceRoomList = FakeSpaceRoomList( - paginateResult = { Result.success(Unit) }, - ) - val leaveRoomLambda = lambdaRecorder> { - Result.failure(AN_EXCEPTION) - } - val presenter = createLeaveSpacePresenter( - matrixClient = FakeMatrixClient( - spaceService = FakeSpaceService( - spaceRoomListResult = { fakeSpaceRoomList }, - ), - ).apply { - givenGetRoomResult( - roomId = A_ROOM_ID, - result = FakeBaseRoom( - leaveRoomLambda = leaveRoomLambda, - ) - ) - }, - ) - presenter.test { - val state = awaitItem() - advanceUntilIdle() - assertThat(state.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized) - state.eventSink(LeaveSpaceEvents.LeaveSpace) - val stateAfterStarting = awaitItem() - assertThat(stateAfterStarting.leaveSpaceAction).isInstanceOf(LeaveSpaceState::class.java) - val shown = stateAfterStarting.leaveSpaceAction as LeaveSpaceState - assertThat(shown.spaceName).isNull() - assertThat(shown.selectableSpaceRooms).isInstanceOf(AsyncData.Loading::class.java) - val stateAfterLoading = awaitItem() - val shownLoaded = stateAfterLoading.leaveSpaceAction as LeaveSpaceState - assertThat(shownLoaded.selectableSpaceRooms.dataOrNull()!!).isEmpty() - stateAfterLoading.eventSink(LeaveSpaceEvents.LeaveSpace) - val stateLoading = awaitItem() - assertThat(stateLoading.leaveSpaceAction).isEqualTo(AsyncAction.Loading) - val stateError = awaitItem() - assertThat(stateError.leaveSpaceAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION)) - leaveRoomLambda.assertions().isCalledOnce() - // Close error - stateError.eventSink(LeaveSpaceEvents.CloseError) - val stateFinal = awaitItem() - assertThat(stateFinal.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized) - } - } - private fun createLeaveSpacePresenter( inputs: SpaceEntryPoint.Inputs = SpaceEntryPoint.Inputs(A_ROOM_ID), matrixClient: MatrixClient = FakeMatrixClient(), From c1678b548bcaea6750ffeb6ed66dbc2da780fc12 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 26 Sep 2025 12:10:41 +0200 Subject: [PATCH 08/12] Cleanup to be able to merge. --- .../space/impl/leave/LeaveSpacePresenter.kt | 23 +++++-------------- .../features/space/impl/root/SpaceView.kt | 5 +++- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenter.kt index 59713f2f82..810b73cffd 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenter.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenter.kt @@ -25,18 +25,17 @@ import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runUpdatingState import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.previewutils.room.aSpaceRoom +import io.element.android.libraries.matrix.api.spaces.SpaceRoom import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlin.jvm.optionals.getOrNull @Inject class LeaveSpacePresenter( @Assisted private val inputs: SpaceEntryPoint.Inputs, - private val matrixClient: MatrixClient, + matrixClient: MatrixClient, ) : Presenter { @AssistedFactory fun interface Factory { @@ -57,14 +56,7 @@ class LeaveSpacePresenter( } val joinedSpaceRooms by produceState(emptyList()) { // TODO Get the joined room from the SDK, should also have the - val rooms = listOf( - aSpaceRoom( - roomId = RoomId("!roomId1:example.com"), - ), - aSpaceRoom( - roomId = RoomId("!roomId2:example.com"), - ), - ) + val rooms = emptyList() // By default select all rooms selectedRoomIds.value = rooms.map { it.roomId }.toSet() value = rooms @@ -77,10 +69,10 @@ class LeaveSpacePresenter( value = AsyncData.Success( joinedSpaceRooms.map { SelectableSpaceRoom( - it, + spaceRoom = it, // TODO Get this value from the SDK isLastAdmin = false, - selectedRoomIds.value.contains(it.roomId), + isSelected = selectedRoomIds.value.contains(it.roomId), ) }.toPersistentList() ) @@ -130,10 +122,7 @@ class LeaveSpacePresenter( ) = launch { runUpdatingState(leaveSpaceAction) { // TODO SDK API call to leave all the rooms and space - delay(1000) - val room = matrixClient.getRoom(inputs.roomId) - ?: return@runUpdatingState Result.failure(Exception("Room not found")) - room.leave() + Result.failure(Exception("Not implemented")) } } } 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 e8d5a8f2c3..2f9e4ac291 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 @@ -157,7 +157,7 @@ private fun LoadingMoreIndicator( private fun SpaceViewTopBar( state: SpaceState, onBackClick: () -> Unit, - onLeaveSpaceClick: () -> Unit, + @Suppress("unused") onLeaveSpaceClick: () -> Unit, onShareSpace: () -> Unit, modifier: Modifier = Modifier, ) { @@ -203,6 +203,8 @@ private fun SpaceViewTopBar( ) } ) + /* + // TODO re-enable when we have SDK APIs to leave a space DropdownMenuItem( onClick = { showMenu = false @@ -217,6 +219,7 @@ private fun SpaceViewTopBar( ) } ) + */ } }, ) From 4d1a5031da9c59f336d0889ea32a2d1be6e923e1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 26 Sep 2025 14:15:02 +0200 Subject: [PATCH 09/12] Metro now have `@AssistedInject`. --- .../io/element/android/features/space/impl/root/SpaceNode.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceNode.kt index 706777bfa7..768bd9c795 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceNode.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceNode.kt @@ -16,7 +16,7 @@ import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import dev.zacsweers.metro.Assisted -import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode import io.element.android.features.space.api.SpaceEntryPoint import io.element.android.libraries.androidutils.R @@ -30,7 +30,7 @@ import kotlinx.coroutines.launch import timber.log.Timber @ContributesNode(SessionScope::class) -@Inject +@AssistedInject class SpaceNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, From 692111790ef447a13e9dc9b55f5ce5b6c3975fbe Mon Sep 17 00:00:00 2001 From: ElementBot Date: Fri, 26 Sep 2025 12:33:35 +0000 Subject: [PATCH 10/12] Update screenshots --- .../features.space.impl.leave_LeaveSpaceView_Day_0_en.png | 3 +++ .../features.space.impl.leave_LeaveSpaceView_Day_1_en.png | 3 +++ .../features.space.impl.leave_LeaveSpaceView_Day_2_en.png | 3 +++ .../features.space.impl.leave_LeaveSpaceView_Day_3_en.png | 3 +++ .../features.space.impl.leave_LeaveSpaceView_Day_4_en.png | 3 +++ .../features.space.impl.leave_LeaveSpaceView_Day_5_en.png | 3 +++ .../features.space.impl.leave_LeaveSpaceView_Day_6_en.png | 3 +++ .../features.space.impl.leave_LeaveSpaceView_Day_7_en.png | 3 +++ .../features.space.impl.leave_LeaveSpaceView_Day_8_en.png | 3 +++ .../features.space.impl.leave_LeaveSpaceView_Night_0_en.png | 3 +++ .../features.space.impl.leave_LeaveSpaceView_Night_1_en.png | 3 +++ .../features.space.impl.leave_LeaveSpaceView_Night_2_en.png | 3 +++ .../features.space.impl.leave_LeaveSpaceView_Night_3_en.png | 3 +++ .../features.space.impl.leave_LeaveSpaceView_Night_4_en.png | 3 +++ .../features.space.impl.leave_LeaveSpaceView_Night_5_en.png | 3 +++ .../features.space.impl.leave_LeaveSpaceView_Night_6_en.png | 3 +++ .../features.space.impl.leave_LeaveSpaceView_Night_7_en.png | 3 +++ .../features.space.impl.leave_LeaveSpaceView_Night_8_en.png | 3 +++ .../images/features.space.impl.root_SpaceView_Day_0_en.png | 3 +++ .../images/features.space.impl.root_SpaceView_Day_1_en.png | 3 +++ .../images/features.space.impl.root_SpaceView_Day_2_en.png | 3 +++ .../images/features.space.impl.root_SpaceView_Day_3_en.png | 3 +++ .../images/features.space.impl.root_SpaceView_Night_0_en.png | 3 +++ .../images/features.space.impl.root_SpaceView_Night_1_en.png | 3 +++ .../images/features.space.impl.root_SpaceView_Night_2_en.png | 3 +++ .../images/features.space.impl.root_SpaceView_Night_3_en.png | 3 +++ .../images/features.space.impl_SpaceView_Day_0_en.png | 3 --- .../images/features.space.impl_SpaceView_Day_1_en.png | 3 --- .../images/features.space.impl_SpaceView_Day_2_en.png | 3 --- .../images/features.space.impl_SpaceView_Day_3_en.png | 3 --- .../images/features.space.impl_SpaceView_Night_0_en.png | 3 --- .../images/features.space.impl_SpaceView_Night_1_en.png | 3 --- .../images/features.space.impl_SpaceView_Night_2_en.png | 3 --- .../images/features.space.impl_SpaceView_Night_3_en.png | 3 --- ...es.designsystem.components.avatar_Avatar_Avatars_114_en.png | 3 +++ ...es.designsystem.components.avatar_Avatar_Avatars_115_en.png | 3 +++ ...es.designsystem.components.avatar_Avatar_Avatars_116_en.png | 3 +++ 37 files changed, 87 insertions(+), 24 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_5_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_6_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_7_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_8_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_5_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_6_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_7_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_8_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_3_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Day_0_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Day_1_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Day_2_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Day_3_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Night_0_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Night_1_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Night_2_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Night_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_114_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_115_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_116_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_0_en.png new file mode 100644 index 0000000000..b3abe80b38 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5219016a0af4e1e4f7703dcef3400bf030444845dc0ef52f084e69963170bf1e +size 13951 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_1_en.png new file mode 100644 index 0000000000..c7a2988917 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:964aff2fc49b9d71ebec506bc9acc5010071efca01ed95f240a65b2c91f0f3b1 +size 15852 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_2_en.png new file mode 100644 index 0000000000..8be29a4767 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2097486621a733b1662c02849ec71423d00f9c97c679996fafc8f20a4ef27ce +size 43844 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_3_en.png new file mode 100644 index 0000000000..3b29fd1488 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:daff7333af912c8a0d7f009ba10d044aaa7cdfea2dfa3ccff2fd97209bf3278e +size 44225 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_4_en.png new file mode 100644 index 0000000000..b312c338e7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16b86605705dbe40f359cb93eba19efcd5b119b2da38eadf2636bfb26458e838 +size 35844 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_5_en.png new file mode 100644 index 0000000000..85809372c1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:251c739a4d2f4b553d192bdfaf4565a0d2ac45938ce32c53205500b8ef347f19 +size 42540 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_6_en.png new file mode 100644 index 0000000000..3191bb9c6e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5c8bdf8d389cccb4cced29aa854a70860e9b44cbdde2a25ab95ccd5bc5e1674 +size 39222 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_7_en.png new file mode 100644 index 0000000000..774c1e01c3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbf5d7c5320daa0d6b3c6678a6766c7143bc877850257e2c6923b174dfa9b6d4 +size 34565 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_8_en.png new file mode 100644 index 0000000000..38f435b1b0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:298db58a3f80f4194c5710ceeabec0e49b0e80861b931693d58b1a17f4394b4e +size 13873 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_0_en.png new file mode 100644 index 0000000000..1c82d3955f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b982dc465fd47dbce6f88321f80c5390ecc90f7e8ad59a76dd44b6c2b9b80c8a +size 13923 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_1_en.png new file mode 100644 index 0000000000..485fa05bb5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:00b13582a4a8a380cc502ca2bb181c1ee7c9f2e1685e1d2a75817107d5ebea99 +size 15393 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_2_en.png new file mode 100644 index 0000000000..a52fe36b2e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ad6ae058adb3decb4733c74ad40e4efdd33744eea3e9a793e92fc6b4cbc1464 +size 42784 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_3_en.png new file mode 100644 index 0000000000..225298ea51 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad30950d847bd769e19ce67d8944c16145e929c52210cd54b1ebc5966efe89b5 +size 43221 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_4_en.png new file mode 100644 index 0000000000..0e360e7d6b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:09903fc1e3e5adf5b165acbd4cf504c5d570da9ad3ea8a7cc90c17e72e491fe4 +size 34953 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_5_en.png new file mode 100644 index 0000000000..37d6117ae0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e4ca33fcc750bbdea7e25aaed5f9ac7fef377ae44e9015576362af873da99af +size 41658 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_6_en.png new file mode 100644 index 0000000000..ea980ce184 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73f85bf9ec6c6682e293db2c740d206f6d895d79b3f3d30b967ad55aedbb268c +size 37895 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_7_en.png new file mode 100644 index 0000000000..55995f7fd6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49e2727c42d334c539ee14aab396cadd68b1290e6a256ec2f677f7e4801b30e9 +size 32677 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_8_en.png new file mode 100644 index 0000000000..a3e293c5f5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ef494178398bc0f8e7721b7dfde669da4d723aeafa0d4ca6975a58215193416 +size 13848 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 new file mode 100644 index 0000000000..b952647d5d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bad1ef12ad0fe54c78eb91b56bf6f5528a25fd0e79cfa38976535d97f179dae2 +size 15999 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 new file mode 100644 index 0000000000..55266468a5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1ef7cc669d9e180c220aa6e2d7d69a16be61569c98dbb9e81f41e23d6ecaafd +size 20072 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 new file mode 100644 index 0000000000..a3cdce72f3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a8eb29dd4b1667250d9bdf880bbf16fe343ac40704df793b1ba0725cbc036a56 +size 46929 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 new file mode 100644 index 0000000000..9a52e1513f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:48a538ac752a6f0a84d09bb6b81a7c7c8f47f01537d500de28040cb859db8498 +size 45652 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 new file mode 100644 index 0000000000..eb53e9990e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbe6f27a97a23f24221a89362cd745dedba4ce6de25b93e26117a340b6d565ca +size 15811 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 new file mode 100644 index 0000000000..66d341ac37 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f5798743a5ee59f6842297439ce338652225d8aa18aebebd3aa1785f06fa4863 +size 19723 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 new file mode 100644 index 0000000000..7e59d160b0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c95c84150a78322d6b101fe5ee4b9d0c8436ff9792ca87aa1d4394a0f86a345 +size 46364 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 new file mode 100644 index 0000000000..566cd3ac21 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c88b9691b4dc811a5504dca5baa75f46dca6083b161ddf3c2e71050aa449d59 +size 44862 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Day_0_en.png deleted file mode 100644 index 47fc782a13..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4c53e7edfe8b9ea00bb97f9ab6e8289081b5c36aad54a58d4f535fc533338797 -size 15733 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Day_1_en.png deleted file mode 100644 index fe88fafe4f..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Day_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0075dca9fd927dc7df4e987fe5cca8d2a2ca5b268bc46916dcd49724264a3859 -size 19834 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Day_2_en.png deleted file mode 100644 index 80a6ed2561..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Day_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9d784e5d4d4af937a33825f7098d0849fa044d96e4cf21c7244f51432d3f32c7 -size 46725 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Day_3_en.png deleted file mode 100644 index 1c0ff9913e..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Day_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1a1c851207069720dd83cf32da6da80d25a4a4c40cba9884548f9dea09ca6654 -size 45383 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Night_0_en.png deleted file mode 100644 index 1de4e50542..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ea671a40116dfe7a0e4ac97b58cbbc48b3f70fe08b5613c4b21ec13bc6850c3f -size 15589 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Night_1_en.png deleted file mode 100644 index 18a49cfc20..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Night_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8b69241047e1d5787620331adc33f2b78084f47cf02c531a4e41011816590a09 -size 19523 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Night_2_en.png deleted file mode 100644 index bd899f7f08..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Night_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ae27a6a05d85587b066203ec8828db0ec1cec2171e1f3a63c6b0c26e0b71555e -size 46151 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Night_3_en.png deleted file mode 100644 index b3c914f630..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.space.impl_SpaceView_Night_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:067d6b499ce3ec737792a773c2959aea44829cb27a8fecbe1925ec884bc7f53f -size 44650 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_114_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_114_en.png new file mode 100644 index 0000000000..d19cf477f7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_114_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:157bbbc4511b29b531d85f465c473b7ac186af5f697dfd6ffb6da5a02bb0ca02 +size 16960 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_115_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_115_en.png new file mode 100644 index 0000000000..59c1e3c6ec --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_115_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf17a6c4cd39061bd34e616ec10440d47826a6af81033b99c3a1fb1060e95563 +size 16237 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_116_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_116_en.png new file mode 100644 index 0000000000..4e70c3836d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_116_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:992430d8297f30891aeaf61b054660fc4a4d1c125b06f7df4acc860beda79611 +size 18890 From c78aa7cef1af0238a6591a1a13f72f5ddf3deb2e Mon Sep 17 00:00:00 2001 From: ElementBot Date: Fri, 26 Sep 2025 14:01:17 +0000 Subject: [PATCH 11/12] Update screenshots --- ...s.designsystem.components.avatar_Avatar_Avatars_114_en.png | 4 ++-- ...s.designsystem.components.avatar_Avatar_Avatars_115_en.png | 4 ++-- ...s.designsystem.components.avatar_Avatar_Avatars_116_en.png | 4 ++-- ...s.designsystem.components.avatar_Avatar_Avatars_117_en.png | 3 +++ ...s.designsystem.components.avatar_Avatar_Avatars_118_en.png | 3 +++ ...s.designsystem.components.avatar_Avatar_Avatars_119_en.png | 3 +++ 6 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_117_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_118_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_119_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_114_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_114_en.png index 9b1636f3bd..d19cf477f7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_114_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_114_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ab6848052b8bb306bbe8bbd87a62599047f7e530b70a17417b7ba51f2d90ecf5 -size 14278 +oid sha256:157bbbc4511b29b531d85f465c473b7ac186af5f697dfd6ffb6da5a02bb0ca02 +size 16960 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_115_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_115_en.png index 73fb759161..59c1e3c6ec 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_115_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_115_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ddb1b57c91c8d3adec9b79307d52a36ac4516e7020f2c3eafc9343fd9d9e368 -size 13536 +oid sha256:cf17a6c4cd39061bd34e616ec10440d47826a6af81033b99c3a1fb1060e95563 +size 16237 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_116_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_116_en.png index 73fbe1fcdf..4e70c3836d 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_116_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_116_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f80fd46fb18b92e462e079647b42f6cb8cd101f900120d4927d3101defe2dd36 -size 16213 +oid sha256:992430d8297f30891aeaf61b054660fc4a4d1c125b06f7df4acc860beda79611 +size 18890 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_117_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_117_en.png new file mode 100644 index 0000000000..9b1636f3bd --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_117_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab6848052b8bb306bbe8bbd87a62599047f7e530b70a17417b7ba51f2d90ecf5 +size 14278 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_118_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_118_en.png new file mode 100644 index 0000000000..73fb759161 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_118_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ddb1b57c91c8d3adec9b79307d52a36ac4516e7020f2c3eafc9343fd9d9e368 +size 13536 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_119_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_119_en.png new file mode 100644 index 0000000000..73fbe1fcdf --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_119_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f80fd46fb18b92e462e079647b42f6cb8cd101f900120d4927d3101defe2dd36 +size 16213 From ef369225942a7397715be690ddf2d21d6bdf705a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 26 Sep 2025 16:28:13 +0200 Subject: [PATCH 12/12] Address review comments. --- .../space/impl/leave/LeaveSpacePresenter.kt | 14 +++++++----- .../space/impl/leave/LeaveSpaceState.kt | 22 +++++++------------ .../space/impl/leave/LeaveSpaceView.kt | 8 +++---- .../space/impl/leave/LeaveSpaceStateTest.kt | 18 +++++++-------- 4 files changed, 30 insertions(+), 32 deletions(-) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenter.kt index 810b73cffd..c7be10d956 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenter.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenter.kt @@ -27,7 +27,10 @@ import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.spaces.SpaceRoom import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toPersistentList +import kotlinx.collections.immutable.toPersistentSet import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlin.jvm.optionals.getOrNull @@ -52,13 +55,13 @@ class LeaveSpacePresenter( mutableStateOf>(AsyncAction.Uninitialized) } val selectedRoomIds = remember { - mutableStateOf>(emptySet()) + mutableStateOf>(persistentSetOf()) } val joinedSpaceRooms by produceState(emptyList()) { - // TODO Get the joined room from the SDK, should also have the + // TODO Get the joined room from the SDK, should also have the isLastAdmin boolean val rooms = emptyList() // By default select all rooms - selectedRoomIds.value = rooms.map { it.roomId }.toSet() + selectedRoomIds.value = rooms.map { it.roomId }.toPersistentSet() value = rooms } val selectableSpaceRooms by produceState>>( @@ -81,14 +84,14 @@ class LeaveSpacePresenter( fun handleEvents(event: LeaveSpaceEvents) { when (event) { LeaveSpaceEvents.DeselectAllRooms -> { - selectedRoomIds.value = emptySet() + selectedRoomIds.value = persistentSetOf() } LeaveSpaceEvents.SelectAllRooms -> { selectedRoomIds.value = selectableSpaceRooms.dataOrNull() .orEmpty() .filter { it.isLastAdmin.not() } .map { it.spaceRoom.roomId } - .toSet() + .toPersistentSet() } is LeaveSpaceEvents.ToggleRoomSelection -> { val currentSet = selectedRoomIds.value @@ -97,6 +100,7 @@ class LeaveSpacePresenter( } else { currentSet + event.roomId } + .toPersistentSet() } LeaveSpaceEvents.LeaveSpace -> coroutineScope.leaveSpace( leaveSpaceAction = leaveSpaceAction, diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceState.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceState.kt index 6aa29925c2..f63eef2333 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceState.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceState.kt @@ -18,33 +18,27 @@ data class LeaveSpaceState( val eventSink: (LeaveSpaceEvents) -> Unit, ) { private val rooms = selectableSpaceRooms.dataOrNull().orEmpty() + private val partition = rooms.partition { it.isLastAdmin } + private val lastAdminRooms = partition.first + private val selectableRooms = partition.second /** * True if we should show the quick action to select/deselect all rooms. */ - val showQuickAction = rooms - .any { !it.isLastAdmin } + val showQuickAction = selectableRooms.isNotEmpty() /** - * True if there are rooms and they are all selected. + * True if there all the selectable rooms are selected. */ - val areAllSelected = rooms - .filter { !it.isLastAdmin } - .let { rooms -> - rooms.isNotEmpty() && rooms.all { it.isSelected } - } + val areAllSelected = selectableRooms.all { it.isSelected } /** * True if there are rooms but the user is the last admin in all of them. */ - val hasOnlyLastAdminRoom = rooms - .let { rooms -> - rooms.isNotEmpty() && rooms.all { it.isLastAdmin } - } + val hasOnlyLastAdminRoom = lastAdminRooms.isNotEmpty() && selectableRooms.isEmpty() /** * Number of selected rooms. */ - val numberOfSelectRooms = rooms - .count { it.isSelected } + val selectedRoomsCount = selectableRooms.count { it.isSelected } } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceView.kt index 5a894e0e7a..fbfb659f53 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceView.kt @@ -118,7 +118,7 @@ fun LeaveSpaceView( } LeaveSpaceButtons( showLeaveButton = state.selectableSpaceRooms is AsyncData.Success, - nbOfSelectedRooms = state.numberOfSelectRooms, + selectedRoomsCount = state.selectedRoomsCount, onLeaveSpace = { state.eventSink(LeaveSpaceEvents.LeaveSpace) }, @@ -198,7 +198,7 @@ private fun LeaveSpaceHeader( @Composable private fun LeaveSpaceButtons( showLeaveButton: Boolean, - nbOfSelectedRooms: Int, + selectedRoomsCount: Int, onLeaveSpace: () -> Unit, onCancel: () -> Unit, ) { @@ -206,8 +206,8 @@ private fun LeaveSpaceButtons( modifier = Modifier.padding(top = 16.dp) ) { if (showLeaveButton) { - val text = if (nbOfSelectedRooms > 0) { - pluralStringResource(R.plurals.screen_leave_space_submit, nbOfSelectedRooms, nbOfSelectedRooms) + val text = if (selectedRoomsCount > 0) { + pluralStringResource(R.plurals.screen_leave_space_submit, selectedRoomsCount, selectedRoomsCount) } else { stringResource(CommonStrings.action_leave_space) } diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateTest.kt index 060b5043a3..eaf3f1a783 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateTest.kt @@ -20,9 +20,9 @@ class LeaveSpaceStateTest { selectableSpaceRooms = AsyncData.Loading() ) assertThat(sut.showQuickAction).isFalse() - assertThat(sut.areAllSelected).isFalse() + assertThat(sut.areAllSelected).isTrue() assertThat(sut.hasOnlyLastAdminRoom).isFalse() - assertThat(sut.numberOfSelectRooms).isEqualTo(0) + assertThat(sut.selectedRoomsCount).isEqualTo(0) } @Test @@ -33,9 +33,9 @@ class LeaveSpaceStateTest { ) ) assertThat(sut.showQuickAction).isFalse() - assertThat(sut.areAllSelected).isFalse() + assertThat(sut.areAllSelected).isTrue() assertThat(sut.hasOnlyLastAdminRoom).isFalse() - assertThat(sut.numberOfSelectRooms).isEqualTo(0) + assertThat(sut.selectedRoomsCount).isEqualTo(0) } @Test @@ -51,7 +51,7 @@ class LeaveSpaceStateTest { assertThat(sut.showQuickAction).isTrue() assertThat(sut.areAllSelected).isFalse() assertThat(sut.hasOnlyLastAdminRoom).isFalse() - assertThat(sut.numberOfSelectRooms).isEqualTo(1) + assertThat(sut.selectedRoomsCount).isEqualTo(1) } @Test @@ -67,7 +67,7 @@ class LeaveSpaceStateTest { assertThat(sut.showQuickAction).isTrue() assertThat(sut.areAllSelected).isTrue() assertThat(sut.hasOnlyLastAdminRoom).isFalse() - assertThat(sut.numberOfSelectRooms).isEqualTo(2) + assertThat(sut.selectedRoomsCount).isEqualTo(2) } @Test @@ -84,7 +84,7 @@ class LeaveSpaceStateTest { assertThat(sut.showQuickAction).isTrue() assertThat(sut.areAllSelected).isTrue() assertThat(sut.hasOnlyLastAdminRoom).isFalse() - assertThat(sut.numberOfSelectRooms).isEqualTo(2) + assertThat(sut.selectedRoomsCount).isEqualTo(2) } @Test @@ -98,8 +98,8 @@ class LeaveSpaceStateTest { ) ) assertThat(sut.showQuickAction).isFalse() - assertThat(sut.areAllSelected).isFalse() + assertThat(sut.areAllSelected).isTrue() assertThat(sut.hasOnlyLastAdminRoom).isTrue() - assertThat(sut.numberOfSelectRooms).isEqualTo(0) + assertThat(sut.selectedRoomsCount).isEqualTo(0) } }