From 9fcbd36f42c4752de89eb83cb0ae93ff3ea698a5 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 3 Dec 2025 17:43:13 +0100 Subject: [PATCH 01/22] feature(security&privacy): create ManageAuthorizedSpace classes --- .../ManageAuthorizedSpacesEvent.kt | 16 ++++ .../ManageAuthorizedSpacesNode.kt | 42 ++++++++++ .../ManageAuthorizedSpacesPresenter.kt | 48 +++++++++++ .../ManageAuthorizedSpacesState.kt | 13 +++ .../ManageAuthorizedSpacesStateProvider.kt | 19 +++++ .../ManageAuthorizedSpacesView.kt | 80 +++++++++++++++++++ 6 files changed, 218 insertions(+) create mode 100644 features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesEvent.kt create mode 100644 features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt create mode 100644 features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt create mode 100644 features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt create mode 100644 features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt create mode 100644 features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesEvent.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesEvent.kt new file mode 100644 index 0000000000..0515878ade --- /dev/null +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesEvent.kt @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * 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.securityandprivacy.impl.manageauthorizedspaces + +import io.element.android.libraries.matrix.api.core.RoomId + +sealed interface ManageAuthorizedSpacesEvent { + data object Done : ManageAuthorizedSpacesEvent + data class ToggleSpace(val roomId: RoomId) : ManageAuthorizedSpacesEvent +} diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt new file mode 100644 index 0000000000..3b52c2ac3f --- /dev/null +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * 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.securityandprivacy.impl.manageauthorizedspaces + +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.core.plugin.plugins +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode +import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNavigator +import io.element.android.libraries.di.RoomScope + +@ContributesNode(RoomScope::class) +@AssistedInject +class ManageAuthorizedSpacesNode( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: EditRoomAddressPresenter.Factory, +) : Node(buildContext, plugins = plugins) { + private val navigator = plugins().first() + private val presenter = presenterFactory.create(navigator) + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + ManageAuthorizedSpacesView( + state = state, + onBackClick = ::navigateUp, + modifier = modifier + ) + } +} diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt new file mode 100644 index 0000000000..758ea030d2 --- /dev/null +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * 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.securityandprivacy.impl.manageauthorizedspaces + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject +import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNavigator +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.room.JoinedRoom + +@AssistedInject +class EditRoomAddressPresenter( + @Assisted private val navigator: SecurityAndPrivacyNavigator, + private val client: MatrixClient, + private val room: JoinedRoom, +) : Presenter { + @AssistedFactory + interface Factory { + fun create(navigator: SecurityAndPrivacyNavigator): EditRoomAddressPresenter + } + + @Composable + override fun present(): ManageAuthorizedSpacesState { + val roomInfo by room.roomInfoFlow.collectAsState() + + fun handleEvent(event: ManageAuthorizedSpacesEvent) { + when (event) { + ManageAuthorizedSpacesEvent.Done -> TODO() + is ManageAuthorizedSpacesEvent.ToggleSpace -> TODO() + } + } + + return ManageAuthorizedSpacesState( + eventSink = ::handleEvent, + ) + } +} diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt new file mode 100644 index 0000000000..f0936e81b7 --- /dev/null +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * 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.securityandprivacy.impl.manageauthorizedspaces + +data class ManageAuthorizedSpacesState( + val eventSink: (ManageAuthorizedSpacesEvent) -> Unit +) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt new file mode 100644 index 0000000000..91ff1f9b22 --- /dev/null +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * 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.securityandprivacy.impl.manageauthorizedspaces + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity + +open class ManageAuthorizedSpacesStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf() +} + diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt new file mode 100644 index 0000000000..632fb7ffcc --- /dev/null +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * 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.securityandprivacy.impl.manageauthorizedspaces + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +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.Scaffold +import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun ManageAuthorizedSpacesView( + state: ManageAuthorizedSpacesState, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + topBar = { + ManageAuthorizedSpacesTopBar( + onBackClick = onBackClick, + onDoneClick = { + state.eventSink(ManageAuthorizedSpacesEvent.Done) + }, + ) + } + ) { padding -> + LazyColumn( + modifier = Modifier.padding(padding) + ) { + + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ManageAuthorizedSpacesTopBar( + onBackClick: () -> Unit, + onDoneClick: () -> Unit, + modifier: Modifier = Modifier, +) { + TopAppBar( + modifier = modifier, + titleStr = stringResource(CommonStrings.screen_manage_authorized_spaces_title), + navigationIcon = { BackButton(onClick = onBackClick) }, + actions = { + TextButton( + text = stringResource(CommonStrings.action_done), + onClick = onDoneClick, + ) + } + ) +} + +@PreviewsDayNight +@Composable +internal fun ManageAuthorizedSpacesViewPreview( + @PreviewParameter(ManageAuthorizedSpacesStateProvider::class) state: ManageAuthorizedSpacesState +) = ElementPreview { + ManageAuthorizedSpacesView( + state = state, + onBackClick = {}, + ) +} From d868bf64bf074319cf154a297f4275b443a87515 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 17 Dec 2025 13:58:54 +0100 Subject: [PATCH 02/22] localazy : sync strings # Conflicts: # features/roomdetails/impl/src/main/res/values/localazy.xml # features/securityandprivacy/impl/src/main/res/values/localazy.xml # libraries/ui-strings/src/main/res/values/localazy.xml --- .../src/main/res/values/localazy.xml | 31 ++++++++++++++++--- tools/localazy/config.json | 3 +- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index df47b71577..9afde6fefc 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -435,11 +435,32 @@ Are you sure you want to continue?" "Options" "Remove %1$s" "Settings" - "Spaces where members can join the room without an invitation." - "Manage spaces" - "(Unknown space)" - "Other spaces you’re not a member of" - "Your spaces" + "Scan the QR code" + "Open %1$s on a laptop or desktop computer" + "Scan the QR code with this device" + "Ready to scan" + "Open %1$s on a desktop computer to get the QR code" + "The numbers don’t match" + "Enter 2-digit code" + "This will verify that the connection to your other device is secure." + "Enter the number shown on your other device" + "Your account provider does not support %1$s." + "%1$s not supported" + "Your account provider doesn’t support signing into a new device with a QR code." + "QR code not supported" + "The sign in was cancelled on the other device." + "Sign in request cancelled" + "Sign in expired. Please try again." + "The sign in was not completed in time" + "Open %1$s on the other device" + "Select %1$s" + "“Sign in with QR code”" + "Scan the QR code shown here with the other device" + "Open %1$s on the other device" + "Desktop computer" + "Loading QR code…" + "Mobile device" + "What type of device do you want to link?" "Failed selecting media, please try again." "Press on a message and choose “%1$s” to include here." "Pin important messages so that they can be easily discovered" diff --git a/tools/localazy/config.json b/tools/localazy/config.json index a8572fb660..3894befcb0 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -403,7 +403,8 @@ "name" : ":features:securityandprivacy:impl", "includeRegex" : [ "screen\\.edit_room_address\\..*", - "screen\\.security_and_privacy\\..*" + "screen\\.security_and_privacy\\..*", + "screen\\.manage_authorized_spaces\\..*" ] } ] From 7b8950a51bc968f96ad7c0cfbd9700a1a994a114 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 18 Dec 2025 10:07:57 +0100 Subject: [PATCH 03/22] feature(security&privacy): start ManageAuthorizedSpacesView --- .../ManageAuthorizedSpacesPresenter.kt | 6 +- .../ManageAuthorizedSpacesState.kt | 8 ++ .../ManageAuthorizedSpacesStateProvider.kt | 48 +++++++- .../ManageAuthorizedSpacesView.kt | 115 +++++++++++++++++- 4 files changed, 171 insertions(+), 6 deletions(-) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt index 758ea030d2..527393c9d3 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt @@ -18,6 +18,7 @@ import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNav import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.room.JoinedRoom +import kotlinx.collections.immutable.persistentListOf @AssistedInject class EditRoomAddressPresenter( @@ -33,7 +34,6 @@ class EditRoomAddressPresenter( @Composable override fun present(): ManageAuthorizedSpacesState { val roomInfo by room.roomInfoFlow.collectAsState() - fun handleEvent(event: ManageAuthorizedSpacesEvent) { when (event) { ManageAuthorizedSpacesEvent.Done -> TODO() @@ -42,6 +42,10 @@ class EditRoomAddressPresenter( } return ManageAuthorizedSpacesState( + joinedSpaces = persistentListOf(), + unknownSpaceIds = persistentListOf(), + currentSelection = persistentListOf(), + initialSelection = persistentListOf(), eventSink = ::handleEvent, ) } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt index f0936e81b7..688ef6e6e9 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt @@ -8,6 +8,14 @@ package io.element.android.features.securityandprivacy.impl.manageauthorizedspaces +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.spaces.SpaceRoom +import kotlinx.collections.immutable.ImmutableList + data class ManageAuthorizedSpacesState( + val joinedSpaces: ImmutableList, + val unknownSpaceIds: ImmutableList, + val currentSelection: ImmutableList, + val initialSelection: ImmutableList, val eventSink: (ManageAuthorizedSpacesEvent) -> Unit ) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt index 91ff1f9b22..d91e16f3e6 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt @@ -9,11 +9,53 @@ package io.element.android.features.securityandprivacy.impl.manageauthorizedspaces import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity +import io.element.android.libraries.matrix.api.core.RoomAlias +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.spaces.SpaceRoom +import io.element.android.libraries.previewutils.room.aSpaceRoom +import kotlinx.collections.immutable.toImmutableList open class ManageAuthorizedSpacesStateProvider : PreviewParameterProvider { override val values: Sequence - get() = sequenceOf() + get() = sequenceOf( + aManageAuthorizedSpacesState(), + aManageAuthorizedSpacesState( + unknownSpaceIds = listOf(aRoomId(99)) + ), + aManageAuthorizedSpacesState( + currentSelection = listOf(aRoomId(1), aRoomId(3)), + initialSelection = listOf(aRoomId(1)), + ), + ) } +private fun aRoomId(index: Int) = RoomId("!roomId$index:matrix.org") + +private fun aSpaceRoomList(count: Int): List { + return (1..count).map { index -> + aSpaceRoom( + roomId = aRoomId(index), + displayName = "Space $index", + canonicalAlias = if (index % 2 == 0) { + RoomAlias("#space$index:matrix.org") + } else { + null + } + ) + } +} + +private fun aManageAuthorizedSpacesState( + joinedSpaces: List = aSpaceRoomList(5), + unknownSpaceIds: List = emptyList(), + currentSelection: List = emptyList(), + initialSelection: List = emptyList(), + eventSink: (ManageAuthorizedSpacesEvent) -> Unit = {}, +) = ManageAuthorizedSpacesState( + joinedSpaces = joinedSpaces.toImmutableList(), + unknownSpaceIds = unknownSpaceIds.toImmutableList(), + currentSelection = currentSelection.toImmutableList(), + initialSelection = initialSelection.toImmutableList(), + eventSink = eventSink, +) + diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt index 632fb7ffcc..ed49e048aa 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt @@ -10,17 +10,34 @@ package io.element.android.features.securityandprivacy.impl.manageauthorizedspac import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.securityandprivacy.impl.R +import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule +import io.element.android.libraries.designsystem.components.BigIcon +import io.element.android.libraries.designsystem.components.avatar.Avatar +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.ListItem +import io.element.android.libraries.designsystem.theme.components.ListSectionHeader 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.spaces.SpaceRoom +import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.ui.strings.CommonStrings @Composable @@ -43,11 +60,105 @@ fun ManageAuthorizedSpacesView( LazyColumn( modifier = Modifier.padding(padding) ) { - + headerItem() + item { + ListSectionHeader( + title = stringResource(R.string.screen_manage_authorized_spaces_your_spaces_section_title), + hasDivider = false, + ) + } + items(items = state.joinedSpaces) { space -> + CheckableSpaceListItem( + headlineText = space.displayName, + supportingText = space.canonicalAlias?.value, + avatarData = space.getAvatarData(AvatarSize.SpaceMember), + checked = state.currentSelection.contains(space.roomId), + onCheckedChange = { _ -> + state.eventSink( + ManageAuthorizedSpacesEvent.ToggleSpace(space.roomId) + ) + } + ) + } + if(state.unknownSpaceIds.isNotEmpty()){ + item { + ListSectionHeader( + title = stringResource(R.string.screen_manage_authorized_spaces_unknown_spaces_section_title), + hasDivider = false, + ) + } + items(items = state.unknownSpaceIds) { + CheckableSpaceListItem( + headlineText = stringResource(R.string.screen_manage_authorized_spaces_unknown_space), + supportingText = it.value, + avatarData = null, + checked = state.currentSelection.contains(it), + onCheckedChange = { _ -> + state.eventSink( + ManageAuthorizedSpacesEvent.ToggleSpace(it) + ) + } + ) + } + } } } } +private fun LazyListScope.headerItem() { + item(key = "header", contentType = "header") { + IconTitleSubtitleMolecule( + modifier = Modifier.padding( + vertical = 16.dp, + horizontal = 24.dp + ), + title = stringResource(R.string.screen_manage_authorized_spaces_header), + subTitle = null, + iconStyle = BigIcon.Style.Default( + vectorIcon = CompoundIcons.SpaceSolid(), + ) + ) + } +} + +@Composable +private fun CheckableSpaceListItem( + headlineText: String, + supportingText: String?, + avatarData: AvatarData?, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, +) { + ListItem( + headlineContent = { + Text(text = headlineText) + }, + supportingContent = supportingText?.let { + @Composable { + Text(text = supportingText) + } + }, + leadingContent = avatarData?.let{ + ListItemContent.Custom { + Avatar( + avatarData = avatarData, + avatarType = AvatarType.Space(), + ) + } + }, + trailingContent = ListItemContent.Checkbox( + checked = checked, + enabled = enabled, + ), + enabled = enabled, + onClick = { onCheckedChange(!checked) }, + modifier = modifier, + ) +} + + @OptIn(ExperimentalMaterial3Api::class) @Composable private fun ManageAuthorizedSpacesTopBar( @@ -57,7 +168,7 @@ private fun ManageAuthorizedSpacesTopBar( ) { TopAppBar( modifier = modifier, - titleStr = stringResource(CommonStrings.screen_manage_authorized_spaces_title), + titleStr = stringResource(R.string.screen_manage_authorized_spaces_title), navigationIcon = { BackButton(onClick = onBackClick) }, actions = { TextButton( From 421f12f396a17da5f1ccb13d5c6f750526c514ad Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 23 Dec 2025 15:41:16 +0100 Subject: [PATCH 04/22] localazy: sync strings --- .../src/main/res/values-et/translations.xml | 5 ++++ .../src/main/res/values-fr/translations.xml | 5 ++++ .../src/main/res/values-hr/translations.xml | 5 ++++ .../main/res/values-pt-rBR/translations.xml | 5 ++++ .../src/main/res/values-ro/translations.xml | 5 ++++ .../src/main/res/values-sk/translations.xml | 5 ++++ .../impl/src/main/res/values/localazy.xml | 5 ++++ .../src/main/res/values-et/translations.xml | 5 ---- .../src/main/res/values-fr/translations.xml | 5 ---- .../src/main/res/values-hr/translations.xml | 5 ---- .../main/res/values-pt-rBR/translations.xml | 5 ---- .../src/main/res/values-ro/translations.xml | 5 ---- .../src/main/res/values-sk/translations.xml | 5 ---- .../src/main/res/values/localazy.xml | 26 ------------------- 14 files changed, 35 insertions(+), 56 deletions(-) diff --git a/features/securityandprivacy/impl/src/main/res/values-et/translations.xml b/features/securityandprivacy/impl/src/main/res/values-et/translations.xml index 8d74c35733..06d095cf4f 100644 --- a/features/securityandprivacy/impl/src/main/res/values-et/translations.xml +++ b/features/securityandprivacy/impl/src/main/res/values-et/translations.xml @@ -2,6 +2,11 @@ "Selleks, et jututuba oleks nähtav jututubade avalikus kataloogis, vajab ta aadressi." "Muuda aadressi" + "Kogukonnad, milles on võimalik jututoaga liituda ilma kutseta." + "Halda kogukondi" + "(Tundmatu kogukond)" + "Muud kogukonnad, mille liige sa ei ole" + "Sinu kogukonnad" "Lisa aadress" "Liituda saavad kõik volitatud kogukondade liikmed, kuid kõik teised peavad küsima võimalust ligipääsuks." "Kõik võivad paluda jututoaga liitumist." diff --git a/features/securityandprivacy/impl/src/main/res/values-fr/translations.xml b/features/securityandprivacy/impl/src/main/res/values-fr/translations.xml index 5426d71706..afa85522f1 100644 --- a/features/securityandprivacy/impl/src/main/res/values-fr/translations.xml +++ b/features/securityandprivacy/impl/src/main/res/values-fr/translations.xml @@ -2,6 +2,11 @@ "Vous aurez besoin d’une adresse pour le rendre visible dans l’annuaire public." "Modifier l’adresse" + "Espaces où les membres peuvent rejoindre le salon sans invitation." + "Gérer les espaces" + "(Espace inconnu)" + "Autres espaces dont vous n’êtes pas membre" + "Vos espaces" "Ajouter une adresse" "Tout le monde doit demander un accès." "Demander à rejoindre" diff --git a/features/securityandprivacy/impl/src/main/res/values-hr/translations.xml b/features/securityandprivacy/impl/src/main/res/values-hr/translations.xml index 401fe806f8..b8905bef30 100644 --- a/features/securityandprivacy/impl/src/main/res/values-hr/translations.xml +++ b/features/securityandprivacy/impl/src/main/res/values-hr/translations.xml @@ -2,6 +2,11 @@ "Trebat će vam adresa kako bi bila vidljiva u javnom direktoriju." "Uredi adresu" + "Prostori u kojima se članovi mogu pridružiti sobi bez pozivnice." + "Upravljaj prostorima" + "(nepoznati prostor)" + "Drugi prostori čiji niste član" + "Vaši prostori" "Dodaj adresu" "Svatko tko se nalazi u ovlaštenim prostorima može se pridružiti, ali svi ostali moraju zatražiti pristup." "Svi moraju zatražiti pristup." diff --git a/features/securityandprivacy/impl/src/main/res/values-pt-rBR/translations.xml b/features/securityandprivacy/impl/src/main/res/values-pt-rBR/translations.xml index 658067be2d..dfe1aee558 100644 --- a/features/securityandprivacy/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/securityandprivacy/impl/src/main/res/values-pt-rBR/translations.xml @@ -2,6 +2,11 @@ "Você precisará de um endereço para torná-la visível no diretório." "Editar endereço" + "Os espaços dos quais os membros podem entrar na sala sem um convite." + "Gerenciar espaços" + "(Espaço desconhecido)" + "Outros espaços dos quais você não é um membro" + "Seus espaços" "Adicionar endereço" "Qualquer um nos espaços autorizados podem entrar, mas todos os outros devem pedir acesso." "Qualquer um pode pedir acesso, mas um administrador terá que aceitar o pedido." diff --git a/features/securityandprivacy/impl/src/main/res/values-ro/translations.xml b/features/securityandprivacy/impl/src/main/res/values-ro/translations.xml index 701cb72370..f3c62f2497 100644 --- a/features/securityandprivacy/impl/src/main/res/values-ro/translations.xml +++ b/features/securityandprivacy/impl/src/main/res/values-ro/translations.xml @@ -2,6 +2,11 @@ "Veți avea nevoie de o adresă pentru a o face vizibilă în directorul public." "Editați adresa" + "Spațile din care membrii se pot alătura camerei fără invitație." + "Gestionați spațiile" + "(Spațiu necunoscut)" + "Alte spații din care nu faceți parte" + "Spațiile dumneavoastră" "Adăugați o adresă" "Oricine se află în spațiile autorizate se poate alătura, dar toți ceilalți trebuie să solicite accesul." "Toată lumea trebuie să solicite acces." diff --git a/features/securityandprivacy/impl/src/main/res/values-sk/translations.xml b/features/securityandprivacy/impl/src/main/res/values-sk/translations.xml index ee346dd5d0..3853fc5f8a 100644 --- a/features/securityandprivacy/impl/src/main/res/values-sk/translations.xml +++ b/features/securityandprivacy/impl/src/main/res/values-sk/translations.xml @@ -2,6 +2,11 @@ "Budete potrebovať adresu, aby sa zobrazovala vo verejnom adresári." "Upraviť adresu" + "Priestory, kde sa členovia môžu pripojiť k miestnosti bez pozvania." + "Spravovať priestory" + "(Neznámy priestor)" + "Iné priestory, ktorých nie ste členom" + "Vaše priestory" "Pridať adresu" "Všetci musia požiadať o prístup." "Požiadať o pripojenie" diff --git a/features/securityandprivacy/impl/src/main/res/values/localazy.xml b/features/securityandprivacy/impl/src/main/res/values/localazy.xml index acf4b04e71..902a89d7bd 100644 --- a/features/securityandprivacy/impl/src/main/res/values/localazy.xml +++ b/features/securityandprivacy/impl/src/main/res/values/localazy.xml @@ -2,6 +2,11 @@ "You’ll need an address in order to make it visible in the public directory." "Edit address" + "Spaces where members can join the room without an invitation." + "Manage spaces" + "(Unknown space)" + "Other spaces you’re not a member of" + "Your spaces" "Add address" "Anyone in authorised spaces can join, but everyone else must request access." "Everyone must request access." 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 67d323e77b..b2f08ecb65 100644 --- a/libraries/ui-strings/src/main/res/values-et/translations.xml +++ b/libraries/ui-strings/src/main/res/values-et/translations.xml @@ -435,11 +435,6 @@ Kas sa oled kindel, et soovid jätkata?" "Valikud" "Kustuta: %1$s" "Seadistused" - "Kogukonnad, milles on võimalik jututoaga liituda ilma kutseta." - "Halda kogukondi" - "(Tundmatu kogukond)" - "Muud kogukonnad, mille liige sa ei ole" - "Sinu kogukonnad" "Meediafaili valimine ei õnnestunud. Palun proovi uuesti." "Siia lisamiseks vajuta sõnumil ja vali „%1$s“." "Et olulisi sõnumeid oleks lihtsam leida, tõsta nad esile" diff --git a/libraries/ui-strings/src/main/res/values-fr/translations.xml b/libraries/ui-strings/src/main/res/values-fr/translations.xml index 9392933cf8..4acbb6be48 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -431,11 +431,6 @@ Raison : %1$s." "Options" "Supprimer %1$s" "Paramètres" - "Espaces où les membres peuvent rejoindre le salon sans invitation." - "Gérer les espaces" - "(Espace inconnu)" - "Autres espaces dont vous n’êtes pas membre" - "Vos espaces" "Échec de la sélection du média, veuillez réessayer." "Cliquez (clic long) sur un message et choisissez « %1$s » pour qu‘il apparaisse ici." "Épinglez les messages importants pour leur donner plus de visibilité" diff --git a/libraries/ui-strings/src/main/res/values-hr/translations.xml b/libraries/ui-strings/src/main/res/values-hr/translations.xml index 139e68230f..4cbeecf7a8 100644 --- a/libraries/ui-strings/src/main/res/values-hr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-hr/translations.xml @@ -442,11 +442,6 @@ Jeste li sigurni da želite nastaviti?" "Mogućnosti" "Ukloni %1$s" "Postavke" - "Prostori u kojima se članovi mogu pridružiti sobi bez pozivnice." - "Upravljaj prostorima" - "(nepoznati prostor)" - "Drugi prostori čiji niste član" - "Vaši prostori" "Odabir medija nije uspio, pokušajte ponovno." "Pritisnite poruku i odaberite “%1$s” kako biste uključili ovdje." "Prikvačite važne poruke kako bi ih se lakše moglo pronaći" diff --git a/libraries/ui-strings/src/main/res/values-pt-rBR/translations.xml b/libraries/ui-strings/src/main/res/values-pt-rBR/translations.xml index 3863a24fd8..f588e6bdca 100644 --- a/libraries/ui-strings/src/main/res/values-pt-rBR/translations.xml +++ b/libraries/ui-strings/src/main/res/values-pt-rBR/translations.xml @@ -434,11 +434,6 @@ Você tem certeza de que deseja continuar?" "Opções" "Remover %1$s" "Configurações" - "Os espaços dos quais os membros podem entrar na sala sem um convite." - "Gerenciar espaços" - "(Espaço desconhecido)" - "Outros espaços dos quais você não é um membro" - "Seus espaços" "Falha ao selecionar a mídia, tente novamente." "Pressione em uma mensagem e escolha \"%1$s\" para incluir aqui." "Fixe mensagens importantes para que elas possam ser facilmente descobertas" diff --git a/libraries/ui-strings/src/main/res/values-ro/translations.xml b/libraries/ui-strings/src/main/res/values-ro/translations.xml index 9e56d09b16..69f2aba019 100644 --- a/libraries/ui-strings/src/main/res/values-ro/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ro/translations.xml @@ -442,11 +442,6 @@ Sunteți sigur că doriți să continuați?" "Opțiuni" "Ștergeți %1$s" "Setări" - "Spațile din care membrii se pot alătura camerei fără invitație." - "Gestionați spațiile" - "(Spațiu necunoscut)" - "Alte spații din care nu faceți parte" - "Spațiile dumneavoastră" "Selectarea fișierelor media a eșuat, încercați din nou." "Apăsați pe un mesaj și alegeți \"%1$s\" pentru a-l include aici." "Fixați mesajele importante, astfel încât să poată fi descoperite cu ușurință" diff --git a/libraries/ui-strings/src/main/res/values-sk/translations.xml b/libraries/ui-strings/src/main/res/values-sk/translations.xml index 0928010918..3c8f4f16ef 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -439,11 +439,6 @@ Naozaj chcete pokračovať?" "Možnosti" "Odstrániť %1$s" "Nastavenia" - "Priestory, kde sa členovia môžu pripojiť k miestnosti bez pozvania." - "Spravovať priestory" - "(Neznámy priestor)" - "Iné priestory, ktorých nie ste členom" - "Vaše priestory" "Nepodarilo sa vybrať médium, skúste to prosím znova." "Stlačte správu a vyberte možnosť „%1$s“, ktorú chcete zahrnúť sem." "Pripnite dôležité správy, aby sa dali ľahko nájsť" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 9afde6fefc..3a108277ba 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -435,32 +435,6 @@ Are you sure you want to continue?" "Options" "Remove %1$s" "Settings" - "Scan the QR code" - "Open %1$s on a laptop or desktop computer" - "Scan the QR code with this device" - "Ready to scan" - "Open %1$s on a desktop computer to get the QR code" - "The numbers don’t match" - "Enter 2-digit code" - "This will verify that the connection to your other device is secure." - "Enter the number shown on your other device" - "Your account provider does not support %1$s." - "%1$s not supported" - "Your account provider doesn’t support signing into a new device with a QR code." - "QR code not supported" - "The sign in was cancelled on the other device." - "Sign in request cancelled" - "Sign in expired. Please try again." - "The sign in was not completed in time" - "Open %1$s on the other device" - "Select %1$s" - "“Sign in with QR code”" - "Scan the QR code shown here with the other device" - "Open %1$s on the other device" - "Desktop computer" - "Loading QR code…" - "Mobile device" - "What type of device do you want to link?" "Failed selecting media, please try again." "Press on a message and choose “%1$s” to include here." "Pin important messages so that they can be easily discovered" From b59e36aabd4f71e563814a85479c86237263265b Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 23 Dec 2025 20:49:41 +0100 Subject: [PATCH 05/22] space service : expose methods from sdk --- .../matrix/api/spaces/SpaceService.kt | 4 ++++ .../matrix/impl/spaces/RustSpaceService.kt | 23 +++++++++++++++---- .../matrix/test/spaces/FakeSpaceService.kt | 10 ++++++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt index 6f5ba674ec..8f005b0225 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt @@ -15,6 +15,10 @@ interface SpaceService { val spaceRoomsFlow: SharedFlow> suspend fun joinedSpaces(): Result> + suspend fun joinedParents(spaceId: RoomId): Result> + + suspend fun getSpaceRoom(spaceId: RoomId): SpaceRoom? + fun spaceRoomList(id: RoomId): SpaceRoomList fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt index ba816c11c7..fad698c731 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt @@ -51,13 +51,28 @@ class RustSpaceService( override suspend fun joinedSpaces(): Result> = withContext(sessionDispatcher) { runCatchingExceptions { - innerSpaceService.topLevelJoinedSpaces() - .map { - it.let(spaceRoomMapper::map) - } + innerSpaceService + .topLevelJoinedSpaces() + .map(spaceRoomMapper::map) } } + override suspend fun joinedParents(spaceId: RoomId): Result> = withContext(sessionDispatcher) { + runCatchingExceptions { + innerSpaceService + .joinedParentsOfChild(spaceId.value) + .map(spaceRoomMapper::map) + } + } + + override suspend fun getSpaceRoom(spaceId: RoomId): SpaceRoom? = withContext(sessionDispatcher) { + runCatchingExceptions { + innerSpaceService.getSpaceRoom(spaceId.value)?.let { spaceRoom -> + spaceRoomMapper.map(spaceRoom) + } + }.getOrNull() + } + override fun spaceRoomList(id: RoomId): SpaceRoomList { val childCoroutineScope = sessionCoroutineScope.childScope(sessionDispatcher, "SpaceRoomListScope-$this") return RustSpaceRoomList( diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/spaces/FakeSpaceService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/spaces/FakeSpaceService.kt index eaa36ee750..d796c0b538 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/spaces/FakeSpaceService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/spaces/FakeSpaceService.kt @@ -23,6 +23,8 @@ class FakeSpaceService( private val joinedSpacesResult: () -> Result> = { lambdaError() }, private val spaceRoomListResult: (RoomId) -> SpaceRoomList = { lambdaError() }, private val leaveSpaceHandleResult: (RoomId) -> LeaveSpaceHandle = { lambdaError() }, + private val joinedParentsResult: (RoomId) -> Result> = { lambdaError() }, + private val getSpaceRoomResult: (RoomId) -> SpaceRoom? = { lambdaError() }, ) : SpaceService { private val _spaceRoomsFlow = MutableSharedFlow>() override val spaceRoomsFlow: SharedFlow> @@ -36,6 +38,14 @@ class FakeSpaceService( return joinedSpacesResult() } + override suspend fun joinedParents(spaceId: RoomId): Result> { + return joinedParentsResult(spaceId) + } + + override suspend fun getSpaceRoom(spaceId: RoomId): SpaceRoom? { + return getSpaceRoomResult(spaceId) + } + override fun spaceRoomList(id: RoomId): SpaceRoomList { return spaceRoomListResult(id) } From 556fdadd7f06a736b3fbd47420e4d12dd4ed654a Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 30 Dec 2025 15:56:32 +0100 Subject: [PATCH 06/22] feature(security&privacy): start branching logic of ManageAuthorizedSpaces --- .../impl/SecurityAndPrivacyFlowNode.kt | 7 ++ .../impl/SecurityAndPrivacyNavigator.kt | 10 ++ .../ManageAuthorizedSpacesView.kt | 8 +- .../impl/root/SecurityAndPrivacyEvent.kt | 3 + .../impl/root/SecurityAndPrivacyPresenter.kt | 92 +++++++++++++++++-- .../impl/root/SecurityAndPrivacyState.kt | 32 +++++-- .../root/SecurityAndPrivacyStateProvider.kt | 8 +- .../impl/root/SecurityAndPrivacyView.kt | 24 ++++- 8 files changed, 161 insertions(+), 23 deletions(-) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt index 3d62e7b5d7..844c4f1a70 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt @@ -24,6 +24,7 @@ import io.element.android.annotations.ContributesNode import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyEntryPoint import io.element.android.features.securityandprivacy.api.securityAndPrivacyPermissions import io.element.android.features.securityandprivacy.impl.editroomaddress.EditRoomAddressNode +import io.element.android.features.securityandprivacy.impl.manageauthorizedspaces.ManageAuthorizedSpacesNode import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyNode import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode @@ -58,6 +59,9 @@ class SecurityAndPrivacyFlowNode( @Parcelize data object EditRoomAddress : NavTarget + + @Parcelize + data object ManageAuthorizedSpaces : NavTarget } private val callback: SecurityAndPrivacyEntryPoint.Callback = callback() @@ -89,6 +93,9 @@ class SecurityAndPrivacyFlowNode( NavTarget.EditRoomAddress -> { createNode(buildContext, plugins = listOf(navigator)) } + NavTarget.ManageAuthorizedSpaces -> { + createNode(buildContext, plugins = listOf(navigator)) + } } } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyNavigator.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyNavigator.kt index 092da87943..274bf0b823 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyNavigator.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyNavigator.kt @@ -18,6 +18,8 @@ interface SecurityAndPrivacyNavigator : Plugin { fun onDone() fun openEditRoomAddress() fun closeEditRoomAddress() + fun openManageAuthorizedSpaces() + fun closeManageAuthorizedSpaces() } class BackstackSecurityAndPrivacyNavigator( @@ -35,4 +37,12 @@ class BackstackSecurityAndPrivacyNavigator( override fun closeEditRoomAddress() { backStack.pop() } + + override fun openManageAuthorizedSpaces() { + backStack.push(SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces) + } + + override fun closeManageAuthorizedSpaces() { + backStack.pop() + } } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt index ed49e048aa..513ecc980a 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt @@ -36,7 +36,6 @@ 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.spaces.SpaceRoom import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.ui.strings.CommonStrings @@ -80,11 +79,11 @@ fun ManageAuthorizedSpacesView( } ) } - if(state.unknownSpaceIds.isNotEmpty()){ + if (state.unknownSpaceIds.isNotEmpty()) { item { ListSectionHeader( title = stringResource(R.string.screen_manage_authorized_spaces_unknown_spaces_section_title), - hasDivider = false, + hasDivider = true, ) } items(items = state.unknownSpaceIds) { @@ -140,7 +139,7 @@ private fun CheckableSpaceListItem( Text(text = supportingText) } }, - leadingContent = avatarData?.let{ + leadingContent = avatarData?.let { ListItemContent.Custom { Avatar( avatarData = avatarData, @@ -158,7 +157,6 @@ private fun CheckableSpaceListItem( ) } - @OptIn(ExperimentalMaterial3Api::class) @Composable private fun ManageAuthorizedSpacesTopBar( diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyEvent.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyEvent.kt index 39abedab8c..0c56a834de 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyEvent.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyEvent.kt @@ -10,10 +10,13 @@ package io.element.android.features.securityandprivacy.impl.root sealed interface SecurityAndPrivacyEvent { data object EditRoomAddress : SecurityAndPrivacyEvent + data object ManageAuthorizedSpaces : SecurityAndPrivacyEvent data object Save : SecurityAndPrivacyEvent data object Exit : SecurityAndPrivacyEvent data object DismissExitConfirmation : SecurityAndPrivacyEvent data class ChangeRoomAccess(val roomAccess: SecurityAndPrivacyRoomAccess) : SecurityAndPrivacyEvent + // Special case for "Space Members" + data object SelectSpaceMemberAccess : SecurityAndPrivacyEvent data object ToggleEncryptionState : SecurityAndPrivacyEvent data object CancelEnableEncryption : SecurityAndPrivacyEvent data object ConfirmEnableEncryption : SecurityAndPrivacyEvent diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt index e627fef40c..d9038133e6 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt @@ -15,6 +15,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf 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 androidx.compose.runtime.setValue @@ -37,9 +38,15 @@ import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.RoomInfo import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility +import io.element.android.libraries.matrix.api.room.join.AllowRule import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility +import io.element.android.libraries.matrix.api.spaces.SpaceRoom +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentSetOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -86,7 +93,7 @@ class SecurityAndPrivacyPresenter( } } - var editedRoomAccess by remember(savedSettings.roomAccess) { + val editedRoomAccess = remember(savedSettings.roomAccess) { mutableStateOf(savedSettings.roomAccess) } var editedHistoryVisibility by remember(savedSettings.historyVisibility) { @@ -99,13 +106,26 @@ class SecurityAndPrivacyPresenter( mutableStateOf(savedIsVisibleInRoomDirectory.value) } val editedSettings = SecurityAndPrivacySettings( - roomAccess = editedRoomAccess, + roomAccess = editedRoomAccess.value, isEncrypted = editedIsEncrypted, isVisibleInRoomDirectory = editedVisibleInRoomDirectory, historyVisibility = editedHistoryVisibility, address = savedSettings.address, ) + val selectableJoinedSpaces by produceState(persistentSetOf()) { + val joinedParentSpaces = matrixClient + .spaceService + .joinedParents(room.roomId) + .getOrDefault(emptyList()) + + val nonParentJoinedSpaces = savedSettings.roomAccess + .spaceIds() + .mapNotNull { spaceId -> matrixClient.spaceService.getSpaceRoom(spaceId) } + + value = (joinedParentSpaces + nonParentJoinedSpaces).toImmutableSet() + } + var showEnableEncryptionConfirmation by remember(savedSettings.isEncrypted) { mutableStateOf(false) } val permissions by room.permissionsAsState(SecurityAndPrivacyPermissions.DEFAULT) { perms -> perms.securityAndPrivacyPermissions() @@ -122,7 +142,7 @@ class SecurityAndPrivacyPresenter( ) } is SecurityAndPrivacyEvent.ChangeRoomAccess -> { - editedRoomAccess = event.roomAccess + editedRoomAccess.value = event.roomAccess } is SecurityAndPrivacyEvent.ToggleEncryptionState -> { if (editedIsEncrypted) { @@ -161,6 +181,12 @@ class SecurityAndPrivacyPresenter( SecurityAndPrivacyEvent.DismissExitConfirmation -> { saveAction.value = AsyncAction.Uninitialized } + SecurityAndPrivacyEvent.ManageAuthorizedSpaces -> navigator.openManageAuthorizedSpaces() + SecurityAndPrivacyEvent.SelectSpaceMemberAccess -> handleSpaceMemberAccessSelection( + selectableJoinedSpaces = selectableJoinedSpaces, + savedAccess = savedSettings.roomAccess, + editedAccess = editedRoomAccess, + ) } } @@ -179,13 +205,14 @@ class SecurityAndPrivacyPresenter( saveAction = saveAction.value, permissions = permissions, isSpace = roomInfo.isSpace, + selectableJoinedSpaces = selectableJoinedSpaces, eventSink = ::handleEvent, ) // Revert changes that the user is not allowed to make anymore LaunchedEffect(permissions, state.editedSettings.roomAccess) { if (!state.showRoomAccessSection) { - editedRoomAccess = savedSettings.roomAccess + editedRoomAccess.value = savedSettings.roomAccess } if (!state.showEncryptionSection) { editedIsEncrypted = savedSettings.isEncrypted @@ -202,6 +229,51 @@ class SecurityAndPrivacyPresenter( return state } + private fun handleSpaceMemberAccessSelection( + selectableJoinedSpaces: Set, + savedAccess: SecurityAndPrivacyRoomAccess, + editedAccess: MutableState, + ) { + if(editedAccess.value is SecurityAndPrivacyRoomAccess.SpaceMember) { + return + } + val spaceSelection = getSpaceSelection(selectableJoinedSpaces, savedAccess) + when(spaceSelection){ + is SpaceSelection.None -> Unit + is SpaceSelection.Multiple -> navigator.openManageAuthorizedSpaces() + is SpaceSelection.Single -> { + val newRoomAccess = SecurityAndPrivacyRoomAccess.SpaceMember( + spaceIds = persistentListOf(spaceSelection.spaceId) + ) + editedAccess.value = newRoomAccess + } + } + } + + private fun getSpaceSelection( + selectableJoinedSpaces: Set, + savedAccess: SecurityAndPrivacyRoomAccess, + ): SpaceSelection { + val selectableSpacesCount = (selectableJoinedSpaces.map { it.roomId } + savedAccess.spaceIds()).toSet().size + return when { + selectableSpacesCount == 0 -> SpaceSelection.None + selectableSpacesCount > 1 -> SpaceSelection.Multiple + else -> { + val joinedSpace = selectableJoinedSpaces.firstOrNull() + if (joinedSpace != null) { + SpaceSelection.Single(joinedSpace.roomId, joinedSpace) + } else { + val spaceId = savedAccess.spaceIds().firstOrNull() + if (spaceId == null) { + SpaceSelection.None + } else { + SpaceSelection.Single(spaceId, null) + } + } + } + } + } + private fun CoroutineScope.isRoomVisibleInRoomDirectory(isRoomVisible: MutableState>) = launch { isRoomVisible.runUpdatingState { room.getRoomVisibility().map { it == RoomVisibility.Public } @@ -280,7 +352,12 @@ private fun JoinRule?.map(): SecurityAndPrivacyRoomAccess { return when (this) { JoinRule.Public -> SecurityAndPrivacyRoomAccess.Anyone JoinRule.Knock, is JoinRule.KnockRestricted -> SecurityAndPrivacyRoomAccess.AskToJoin - is JoinRule.Restricted -> SecurityAndPrivacyRoomAccess.SpaceMember + is JoinRule.Restricted -> SecurityAndPrivacyRoomAccess.SpaceMember( + spaceIds = this.rules + .filterIsInstance() + .map { it.roomId } + .toImmutableList() + ) JoinRule.Invite -> SecurityAndPrivacyRoomAccess.InviteOnly // All other cases are not supported so we default to InviteOnly is JoinRule.Custom, @@ -294,8 +371,9 @@ private fun SecurityAndPrivacyRoomAccess.map(): JoinRule? { SecurityAndPrivacyRoomAccess.Anyone -> JoinRule.Public SecurityAndPrivacyRoomAccess.AskToJoin -> JoinRule.Knock SecurityAndPrivacyRoomAccess.InviteOnly -> JoinRule.Private - // SpaceMember can't be selected in the ui - SecurityAndPrivacyRoomAccess.SpaceMember -> null + is SecurityAndPrivacyRoomAccess.SpaceMember -> JoinRule.Restricted( + rules = this.spaceIds.map { AllowRule.RoomMembership(it) }.toImmutableList() + ) } } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt index e64cf633a8..b71f375fe7 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt @@ -11,6 +11,11 @@ package io.element.android.features.securityandprivacy.impl.root import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyPermissions import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData +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.persistentListOf import kotlinx.collections.immutable.toImmutableList data class SecurityAndPrivacyState( @@ -24,8 +29,10 @@ data class SecurityAndPrivacyState( val saveAction: AsyncAction, val isSpace: Boolean, private val permissions: SecurityAndPrivacyPermissions, + private val selectableJoinedSpaces: ImmutableSet, val eventSink: (SecurityAndPrivacyEvent) -> Unit ) { + val canBeSaved = savedSettings != editedSettings // Logic is in https://github.com/element-hq/element-meta/issues/3029 @@ -76,18 +83,31 @@ enum class SecurityAndPrivacyHistoryVisibility { } } -enum class SecurityAndPrivacyRoomAccess { - InviteOnly, - AskToJoin, - Anyone, - SpaceMember; +sealed interface SpaceSelection { + data object None : SpaceSelection + data class Single(val spaceId: RoomId, val spaceRoom: SpaceRoom?) : SpaceSelection + data object Multiple : SpaceSelection +} + +sealed interface SecurityAndPrivacyRoomAccess { + data object InviteOnly : SecurityAndPrivacyRoomAccess + data object AskToJoin : SecurityAndPrivacyRoomAccess + data object Anyone : SecurityAndPrivacyRoomAccess + data class SpaceMember(val spaceIds: ImmutableList) : SecurityAndPrivacyRoomAccess fun canConfigureRoomVisibility(): Boolean { return when (this) { - InviteOnly, SpaceMember -> false + InviteOnly, is SpaceMember -> false AskToJoin, Anyone -> true } } + + fun spaceIds(): ImmutableList { + return when (this) { + is SpaceMember -> spaceIds + else -> persistentListOf() + } + } } sealed class SecurityAndPrivacyFailures : Exception() { diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt index 223d524ca3..2c984fd16a 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt @@ -12,6 +12,10 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyPermissions import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.spaces.SpaceRoom +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableSet open class SecurityAndPrivacyStateProvider : PreviewParameterProvider { override val values: Sequence @@ -61,7 +65,7 @@ private fun commonSecurityAndPrivacyStates(isSpace: Boolean): Sequence = emptySet(), eventSink: (SecurityAndPrivacyEvent) -> Unit = {} ) = SecurityAndPrivacyState( editedSettings = editedSettings, @@ -127,5 +132,6 @@ fun aSecurityAndPrivacyState( isKnockEnabled = isKnockEnabled, permissions = permissions, isSpace = isSpace, + selectableJoinedSpaces = selectableJoinedSpaces.toImmutableSet(), eventSink = eventSink, ) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt index bf6a5ffdf2..dc8c349bbe 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt @@ -50,6 +50,7 @@ import io.element.android.libraries.designsystem.text.stringWithLink import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.ListItem +import io.element.android.libraries.designsystem.theme.components.ListSectionHeader import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton @@ -95,6 +96,8 @@ fun SecurityAndPrivacyView( saved = state.savedSettings.roomAccess, isKnockEnabled = state.isKnockEnabled, onSelectOption = { state.eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(it)) }, + onManageSpacesClick = { state.eventSink(SecurityAndPrivacyEvent.ManageAuthorizedSpaces) }, + onSpaceMemberAccessClick = { state.eventSink(SecurityAndPrivacyEvent.SelectSpaceMemberAccess) } ) } if (state.showRoomVisibilitySections) { @@ -212,6 +215,8 @@ private fun RoomAccessSection( saved: SecurityAndPrivacyRoomAccess, isKnockEnabled: Boolean, onSelectOption: (SecurityAndPrivacyRoomAccess) -> Unit, + onSpaceMemberAccessClick: () -> Unit, + onManageSpacesClick: () -> Unit, modifier: Modifier = Modifier, ) { SecurityAndPrivacySection( @@ -226,17 +231,15 @@ private fun RoomAccessSection( onClick = { onSelectOption(SecurityAndPrivacyRoomAccess.Anyone) }, ) // Show space member option, but disabled as we don't support this option for now. - if (saved == SecurityAndPrivacyRoomAccess.SpaceMember) { ListItem( headlineContent = { Text(text = stringResource(R.string.screen_security_and_privacy_room_access_space_members_option_title)) }, supportingContent = { Text(text = stringResource(R.string.screen_security_and_privacy_room_access_space_members_option_unavailable_description)) }, - trailingContent = ListItemContent.RadioButton(selected = edited == SecurityAndPrivacyRoomAccess.SpaceMember, enabled = false), + trailingContent = ListItemContent.RadioButton(selected = edited is SecurityAndPrivacyRoomAccess.SpaceMember), leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Space())), - enabled = false, + onClick = onSpaceMemberAccessClick, ) - } // Show Ask to join option in two cases: // - the Knock FF is enabled // - AskToJoin is the current saved value @@ -257,6 +260,19 @@ private fun RoomAccessSection( leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Lock())), onClick = { onSelectOption(SecurityAndPrivacyRoomAccess.InviteOnly) }, ) + if (edited is SecurityAndPrivacyRoomAccess.SpaceMember) { + val footerText = stringWithLink( + textRes = R.string.screen_security_and_privacy_room_access_footer, + url = stringResource(R.string.screen_security_and_privacy_room_access_footer_manage_spaces_action), + onLinkClick = { onManageSpacesClick()}, + ) + Text( + text = footerText, + style = ElementTheme.typography.fontBodySmRegular, + color = ElementTheme.colors.textSecondary, + modifier = Modifier.padding(bottom = 12.dp, start = 56.dp, end = 24.dp) + ) + } } } From 2be6b6ce24779caef1695ce21fe268198672f5b8 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 5 Jan 2026 20:01:21 +0100 Subject: [PATCH 07/22] quality: rename class --- .../impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt | 2 +- .../manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt index 3b52c2ac3f..dfb7d6c833 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt @@ -25,7 +25,7 @@ import io.element.android.libraries.di.RoomScope class ManageAuthorizedSpacesNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, - presenterFactory: EditRoomAddressPresenter.Factory, + presenterFactory: ManageAuthorizedSpacesPresenter.Factory, ) : Node(buildContext, plugins = plugins) { private val navigator = plugins().first() private val presenter = presenterFactory.create(navigator) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt index 527393c9d3..c815bb91b7 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt @@ -21,14 +21,14 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom import kotlinx.collections.immutable.persistentListOf @AssistedInject -class EditRoomAddressPresenter( +class ManageAuthorizedSpacesPresenter( @Assisted private val navigator: SecurityAndPrivacyNavigator, private val client: MatrixClient, private val room: JoinedRoom, ) : Presenter { @AssistedFactory interface Factory { - fun create(navigator: SecurityAndPrivacyNavigator): EditRoomAddressPresenter + fun create(navigator: SecurityAndPrivacyNavigator): ManageAuthorizedSpacesPresenter } @Composable From 8e08c6108da3751dd0739270cf3435592bb6271b Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 5 Jan 2026 20:01:38 +0100 Subject: [PATCH 08/22] feature(security&privacy): make spaceSelection part of the state --- .../impl/root/SecurityAndPrivacyPresenter.kt | 18 +++++++++++------- .../impl/root/SecurityAndPrivacyState.kt | 1 + .../root/SecurityAndPrivacyStateProvider.kt | 2 ++ 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt index d9038133e6..c1a7bbeb33 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt @@ -126,6 +126,12 @@ class SecurityAndPrivacyPresenter( value = (joinedParentSpaces + nonParentJoinedSpaces).toImmutableSet() } + val spaceSelection by remember { + derivedStateOf { + getSpaceSelection(selectableJoinedSpaces, savedSettings.roomAccess) + } + } + var showEnableEncryptionConfirmation by remember(savedSettings.isEncrypted) { mutableStateOf(false) } val permissions by room.permissionsAsState(SecurityAndPrivacyPermissions.DEFAULT) { perms -> perms.securityAndPrivacyPermissions() @@ -183,8 +189,7 @@ class SecurityAndPrivacyPresenter( } SecurityAndPrivacyEvent.ManageAuthorizedSpaces -> navigator.openManageAuthorizedSpaces() SecurityAndPrivacyEvent.SelectSpaceMemberAccess -> handleSpaceMemberAccessSelection( - selectableJoinedSpaces = selectableJoinedSpaces, - savedAccess = savedSettings.roomAccess, + spaceSelection = spaceSelection, editedAccess = editedRoomAccess, ) } @@ -206,6 +211,7 @@ class SecurityAndPrivacyPresenter( permissions = permissions, isSpace = roomInfo.isSpace, selectableJoinedSpaces = selectableJoinedSpaces, + spaceSelection = spaceSelection, eventSink = ::handleEvent, ) @@ -230,15 +236,13 @@ class SecurityAndPrivacyPresenter( } private fun handleSpaceMemberAccessSelection( - selectableJoinedSpaces: Set, - savedAccess: SecurityAndPrivacyRoomAccess, + spaceSelection: SpaceSelection, editedAccess: MutableState, ) { - if(editedAccess.value is SecurityAndPrivacyRoomAccess.SpaceMember) { + if (editedAccess.value is SecurityAndPrivacyRoomAccess.SpaceMember) { return } - val spaceSelection = getSpaceSelection(selectableJoinedSpaces, savedAccess) - when(spaceSelection){ + when (spaceSelection) { is SpaceSelection.None -> Unit is SpaceSelection.Multiple -> navigator.openManageAuthorizedSpaces() is SpaceSelection.Single -> { diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt index b71f375fe7..70a2a72179 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt @@ -30,6 +30,7 @@ data class SecurityAndPrivacyState( val isSpace: Boolean, private val permissions: SecurityAndPrivacyPermissions, private val selectableJoinedSpaces: ImmutableSet, + private val spaceSelection: SpaceSelection, val eventSink: (SecurityAndPrivacyEvent) -> Unit ) { diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt index 2c984fd16a..11c4054ac5 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt @@ -122,6 +122,7 @@ fun aSecurityAndPrivacyState( isKnockEnabled: Boolean = true, isSpace: Boolean = false, selectableJoinedSpaces: Set = emptySet(), + spaceSelection: SpaceSelection = SpaceSelection.None, eventSink: (SecurityAndPrivacyEvent) -> Unit = {} ) = SecurityAndPrivacyState( editedSettings = editedSettings, @@ -133,5 +134,6 @@ fun aSecurityAndPrivacyState( permissions = permissions, isSpace = isSpace, selectableJoinedSpaces = selectableJoinedSpaces.toImmutableSet(), + spaceSelection = SpaceSelection.None, eventSink = eventSink, ) From 96745c765ac09f1469989d786e1e883f7b50bc41 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 5 Jan 2026 21:20:26 +0100 Subject: [PATCH 09/22] feature(security&privacy): check SpaceSettings ff --- .../impl/root/SecurityAndPrivacyPresenter.kt | 5 +++++ .../impl/root/SecurityAndPrivacyState.kt | 1 + .../impl/root/SecurityAndPrivacyStateProvider.kt | 2 ++ .../impl/root/SecurityAndPrivacyView.kt | 11 ++++++++--- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt index c1a7bbeb33..e025914801 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt @@ -72,6 +72,10 @@ class SecurityAndPrivacyPresenter( val isKnockEnabled by remember { featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock) }.collectAsState(false) + val isSpaceSettingsEnabled by remember { + featureFlagService.isFeatureEnabledFlow(FeatureFlags.SpaceSettings) + }.collectAsState(false) + val saveAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } val homeserverName = remember { matrixClient.userIdServerName() } val roomInfo by room.roomInfoFlow.collectAsState() @@ -210,6 +214,7 @@ class SecurityAndPrivacyPresenter( saveAction = saveAction.value, permissions = permissions, isSpace = roomInfo.isSpace, + isSpaceSettingsEnabled = isSpaceSettingsEnabled, selectableJoinedSpaces = selectableJoinedSpaces, spaceSelection = spaceSelection, eventSink = ::handleEvent, diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt index 70a2a72179..bcbee0203d 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt @@ -26,6 +26,7 @@ data class SecurityAndPrivacyState( val homeserverName: String, val showEnableEncryptionConfirmation: Boolean, val isKnockEnabled: Boolean, + val isSpaceSettingsEnabled: Boolean, val saveAction: AsyncAction, val isSpace: Boolean, private val permissions: SecurityAndPrivacyPermissions, diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt index 11c4054ac5..312043324e 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt @@ -123,6 +123,7 @@ fun aSecurityAndPrivacyState( isSpace: Boolean = false, selectableJoinedSpaces: Set = emptySet(), spaceSelection: SpaceSelection = SpaceSelection.None, + isSpaceSettingsEnabled: Boolean = true, eventSink: (SecurityAndPrivacyEvent) -> Unit = {} ) = SecurityAndPrivacyState( editedSettings = editedSettings, @@ -135,5 +136,6 @@ fun aSecurityAndPrivacyState( isSpace = isSpace, selectableJoinedSpaces = selectableJoinedSpaces.toImmutableSet(), spaceSelection = SpaceSelection.None, + isSpaceSettingsEnabled = isSpaceSettingsEnabled, eventSink = eventSink, ) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt index dc8c349bbe..374a4e3911 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt @@ -50,7 +50,6 @@ import io.element.android.libraries.designsystem.text.stringWithLink import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.ListItem -import io.element.android.libraries.designsystem.theme.components.ListSectionHeader import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton @@ -95,6 +94,7 @@ fun SecurityAndPrivacyView( edited = state.editedSettings.roomAccess, saved = state.savedSettings.roomAccess, isKnockEnabled = state.isKnockEnabled, + isSpaceSettingsEnabled = state.isSpaceSettingsEnabled, onSelectOption = { state.eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(it)) }, onManageSpacesClick = { state.eventSink(SecurityAndPrivacyEvent.ManageAuthorizedSpaces) }, onSpaceMemberAccessClick = { state.eventSink(SecurityAndPrivacyEvent.SelectSpaceMemberAccess) } @@ -214,6 +214,7 @@ private fun RoomAccessSection( edited: SecurityAndPrivacyRoomAccess, saved: SecurityAndPrivacyRoomAccess, isKnockEnabled: Boolean, + isSpaceSettingsEnabled: Boolean, onSelectOption: (SecurityAndPrivacyRoomAccess) -> Unit, onSpaceMemberAccessClick: () -> Unit, onManageSpacesClick: () -> Unit, @@ -230,7 +231,10 @@ private fun RoomAccessSection( leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Public())), onClick = { onSelectOption(SecurityAndPrivacyRoomAccess.Anyone) }, ) - // Show space member option, but disabled as we don't support this option for now. + // Show SpaceMember option in two cases: + // - the SpaceSettings FF is enabled + // - SpaceMember is the current saved value + if (saved is SecurityAndPrivacyRoomAccess.SpaceMember || isSpaceSettingsEnabled) ListItem( headlineContent = { Text(text = stringResource(R.string.screen_security_and_privacy_room_access_space_members_option_title)) }, supportingContent = { @@ -239,6 +243,7 @@ private fun RoomAccessSection( trailingContent = ListItemContent.RadioButton(selected = edited is SecurityAndPrivacyRoomAccess.SpaceMember), leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Space())), onClick = onSpaceMemberAccessClick, + enabled = isSpaceSettingsEnabled, ) // Show Ask to join option in two cases: // - the Knock FF is enabled @@ -264,7 +269,7 @@ private fun RoomAccessSection( val footerText = stringWithLink( textRes = R.string.screen_security_and_privacy_room_access_footer, url = stringResource(R.string.screen_security_and_privacy_room_access_footer_manage_spaces_action), - onLinkClick = { onManageSpacesClick()}, + onLinkClick = { onManageSpacesClick() }, ) Text( text = footerText, From 1930877a81241da06827e7b3bee9baf458797bdc Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 6 Jan 2026 15:15:38 +0100 Subject: [PATCH 10/22] feature(security&privacy): iterate on SpaceMember option --- .../impl/SecurityAndPrivacyFlowNode.kt | 21 ++++++- .../impl/SecurityAndPrivacyNavigator.kt | 7 ++- .../ManageAuthorizedSpacesEvent.kt | 1 + .../ManageAuthorizedSpacesNode.kt | 21 ++++++- .../ManageAuthorizedSpacesPresenter.kt | 35 ++++++++--- .../ManageAuthorizedSpacesState.kt | 14 +++-- .../ManageAuthorizedSpacesStateProvider.kt | 27 ++++++--- .../ManageAuthorizedSpacesView.kt | 10 ++-- .../impl/root/SecurityAndPrivacyNode.kt | 14 +++++ .../impl/root/SecurityAndPrivacyPresenter.kt | 43 +++++++------ .../impl/root/SecurityAndPrivacyState.kt | 60 ++++++++++++++++--- .../root/SecurityAndPrivacyStateProvider.kt | 5 +- .../impl/root/SecurityAndPrivacyView.kt | 56 +++++++++-------- 13 files changed, 226 insertions(+), 88 deletions(-) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt index 844c4f1a70..d29a7ffd20 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt @@ -12,12 +12,14 @@ import android.os.Parcelable import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.lifecycle.Lifecycle +import androidx.lifecycle.coroutineScope import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle 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.pop import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode @@ -31,12 +33,15 @@ import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.callback import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.powerlevels.use +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize @ContributesNode(RoomScope::class) @@ -61,7 +66,7 @@ class SecurityAndPrivacyFlowNode( data object EditRoomAddress : NavTarget @Parcelize - data object ManageAuthorizedSpaces : NavTarget + data class ManageAuthorizedSpaces(val initialSelection: List) : NavTarget } private val callback: SecurityAndPrivacyEntryPoint.Callback = callback() @@ -83,6 +88,18 @@ class SecurityAndPrivacyFlowNode( callback.onDone() } } + whenChildrenAttached { commonLifecycle: Lifecycle, + securityAndPrivacyNode: SecurityAndPrivacyNode, + manageAuthorizedSpacesNode: ManageAuthorizedSpacesNode -> + commonLifecycle.coroutineScope.launch { + val authorizedSpacesData = securityAndPrivacyNode.getAuthorizedSpacesData() + val selectedSpaces = manageAuthorizedSpacesNode.waitForCompletion(authorizedSpacesData) + withContext(NonCancellable) { + backstack.pop() + securityAndPrivacyNode.onAuthorizedSpacesSelected(selectedSpaces) + } + } + } } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -93,7 +110,7 @@ class SecurityAndPrivacyFlowNode( NavTarget.EditRoomAddress -> { createNode(buildContext, plugins = listOf(navigator)) } - NavTarget.ManageAuthorizedSpaces -> { + is NavTarget.ManageAuthorizedSpaces -> { createNode(buildContext, plugins = listOf(navigator)) } } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyNavigator.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyNavigator.kt index 274bf0b823..da6ca379e8 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyNavigator.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyNavigator.kt @@ -13,12 +13,13 @@ import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.operation.pop import com.bumble.appyx.navmodel.backstack.operation.push import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyEntryPoint +import io.element.android.libraries.matrix.api.core.RoomId interface SecurityAndPrivacyNavigator : Plugin { fun onDone() fun openEditRoomAddress() fun closeEditRoomAddress() - fun openManageAuthorizedSpaces() + fun openManageAuthorizedSpaces(initialSelection: List) fun closeManageAuthorizedSpaces() } @@ -38,8 +39,8 @@ class BackstackSecurityAndPrivacyNavigator( backStack.pop() } - override fun openManageAuthorizedSpaces() { - backStack.push(SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces) + override fun openManageAuthorizedSpaces(initialSelection: List) { + backStack.push(SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces(initialSelection)) } override fun closeManageAuthorizedSpaces() { diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesEvent.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesEvent.kt index 0515878ade..47abe2fbce 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesEvent.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesEvent.kt @@ -11,6 +11,7 @@ package io.element.android.features.securityandprivacy.impl.manageauthorizedspac import io.element.android.libraries.matrix.api.core.RoomId sealed interface ManageAuthorizedSpacesEvent { + data class SetData(val data: AuthorizedSpacesSelection) : ManageAuthorizedSpacesEvent data object Done : ManageAuthorizedSpacesEvent data class ToggleSpace(val roomId: RoomId) : ManageAuthorizedSpacesEvent } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt index dfb7d6c833..5608c0ce16 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt @@ -9,6 +9,8 @@ package io.element.android.features.securityandprivacy.impl.manageauthorizedspaces import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node @@ -18,7 +20,12 @@ import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNavigator +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.appyx.launchMolecule import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.core.RoomId +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.flow.first @ContributesNode(RoomScope::class) @AssistedInject @@ -27,12 +34,24 @@ class ManageAuthorizedSpacesNode( @Assisted plugins: List, presenterFactory: ManageAuthorizedSpacesPresenter.Factory, ) : Node(buildContext, plugins = plugins) { + + data class Params( + val initialSelection: List + ) : NodeInputs + private val navigator = plugins().first() private val presenter = presenterFactory.create(navigator) + private val stateFlow = launchMolecule { presenter.present() } + + suspend fun waitForCompletion(data: AuthorizedSpacesSelection): ImmutableList { + stateFlow.value.eventSink(ManageAuthorizedSpacesEvent.SetData(data)) + return stateFlow.first { it.isSelectionComplete }.selectedIds + } + @Composable override fun View(modifier: Modifier) { - val state = presenter.present() + val state by stateFlow.collectAsState() ManageAuthorizedSpacesView( state = state, onBackClick = ::navigateUp, diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt index c815bb91b7..09869cdf09 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt @@ -9,16 +9,21 @@ package io.element.android.features.securityandprivacy.impl.manageauthorizedspaces import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedInject import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNavigator import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.JoinedRoom +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList @AssistedInject class ManageAuthorizedSpacesPresenter( @@ -33,19 +38,33 @@ class ManageAuthorizedSpacesPresenter( @Composable override fun present(): ManageAuthorizedSpacesState { - val roomInfo by room.roomInfoFlow.collectAsState() + var currentSelection: ImmutableList by remember { mutableStateOf(persistentListOf()) } + var spacesData by remember { mutableStateOf(AuthorizedSpacesSelection()) } + var isSelectionComplete by remember { mutableStateOf(false) } + fun handleEvent(event: ManageAuthorizedSpacesEvent) { when (event) { - ManageAuthorizedSpacesEvent.Done -> TODO() - is ManageAuthorizedSpacesEvent.ToggleSpace -> TODO() + ManageAuthorizedSpacesEvent.Done -> { + isSelectionComplete = true + } + is ManageAuthorizedSpacesEvent.ToggleSpace -> { + currentSelection = if (currentSelection.contains(event.roomId)) { + currentSelection.minus(event.roomId).toPersistentList() + } else { + currentSelection.plus(event.roomId).toPersistentList() + } + } + is ManageAuthorizedSpacesEvent.SetData -> { + spacesData = event.data + currentSelection = event.data.initialSelectedIds + } } } return ManageAuthorizedSpacesState( - joinedSpaces = persistentListOf(), - unknownSpaceIds = persistentListOf(), - currentSelection = persistentListOf(), - initialSelection = persistentListOf(), + selection = spacesData, + selectedIds = currentSelection, + isSelectionComplete = isSelectionComplete, eventSink = ::handleEvent, ) } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt index 688ef6e6e9..7564fabae9 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt @@ -11,11 +11,17 @@ package io.element.android.features.securityandprivacy.impl.manageauthorizedspac 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.persistentListOf data class ManageAuthorizedSpacesState( - val joinedSpaces: ImmutableList, - val unknownSpaceIds: ImmutableList, - val currentSelection: ImmutableList, - val initialSelection: ImmutableList, + val selection: AuthorizedSpacesSelection, + val selectedIds: ImmutableList, + val isSelectionComplete: Boolean, val eventSink: (ManageAuthorizedSpacesEvent) -> Unit ) + +data class AuthorizedSpacesSelection( + val joinedSpaces: ImmutableList = persistentListOf(), + val unknownSpaceIds: ImmutableList = persistentListOf(), + val initialSelectedIds: ImmutableList = persistentListOf() +) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt index d91e16f3e6..e232d02022 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt @@ -20,11 +20,15 @@ open class ManageAuthorizedSpacesStateProvider : PreviewParameterProvider { } } -private fun aManageAuthorizedSpacesState( +fun anAuthorizedSpacesData( joinedSpaces: List = aSpaceRoomList(5), unknownSpaceIds: List = emptyList(), - currentSelection: List = emptyList(), initialSelection: List = emptyList(), - eventSink: (ManageAuthorizedSpacesEvent) -> Unit = {}, -) = ManageAuthorizedSpacesState( +) = AuthorizedSpacesSelection( joinedSpaces = joinedSpaces.toImmutableList(), unknownSpaceIds = unknownSpaceIds.toImmutableList(), - currentSelection = currentSelection.toImmutableList(), - initialSelection = initialSelection.toImmutableList(), + initialSelectedIds = initialSelection.toImmutableList(), +) + +private fun aManageAuthorizedSpacesState( + authorizedSpacesData: AuthorizedSpacesSelection = anAuthorizedSpacesData(), + currentSelection: List = emptyList(), + eventSink: (ManageAuthorizedSpacesEvent) -> Unit = {}, +) = ManageAuthorizedSpacesState( + selection = authorizedSpacesData, + selectedIds = currentSelection.toImmutableList(), + isSelectionComplete = false, eventSink = eventSink, ) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt index 513ecc980a..1c4ff7bc77 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt @@ -66,12 +66,12 @@ fun ManageAuthorizedSpacesView( hasDivider = false, ) } - items(items = state.joinedSpaces) { space -> + items(items = state.selection.joinedSpaces) { space -> CheckableSpaceListItem( headlineText = space.displayName, supportingText = space.canonicalAlias?.value, avatarData = space.getAvatarData(AvatarSize.SpaceMember), - checked = state.currentSelection.contains(space.roomId), + checked = state.selectedIds.contains(space.roomId), onCheckedChange = { _ -> state.eventSink( ManageAuthorizedSpacesEvent.ToggleSpace(space.roomId) @@ -79,19 +79,19 @@ fun ManageAuthorizedSpacesView( } ) } - if (state.unknownSpaceIds.isNotEmpty()) { + if (state.selection.unknownSpaceIds.isNotEmpty()) { item { ListSectionHeader( title = stringResource(R.string.screen_manage_authorized_spaces_unknown_spaces_section_title), hasDivider = true, ) } - items(items = state.unknownSpaceIds) { + items(items = state.selection.unknownSpaceIds) { CheckableSpaceListItem( headlineText = stringResource(R.string.screen_manage_authorized_spaces_unknown_space), supportingText = it.value, avatarData = null, - checked = state.currentSelection.contains(it), + checked = state.selectedIds.contains(it), onCheckedChange = { _ -> state.eventSink( ManageAuthorizedSpacesEvent.ToggleSpace(it) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyNode.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyNode.kt index d5fb72e72e..1e5e6e0943 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyNode.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyNode.kt @@ -23,9 +23,12 @@ import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode import io.element.android.compound.theme.ElementTheme import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNavigator +import io.element.android.features.securityandprivacy.impl.manageauthorizedspaces.AuthorizedSpacesSelection import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.architecture.appyx.launchMolecule import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.core.RoomId +import kotlinx.collections.immutable.ImmutableList @ContributesNode(RoomScope::class) @AssistedInject @@ -43,6 +46,16 @@ class SecurityAndPrivacyNode( activity.openUrlInChromeCustomTab(null, darkTheme, url) } + fun getAuthorizedSpacesData(): AuthorizedSpacesSelection{ + return stateFlow.value.getAuthorizedSpaceData() + } + + fun onAuthorizedSpacesSelected(selectedSpaces: ImmutableList) { + stateFlow.value.eventSink( + SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.SpaceMember(selectedSpaces)) + ) + } + @Composable override fun View(modifier: Modifier) { val activity = requireNotNull(LocalActivity.current) @@ -56,4 +69,5 @@ class SecurityAndPrivacyNode( modifier = modifier ) } + } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt index e025914801..bc7688dab3 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt @@ -35,6 +35,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomAlias +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.RoomInfo import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility @@ -130,9 +131,9 @@ class SecurityAndPrivacyPresenter( value = (joinedParentSpaces + nonParentJoinedSpaces).toImmutableSet() } - val spaceSelection by remember { + val spaceSelectionMode by remember { derivedStateOf { - getSpaceSelection(selectableJoinedSpaces, savedSettings.roomAccess) + getSpaceSelectionMode(selectableJoinedSpaces, savedSettings.roomAccess) } } @@ -191,9 +192,12 @@ class SecurityAndPrivacyPresenter( SecurityAndPrivacyEvent.DismissExitConfirmation -> { saveAction.value = AsyncAction.Uninitialized } - SecurityAndPrivacyEvent.ManageAuthorizedSpaces -> navigator.openManageAuthorizedSpaces() + SecurityAndPrivacyEvent.ManageAuthorizedSpaces -> { + navigator.openManageAuthorizedSpaces(editedSettings.roomAccess.spaceIds()) + } SecurityAndPrivacyEvent.SelectSpaceMemberAccess -> handleSpaceMemberAccessSelection( - spaceSelection = spaceSelection, + spaceSelectionMode = spaceSelectionMode, + spaceIds = editedSettings.roomAccess.spaceIds(), editedAccess = editedRoomAccess, ) } @@ -216,7 +220,7 @@ class SecurityAndPrivacyPresenter( isSpace = roomInfo.isSpace, isSpaceSettingsEnabled = isSpaceSettingsEnabled, selectableJoinedSpaces = selectableJoinedSpaces, - spaceSelection = spaceSelection, + spaceSelectionMode = spaceSelectionMode, eventSink = ::handleEvent, ) @@ -241,42 +245,45 @@ class SecurityAndPrivacyPresenter( } private fun handleSpaceMemberAccessSelection( - spaceSelection: SpaceSelection, + spaceSelectionMode: SpaceSelectionMode, + spaceIds: List, editedAccess: MutableState, ) { if (editedAccess.value is SecurityAndPrivacyRoomAccess.SpaceMember) { return } - when (spaceSelection) { - is SpaceSelection.None -> Unit - is SpaceSelection.Multiple -> navigator.openManageAuthorizedSpaces() - is SpaceSelection.Single -> { + when (spaceSelectionMode) { + is SpaceSelectionMode.None -> Unit + is SpaceSelectionMode.Multiple -> navigator.openManageAuthorizedSpaces( + initialSelection = spaceIds , + ) + is SpaceSelectionMode.Single -> { val newRoomAccess = SecurityAndPrivacyRoomAccess.SpaceMember( - spaceIds = persistentListOf(spaceSelection.spaceId) + spaceIds = persistentListOf(spaceSelectionMode.spaceId) ) editedAccess.value = newRoomAccess } } } - private fun getSpaceSelection( + private fun getSpaceSelectionMode( selectableJoinedSpaces: Set, savedAccess: SecurityAndPrivacyRoomAccess, - ): SpaceSelection { + ): SpaceSelectionMode { val selectableSpacesCount = (selectableJoinedSpaces.map { it.roomId } + savedAccess.spaceIds()).toSet().size return when { - selectableSpacesCount == 0 -> SpaceSelection.None - selectableSpacesCount > 1 -> SpaceSelection.Multiple + selectableSpacesCount == 0 -> SpaceSelectionMode.None + selectableSpacesCount > 1 -> SpaceSelectionMode.Multiple else -> { val joinedSpace = selectableJoinedSpaces.firstOrNull() if (joinedSpace != null) { - SpaceSelection.Single(joinedSpace.roomId, joinedSpace) + SpaceSelectionMode.Single(joinedSpace.roomId, joinedSpace) } else { val spaceId = savedAccess.spaceIds().firstOrNull() if (spaceId == null) { - SpaceSelection.None + SpaceSelectionMode.None } else { - SpaceSelection.Single(spaceId, null) + SpaceSelectionMode.Single(spaceId, null) } } } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt index bcbee0203d..d14d83b16b 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt @@ -8,7 +8,11 @@ package io.element.android.features.securityandprivacy.impl.root +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyPermissions +import io.element.android.features.securityandprivacy.impl.R +import io.element.android.features.securityandprivacy.impl.manageauthorizedspaces.AuthorizedSpacesSelection import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.core.RoomId @@ -25,16 +29,32 @@ data class SecurityAndPrivacyState( val editedSettings: SecurityAndPrivacySettings, val homeserverName: String, val showEnableEncryptionConfirmation: Boolean, - val isKnockEnabled: Boolean, - val isSpaceSettingsEnabled: Boolean, + private val isKnockEnabled: Boolean, + private val isSpaceSettingsEnabled: Boolean, val saveAction: AsyncAction, val isSpace: Boolean, private val permissions: SecurityAndPrivacyPermissions, private val selectableJoinedSpaces: ImmutableSet, - private val spaceSelection: SpaceSelection, + private val spaceSelectionMode: SpaceSelectionMode, val eventSink: (SecurityAndPrivacyEvent) -> Unit ) { + val isSpaceMemberSelectable = isSpaceSettingsEnabled && spaceSelectionMode != SpaceSelectionMode.None + + // Show SpaceMember option in two cases: + // - the SpaceSettings FF is enabled + // - SpaceMember is the current saved value + val showSpaceMemberOption = savedSettings.roomAccess is SecurityAndPrivacyRoomAccess.SpaceMember || isSpaceMemberSelectable + + val showManageSpaceAction = spaceSelectionMode is SpaceSelectionMode.Multiple && editedSettings.roomAccess is SecurityAndPrivacyRoomAccess.SpaceMember + + val isAskToJoinSelectable = isKnockEnabled + + // Show Ask to join option in two cases: + // - the Knock FF is enabled + // - AskToJoin is the current saved value + val showAskToJoinOption = savedSettings.roomAccess == SecurityAndPrivacyRoomAccess.AskToJoin || isAskToJoinSelectable + val canBeSaved = savedSettings != editedSettings // Logic is in https://github.com/element-hq/element-meta/issues/3029 @@ -57,6 +77,32 @@ data class SecurityAndPrivacyState( val showHistoryVisibilitySection = permissions.canChangeHistoryVisibility && !isSpace val showEncryptionSection = permissions.canChangeEncryption && !isSpace + + @Composable + fun spaceMemberDescription(): String { + return if (isSpaceMemberSelectable) { + when (spaceSelectionMode) { + is SpaceSelectionMode.Single -> { + val spaceName = spaceSelectionMode.spaceRoom?.displayName ?: spaceSelectionMode.spaceId.value + stringResource(R.string.screen_security_and_privacy_room_access_space_members_option_single_parent_description, spaceName) + } + is SpaceSelectionMode.None, + is SpaceSelectionMode.Multiple -> stringResource(R.string.screen_security_and_privacy_room_access_space_members_option_multiple_parents_description) + } + } else { + stringResource(R.string.screen_security_and_privacy_room_access_space_members_option_unavailable_description) + } + } + + fun getAuthorizedSpaceData(): AuthorizedSpacesSelection { + return AuthorizedSpacesSelection( + joinedSpaces = selectableJoinedSpaces.toImmutableList(), + unknownSpaceIds = savedSettings.roomAccess.spaceIds().filter { spaceId -> + selectableJoinedSpaces.none { it.roomId == spaceId } + }.toImmutableList(), + initialSelectedIds = editedSettings.roomAccess.spaceIds().toImmutableList() + ) + } } data class SecurityAndPrivacySettings( @@ -85,10 +131,10 @@ enum class SecurityAndPrivacyHistoryVisibility { } } -sealed interface SpaceSelection { - data object None : SpaceSelection - data class Single(val spaceId: RoomId, val spaceRoom: SpaceRoom?) : SpaceSelection - data object Multiple : SpaceSelection +sealed interface SpaceSelectionMode { + data object None : SpaceSelectionMode + data class Single(val spaceId: RoomId, val spaceRoom: SpaceRoom?) : SpaceSelectionMode + data object Multiple : SpaceSelectionMode } sealed interface SecurityAndPrivacyRoomAccess { diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt index 312043324e..61218e8897 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt @@ -14,7 +14,6 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.spaces.SpaceRoom import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet open class SecurityAndPrivacyStateProvider : PreviewParameterProvider { @@ -122,7 +121,7 @@ fun aSecurityAndPrivacyState( isKnockEnabled: Boolean = true, isSpace: Boolean = false, selectableJoinedSpaces: Set = emptySet(), - spaceSelection: SpaceSelection = SpaceSelection.None, + spaceSelectionMode: SpaceSelectionMode = SpaceSelectionMode.None, isSpaceSettingsEnabled: Boolean = true, eventSink: (SecurityAndPrivacyEvent) -> Unit = {} ) = SecurityAndPrivacyState( @@ -135,7 +134,7 @@ fun aSecurityAndPrivacyState( permissions = permissions, isSpace = isSpace, selectableJoinedSpaces = selectableJoinedSpaces.toImmutableSet(), - spaceSelection = SpaceSelection.None, + spaceSelectionMode = spaceSelectionMode, isSpaceSettingsEnabled = isSpaceSettingsEnabled, eventSink = eventSink, ) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt index 374a4e3911..aedea1879c 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt @@ -90,14 +90,8 @@ fun SecurityAndPrivacyView( ) { if (state.showRoomAccessSection) { RoomAccessSection( + state = state, modifier = Modifier.padding(top = 24.dp), - edited = state.editedSettings.roomAccess, - saved = state.savedSettings.roomAccess, - isKnockEnabled = state.isKnockEnabled, - isSpaceSettingsEnabled = state.isSpaceSettingsEnabled, - onSelectOption = { state.eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(it)) }, - onManageSpacesClick = { state.eventSink(SecurityAndPrivacyEvent.ManageAuthorizedSpaces) }, - onSpaceMemberAccessClick = { state.eventSink(SecurityAndPrivacyEvent.SelectSpaceMemberAccess) } ) } if (state.showRoomVisibilitySections) { @@ -211,15 +205,25 @@ private fun SecurityAndPrivacySection( @Composable private fun RoomAccessSection( - edited: SecurityAndPrivacyRoomAccess, - saved: SecurityAndPrivacyRoomAccess, - isKnockEnabled: Boolean, - isSpaceSettingsEnabled: Boolean, - onSelectOption: (SecurityAndPrivacyRoomAccess) -> Unit, - onSpaceMemberAccessClick: () -> Unit, - onManageSpacesClick: () -> Unit, + state: SecurityAndPrivacyState, modifier: Modifier = Modifier, ) { + + val edited = state.editedSettings.roomAccess + val saved = state.savedSettings.roomAccess + + fun onSelectOption(option: SecurityAndPrivacyRoomAccess) { + state.eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(option)) + } + + fun onSpaceMemberAccessClick() { + state.eventSink(SecurityAndPrivacyEvent.SelectSpaceMemberAccess) + } + + fun onManageSpacesClick() { + state.eventSink(SecurityAndPrivacyEvent.ManageAuthorizedSpaces) + } + SecurityAndPrivacySection( title = stringResource(R.string.screen_security_and_privacy_room_access_section_header), modifier = modifier, @@ -231,31 +235,25 @@ private fun RoomAccessSection( leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Public())), onClick = { onSelectOption(SecurityAndPrivacyRoomAccess.Anyone) }, ) - // Show SpaceMember option in two cases: - // - the SpaceSettings FF is enabled - // - SpaceMember is the current saved value - if (saved is SecurityAndPrivacyRoomAccess.SpaceMember || isSpaceSettingsEnabled) + if (state.showSpaceMemberOption) ListItem( headlineContent = { Text(text = stringResource(R.string.screen_security_and_privacy_room_access_space_members_option_title)) }, supportingContent = { - Text(text = stringResource(R.string.screen_security_and_privacy_room_access_space_members_option_unavailable_description)) + Text(text = state.spaceMemberDescription()) }, - trailingContent = ListItemContent.RadioButton(selected = edited is SecurityAndPrivacyRoomAccess.SpaceMember), + trailingContent = ListItemContent.RadioButton(selected = state.editedSettings.roomAccess is SecurityAndPrivacyRoomAccess.SpaceMember), leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Space())), - onClick = onSpaceMemberAccessClick, - enabled = isSpaceSettingsEnabled, + onClick = ::onSpaceMemberAccessClick, + enabled = state.isSpaceMemberSelectable, ) - // Show Ask to join option in two cases: - // - the Knock FF is enabled - // - AskToJoin is the current saved value - if (saved == SecurityAndPrivacyRoomAccess.AskToJoin || isKnockEnabled) { + if (state.showAskToJoinOption) { ListItem( headlineContent = { Text(text = stringResource(R.string.screen_security_and_privacy_ask_to_join_option_title)) }, supportingContent = { Text(text = stringResource(R.string.screen_security_and_privacy_ask_to_join_option_description)) }, trailingContent = ListItemContent.RadioButton(selected = edited == SecurityAndPrivacyRoomAccess.AskToJoin), onClick = { onSelectOption(SecurityAndPrivacyRoomAccess.AskToJoin) }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserAdd())), - enabled = isKnockEnabled, + enabled = state.isAskToJoinSelectable, ) } ListItem( @@ -265,11 +263,11 @@ private fun RoomAccessSection( leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Lock())), onClick = { onSelectOption(SecurityAndPrivacyRoomAccess.InviteOnly) }, ) - if (edited is SecurityAndPrivacyRoomAccess.SpaceMember) { + if (state.showManageSpaceAction) { val footerText = stringWithLink( textRes = R.string.screen_security_and_privacy_room_access_footer, url = stringResource(R.string.screen_security_and_privacy_room_access_footer_manage_spaces_action), - onLinkClick = { onManageSpacesClick() }, + onLinkClick = {onManageSpacesClick()}, ) Text( text = footerText, From 75ab79162953a041570931c5c3d667cc909d0b34 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 7 Jan 2026 11:38:57 +0100 Subject: [PATCH 11/22] feature(security&privacy): working SpaceMember selection --- .../impl/SecurityAndPrivacyFlowNode.kt | 6 +-- .../impl/SecurityAndPrivacyNavigator.kt | 7 ++-- .../ManageAuthorizedSpacesNode.kt | 11 +---- .../ManageAuthorizedSpacesPresenter.kt | 41 ++++++------------- .../ManageAuthorizedSpacesState.kt | 4 +- .../ManageAuthorizedSpacesView.kt | 3 ++ .../impl/root/SecurityAndPrivacyNode.kt | 2 +- .../impl/root/SecurityAndPrivacyPresenter.kt | 8 ++-- .../impl/root/SecurityAndPrivacyState.kt | 9 ++-- 9 files changed, 37 insertions(+), 54 deletions(-) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt index d29a7ffd20..79c0c68ad3 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt @@ -66,7 +66,7 @@ class SecurityAndPrivacyFlowNode( data object EditRoomAddress : NavTarget @Parcelize - data class ManageAuthorizedSpaces(val initialSelection: List) : NavTarget + data object ManageAuthorizedSpaces: NavTarget } private val callback: SecurityAndPrivacyEntryPoint.Callback = callback() @@ -95,7 +95,7 @@ class SecurityAndPrivacyFlowNode( val authorizedSpacesData = securityAndPrivacyNode.getAuthorizedSpacesData() val selectedSpaces = manageAuthorizedSpacesNode.waitForCompletion(authorizedSpacesData) withContext(NonCancellable) { - backstack.pop() + navigator.closeManageAuthorizedSpaces() securityAndPrivacyNode.onAuthorizedSpacesSelected(selectedSpaces) } } @@ -110,7 +110,7 @@ class SecurityAndPrivacyFlowNode( NavTarget.EditRoomAddress -> { createNode(buildContext, plugins = listOf(navigator)) } - is NavTarget.ManageAuthorizedSpaces -> { + NavTarget.ManageAuthorizedSpaces -> { createNode(buildContext, plugins = listOf(navigator)) } } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyNavigator.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyNavigator.kt index da6ca379e8..274bf0b823 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyNavigator.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyNavigator.kt @@ -13,13 +13,12 @@ import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.operation.pop import com.bumble.appyx.navmodel.backstack.operation.push import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyEntryPoint -import io.element.android.libraries.matrix.api.core.RoomId interface SecurityAndPrivacyNavigator : Plugin { fun onDone() fun openEditRoomAddress() fun closeEditRoomAddress() - fun openManageAuthorizedSpaces(initialSelection: List) + fun openManageAuthorizedSpaces() fun closeManageAuthorizedSpaces() } @@ -39,8 +38,8 @@ class BackstackSecurityAndPrivacyNavigator( backStack.pop() } - override fun openManageAuthorizedSpaces(initialSelection: List) { - backStack.push(SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces(initialSelection)) + override fun openManageAuthorizedSpaces() { + backStack.push(SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces) } override fun closeManageAuthorizedSpaces() { diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt index 5608c0ce16..b8f7150d86 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt @@ -20,7 +20,6 @@ import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNavigator -import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.appyx.launchMolecule import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.RoomId @@ -32,16 +31,10 @@ import kotlinx.coroutines.flow.first class ManageAuthorizedSpacesNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, - presenterFactory: ManageAuthorizedSpacesPresenter.Factory, + presenter: ManageAuthorizedSpacesPresenter, ) : Node(buildContext, plugins = plugins) { - data class Params( - val initialSelection: List - ) : NodeInputs - private val navigator = plugins().first() - private val presenter = presenterFactory.create(navigator) - private val stateFlow = launchMolecule { presenter.present() } suspend fun waitForCompletion(data: AuthorizedSpacesSelection): ImmutableList { @@ -54,7 +47,7 @@ class ManageAuthorizedSpacesNode( val state by stateFlow.collectAsState() ManageAuthorizedSpacesView( state = state, - onBackClick = ::navigateUp, + onBackClick = { navigator.closeManageAuthorizedSpaces() }, modifier = modifier ) } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt index 09869cdf09..0679c72066 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt @@ -13,57 +13,42 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import dev.zacsweers.metro.Assisted -import dev.zacsweers.metro.AssistedFactory -import dev.zacsweers.metro.AssistedInject -import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNavigator +import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.room.JoinedRoom import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList -@AssistedInject -class ManageAuthorizedSpacesPresenter( - @Assisted private val navigator: SecurityAndPrivacyNavigator, - private val client: MatrixClient, - private val room: JoinedRoom, -) : Presenter { - @AssistedFactory - interface Factory { - fun create(navigator: SecurityAndPrivacyNavigator): ManageAuthorizedSpacesPresenter - } +@Inject +class ManageAuthorizedSpacesPresenter() : Presenter { @Composable override fun present(): ManageAuthorizedSpacesState { - var currentSelection: ImmutableList by remember { mutableStateOf(persistentListOf()) } - var spacesData by remember { mutableStateOf(AuthorizedSpacesSelection()) } + var selectedIds: ImmutableList by remember { mutableStateOf(persistentListOf()) } + var spacesSelection by remember { mutableStateOf(AuthorizedSpacesSelection()) } var isSelectionComplete by remember { mutableStateOf(false) } fun handleEvent(event: ManageAuthorizedSpacesEvent) { when (event) { - ManageAuthorizedSpacesEvent.Done -> { - isSelectionComplete = true - } + ManageAuthorizedSpacesEvent.Done ->isSelectionComplete = true is ManageAuthorizedSpacesEvent.ToggleSpace -> { - currentSelection = if (currentSelection.contains(event.roomId)) { - currentSelection.minus(event.roomId).toPersistentList() + selectedIds = if (selectedIds.contains(event.roomId)) { + selectedIds.minus(event.roomId).toPersistentList() } else { - currentSelection.plus(event.roomId).toPersistentList() + selectedIds.plus(event.roomId).toPersistentList() } } is ManageAuthorizedSpacesEvent.SetData -> { - spacesData = event.data - currentSelection = event.data.initialSelectedIds + spacesSelection = event.data + selectedIds = event.data.initialSelectedIds } } } return ManageAuthorizedSpacesState( - selection = spacesData, - selectedIds = currentSelection, + selection = spacesSelection, + selectedIds = selectedIds, isSelectionComplete = isSelectionComplete, eventSink = ::handleEvent, ) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt index 7564fabae9..291cd7c760 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt @@ -18,7 +18,9 @@ data class ManageAuthorizedSpacesState( val selectedIds: ImmutableList, val isSelectionComplete: Boolean, val eventSink: (ManageAuthorizedSpacesEvent) -> Unit -) +) { + val isDoneButtonEnabled = selectedIds.isNotEmpty() +} data class AuthorizedSpacesSelection( val joinedSpaces: ImmutableList = persistentListOf(), diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt index 1c4ff7bc77..022010f267 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt @@ -53,6 +53,7 @@ fun ManageAuthorizedSpacesView( onDoneClick = { state.eventSink(ManageAuthorizedSpacesEvent.Done) }, + isDoneButtonEnabled = state.isDoneButtonEnabled ) } ) { padding -> @@ -160,6 +161,7 @@ private fun CheckableSpaceListItem( @OptIn(ExperimentalMaterial3Api::class) @Composable private fun ManageAuthorizedSpacesTopBar( + isDoneButtonEnabled: Boolean, onBackClick: () -> Unit, onDoneClick: () -> Unit, modifier: Modifier = Modifier, @@ -170,6 +172,7 @@ private fun ManageAuthorizedSpacesTopBar( navigationIcon = { BackButton(onClick = onBackClick) }, actions = { TextButton( + enabled = isDoneButtonEnabled, text = stringResource(CommonStrings.action_done), onClick = onDoneClick, ) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyNode.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyNode.kt index 1e5e6e0943..8c41992e7a 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyNode.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyNode.kt @@ -47,7 +47,7 @@ class SecurityAndPrivacyNode( } fun getAuthorizedSpacesData(): AuthorizedSpacesSelection{ - return stateFlow.value.getAuthorizedSpaceData() + return stateFlow.value.getAuthorizedSpacesSelection() } fun onAuthorizedSpacesSelected(selectedSpaces: ImmutableList) { diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt index bc7688dab3..9e2bd6c375 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt @@ -118,7 +118,7 @@ class SecurityAndPrivacyPresenter( address = savedSettings.address, ) - val selectableJoinedSpaces by produceState(persistentSetOf()) { + val selectableJoinedSpaces by produceState(initialValue = persistentSetOf(), key1 = savedSettings.roomAccess.spaceIds()) { val joinedParentSpaces = matrixClient .spaceService .joinedParents(room.roomId) @@ -193,7 +193,7 @@ class SecurityAndPrivacyPresenter( saveAction.value = AsyncAction.Uninitialized } SecurityAndPrivacyEvent.ManageAuthorizedSpaces -> { - navigator.openManageAuthorizedSpaces(editedSettings.roomAccess.spaceIds()) + navigator.openManageAuthorizedSpaces() } SecurityAndPrivacyEvent.SelectSpaceMemberAccess -> handleSpaceMemberAccessSelection( spaceSelectionMode = spaceSelectionMode, @@ -254,9 +254,7 @@ class SecurityAndPrivacyPresenter( } when (spaceSelectionMode) { is SpaceSelectionMode.None -> Unit - is SpaceSelectionMode.Multiple -> navigator.openManageAuthorizedSpaces( - initialSelection = spaceIds , - ) + is SpaceSelectionMode.Multiple -> navigator.openManageAuthorizedSpaces() is SpaceSelectionMode.Single -> { val newRoomAccess = SecurityAndPrivacyRoomAccess.SpaceMember( spaceIds = persistentListOf(spaceSelectionMode.spaceId) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt index d14d83b16b..d74ecb13d9 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt @@ -42,8 +42,8 @@ data class SecurityAndPrivacyState( val isSpaceMemberSelectable = isSpaceSettingsEnabled && spaceSelectionMode != SpaceSelectionMode.None // Show SpaceMember option in two cases: - // - the SpaceSettings FF is enabled // - SpaceMember is the current saved value + // - SpaceMember option is selectable (ie. the FF is enabled and there is at least one space to select) val showSpaceMemberOption = savedSettings.roomAccess is SecurityAndPrivacyRoomAccess.SpaceMember || isSpaceMemberSelectable val showManageSpaceAction = spaceSelectionMode is SpaceSelectionMode.Multiple && editedSettings.roomAccess is SecurityAndPrivacyRoomAccess.SpaceMember @@ -94,13 +94,16 @@ data class SecurityAndPrivacyState( } } - fun getAuthorizedSpaceData(): AuthorizedSpacesSelection { + fun getAuthorizedSpacesSelection(): AuthorizedSpacesSelection { return AuthorizedSpacesSelection( joinedSpaces = selectableJoinedSpaces.toImmutableList(), unknownSpaceIds = savedSettings.roomAccess.spaceIds().filter { spaceId -> selectableJoinedSpaces.none { it.roomId == spaceId } }.toImmutableList(), - initialSelectedIds = editedSettings.roomAccess.spaceIds().toImmutableList() + initialSelectedIds = when (editedSettings.roomAccess) { + is SecurityAndPrivacyRoomAccess.SpaceMember -> editedSettings.roomAccess.spaceIds + else -> savedSettings.roomAccess.spaceIds() + } ) } } From 92acf1edeaa5601b514a34bee39d49fd85e2511f Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 7 Jan 2026 17:05:09 +0100 Subject: [PATCH 12/22] feature(security&privacy): support KnockRestricted join rule --- .../impl/SecurityAndPrivacyFlowNode.kt | 8 ++-- .../impl/SecurityAndPrivacyNavigator.kt | 6 +-- .../impl/root/SecurityAndPrivacyEvent.kt | 2 + .../impl/root/SecurityAndPrivacyNode.kt | 11 +++-- .../impl/root/SecurityAndPrivacyPresenter.kt | 37 +++++++++++++++- .../impl/root/SecurityAndPrivacyState.kt | 42 ++++++++++++++++--- .../root/SecurityAndPrivacyStateProvider.kt | 13 ++++++ .../impl/root/SecurityAndPrivacyView.kt | 19 ++++++++- .../impl/FakeSecurityAndPrivacyNavigator.kt | 10 +++++ 9 files changed, 128 insertions(+), 20 deletions(-) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt index 79c0c68ad3..f7bdde7aa0 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt @@ -19,6 +19,7 @@ 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.activeElement import com.bumble.appyx.navmodel.backstack.operation.pop import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject @@ -66,7 +67,7 @@ class SecurityAndPrivacyFlowNode( data object EditRoomAddress : NavTarget @Parcelize - data object ManageAuthorizedSpaces: NavTarget + data class ManageAuthorizedSpaces(val forKnockRestricted: Boolean = false) : NavTarget } private val callback: SecurityAndPrivacyEntryPoint.Callback = callback() @@ -94,9 +95,10 @@ class SecurityAndPrivacyFlowNode( commonLifecycle.coroutineScope.launch { val authorizedSpacesData = securityAndPrivacyNode.getAuthorizedSpacesData() val selectedSpaces = manageAuthorizedSpacesNode.waitForCompletion(authorizedSpacesData) + val forKnock = (backstack.activeElement as? NavTarget.ManageAuthorizedSpaces)?.forKnockRestricted ?: false withContext(NonCancellable) { navigator.closeManageAuthorizedSpaces() - securityAndPrivacyNode.onAuthorizedSpacesSelected(selectedSpaces) + securityAndPrivacyNode.onAuthorizedSpacesSelected(selectedSpaces, forKnock = forKnock) } } } @@ -110,7 +112,7 @@ class SecurityAndPrivacyFlowNode( NavTarget.EditRoomAddress -> { createNode(buildContext, plugins = listOf(navigator)) } - NavTarget.ManageAuthorizedSpaces -> { + is NavTarget.ManageAuthorizedSpaces -> { createNode(buildContext, plugins = listOf(navigator)) } } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyNavigator.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyNavigator.kt index 274bf0b823..9690c915d5 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyNavigator.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyNavigator.kt @@ -18,7 +18,7 @@ interface SecurityAndPrivacyNavigator : Plugin { fun onDone() fun openEditRoomAddress() fun closeEditRoomAddress() - fun openManageAuthorizedSpaces() + fun openManageAuthorizedSpaces(forKnockRestricted: Boolean = false) fun closeManageAuthorizedSpaces() } @@ -38,8 +38,8 @@ class BackstackSecurityAndPrivacyNavigator( backStack.pop() } - override fun openManageAuthorizedSpaces() { - backStack.push(SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces) + override fun openManageAuthorizedSpaces(forKnockRestricted: Boolean) { + backStack.push(SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces(forKnockRestricted)) } override fun closeManageAuthorizedSpaces() { diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyEvent.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyEvent.kt index 0c56a834de..25045a72c1 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyEvent.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyEvent.kt @@ -17,6 +17,8 @@ sealed interface SecurityAndPrivacyEvent { data class ChangeRoomAccess(val roomAccess: SecurityAndPrivacyRoomAccess) : SecurityAndPrivacyEvent // Special case for "Space Members" data object SelectSpaceMemberAccess : SecurityAndPrivacyEvent + // Special case for "Ask to join with Space Members" + data object SelectAskToJoinWithSpaceMembersAccess : SecurityAndPrivacyEvent data object ToggleEncryptionState : SecurityAndPrivacyEvent data object CancelEnableEncryption : SecurityAndPrivacyEvent data object ConfirmEnableEncryption : SecurityAndPrivacyEvent diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyNode.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyNode.kt index 8c41992e7a..7d3c115be5 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyNode.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyNode.kt @@ -50,10 +50,13 @@ class SecurityAndPrivacyNode( return stateFlow.value.getAuthorizedSpacesSelection() } - fun onAuthorizedSpacesSelected(selectedSpaces: ImmutableList) { - stateFlow.value.eventSink( - SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.SpaceMember(selectedSpaces)) - ) + fun onAuthorizedSpacesSelected(selectedSpaces: ImmutableList, forKnock: Boolean) { + val roomAccess = if (forKnock) { + SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember(selectedSpaces) + } else { + SecurityAndPrivacyRoomAccess.SpaceMember(selectedSpaces) + } + stateFlow.value.eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(roomAccess)) } @Composable diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt index 9e2bd6c375..a27884a1ab 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt @@ -200,6 +200,10 @@ class SecurityAndPrivacyPresenter( spaceIds = editedSettings.roomAccess.spaceIds(), editedAccess = editedRoomAccess, ) + SecurityAndPrivacyEvent.SelectAskToJoinWithSpaceMembersAccess -> handleAskToJoinWithSpaceMembersAccessSelection( + spaceSelectionMode = spaceSelectionMode, + editedAccess = editedRoomAccess, + ) } } @@ -254,7 +258,7 @@ class SecurityAndPrivacyPresenter( } when (spaceSelectionMode) { is SpaceSelectionMode.None -> Unit - is SpaceSelectionMode.Multiple -> navigator.openManageAuthorizedSpaces() + is SpaceSelectionMode.Multiple -> navigator.openManageAuthorizedSpaces(forKnockRestricted = false) is SpaceSelectionMode.Single -> { val newRoomAccess = SecurityAndPrivacyRoomAccess.SpaceMember( spaceIds = persistentListOf(spaceSelectionMode.spaceId) @@ -264,6 +268,25 @@ class SecurityAndPrivacyPresenter( } } + private fun handleAskToJoinWithSpaceMembersAccessSelection( + spaceSelectionMode: SpaceSelectionMode, + editedAccess: MutableState, + ) { + if (editedAccess.value is SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember) { + return + } + when (spaceSelectionMode) { + is SpaceSelectionMode.None -> Unit + is SpaceSelectionMode.Multiple -> navigator.openManageAuthorizedSpaces(forKnockRestricted = true) + is SpaceSelectionMode.Single -> { + val newRoomAccess = SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember( + spaceIds = persistentListOf(spaceSelectionMode.spaceId) + ) + editedAccess.value = newRoomAccess + } + } + } + private fun getSpaceSelectionMode( selectableJoinedSpaces: Set, savedAccess: SecurityAndPrivacyRoomAccess, @@ -328,6 +351,7 @@ class SecurityAndPrivacyPresenter( // the room should be automatically made invisible (private) in the room directory. val editedIsVisibleInRoomDirectory = when (editedSettings.roomAccess) { SecurityAndPrivacyRoomAccess.AskToJoin, + is SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember, SecurityAndPrivacyRoomAccess.Anyone -> editedSettings.isVisibleInRoomDirectory.dataOrNull() else -> false } @@ -365,7 +389,13 @@ class SecurityAndPrivacyPresenter( private fun JoinRule?.map(): SecurityAndPrivacyRoomAccess { return when (this) { JoinRule.Public -> SecurityAndPrivacyRoomAccess.Anyone - JoinRule.Knock, is JoinRule.KnockRestricted -> SecurityAndPrivacyRoomAccess.AskToJoin + JoinRule.Knock -> SecurityAndPrivacyRoomAccess.AskToJoin + is JoinRule.KnockRestricted -> SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember( + spaceIds = this.rules + .filterIsInstance() + .map { it.roomId } + .toImmutableList() + ) is JoinRule.Restricted -> SecurityAndPrivacyRoomAccess.SpaceMember( spaceIds = this.rules .filterIsInstance() @@ -388,6 +418,9 @@ private fun SecurityAndPrivacyRoomAccess.map(): JoinRule? { is SecurityAndPrivacyRoomAccess.SpaceMember -> JoinRule.Restricted( rules = this.spaceIds.map { AllowRule.RoomMembership(it) }.toImmutableList() ) + is SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember -> JoinRule.KnockRestricted( + rules = this.spaceIds.map { AllowRule.RoomMembership(it) }.toImmutableList() + ) } } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt index d74ecb13d9..9d47e57846 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt @@ -46,14 +46,25 @@ data class SecurityAndPrivacyState( // - SpaceMember option is selectable (ie. the FF is enabled and there is at least one space to select) val showSpaceMemberOption = savedSettings.roomAccess is SecurityAndPrivacyRoomAccess.SpaceMember || isSpaceMemberSelectable - val showManageSpaceAction = spaceSelectionMode is SpaceSelectionMode.Multiple && editedSettings.roomAccess is SecurityAndPrivacyRoomAccess.SpaceMember + val showManageSpaceFooter = spaceSelectionMode is SpaceSelectionMode.Multiple && + (editedSettings.roomAccess is SecurityAndPrivacyRoomAccess.SpaceMember || + editedSettings.roomAccess is SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember) val isAskToJoinSelectable = isKnockEnabled - // Show Ask to join option in two cases: - // - the Knock FF is enabled - // - AskToJoin is the current saved value - val showAskToJoinOption = savedSettings.roomAccess == SecurityAndPrivacyRoomAccess.AskToJoin || isAskToJoinSelectable + val isAskToJoinWithSpaceMembersSelectable = isAskToJoinSelectable && isSpaceMemberSelectable + + // Show Ask to join option only when: + // - AskToJoin is the current saved value (legacy), OR + // - Knock FF enabled BUT (SpaceSettings FF disabled OR no spaces available) + val showAskToJoinOption = savedSettings.roomAccess == SecurityAndPrivacyRoomAccess.AskToJoin || + (isAskToJoinSelectable && !isAskToJoinWithSpaceMembersSelectable) + + // Show AskToJoinWithSpaceMember option when: + // - It's the current saved value, OR + // - Both FFs enabled AND spaces available + val showAskToJoinWithSpaceMemberOption = savedSettings.roomAccess is SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember || + isAskToJoinWithSpaceMembersSelectable val canBeSaved = savedSettings != editedSettings @@ -94,6 +105,22 @@ data class SecurityAndPrivacyState( } } + @Composable + fun askToJoinWithSpaceMembersDescription(): String { + return if (isAskToJoinWithSpaceMembersSelectable) { + when (spaceSelectionMode) { + is SpaceSelectionMode.Single -> { + val spaceName = spaceSelectionMode.spaceRoom?.displayName ?: spaceSelectionMode.spaceId.value + stringResource(R.string.screen_security_and_privacy_ask_to_join_single_space_members_option_description, spaceName) + } + is SpaceSelectionMode.None, + is SpaceSelectionMode.Multiple -> stringResource(R.string.screen_security_and_privacy_ask_to_join_multiple_spaces_members_option_description) + } + } else { + stringResource(R.string.screen_security_and_privacy_ask_to_join_option_description) + } + } + fun getAuthorizedSpacesSelection(): AuthorizedSpacesSelection { return AuthorizedSpacesSelection( joinedSpaces = selectableJoinedSpaces.toImmutableList(), @@ -102,6 +129,7 @@ data class SecurityAndPrivacyState( }.toImmutableList(), initialSelectedIds = when (editedSettings.roomAccess) { is SecurityAndPrivacyRoomAccess.SpaceMember -> editedSettings.roomAccess.spaceIds + is SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember -> editedSettings.roomAccess.spaceIds else -> savedSettings.roomAccess.spaceIds() } ) @@ -145,17 +173,19 @@ sealed interface SecurityAndPrivacyRoomAccess { data object AskToJoin : SecurityAndPrivacyRoomAccess data object Anyone : SecurityAndPrivacyRoomAccess data class SpaceMember(val spaceIds: ImmutableList) : SecurityAndPrivacyRoomAccess + data class AskToJoinWithSpaceMember(val spaceIds: ImmutableList) : SecurityAndPrivacyRoomAccess fun canConfigureRoomVisibility(): Boolean { return when (this) { InviteOnly, is SpaceMember -> false - AskToJoin, Anyone -> true + AskToJoin, Anyone, is AskToJoinWithSpaceMember -> true } } fun spaceIds(): ImmutableList { return when (this) { is SpaceMember -> spaceIds + is AskToJoinWithSpaceMember -> spaceIds else -> persistentListOf() } } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt index 61218e8897..d253021143 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt @@ -69,6 +69,19 @@ private fun commonSecurityAndPrivacyStates(isSpace: Boolean): Sequence Unit = { lambdaError() }, private val openEditRoomAddressLambda: () -> Unit = { lambdaError() }, private val closeEditRoomAddressLambda: () -> Unit = { lambdaError() }, + private val openManageAuthorizedSpacesLambda: (Boolean) -> Unit = { lambdaError() }, + private val closeManageAuthorizedSpacesLambda: () -> Unit = { lambdaError() }, ) : SecurityAndPrivacyNavigator { override fun onDone() { onDoneLambda() @@ -26,4 +28,12 @@ class FakeSecurityAndPrivacyNavigator( override fun closeEditRoomAddress() { closeEditRoomAddressLambda() } + + override fun openManageAuthorizedSpaces(forKnockRestricted: Boolean) { + openManageAuthorizedSpacesLambda(forKnockRestricted) + } + + override fun closeManageAuthorizedSpaces() { + closeManageAuthorizedSpacesLambda() + } } From cbb91500f32d88952dbec76474e0cb4c117a2da2 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 7 Jan 2026 20:12:41 +0100 Subject: [PATCH 13/22] quality: add bunch of tests for Security&Privacy new features --- .../impl/SecurityAndPrivacyFlowNode.kt | 5 +- .../ManageAuthorizedSpacesStateProvider.kt | 22 +- .../impl/SecurityAndPrivacyFlowNodeTest.kt | 120 ++++++ .../impl/SecurityAndPrivacyPresenterTest.kt | 376 +++++++++++++++++- .../impl/SecurityAndPrivacyViewTest.kt | 45 +++ .../ManageAuthorizedSpacesPresenterTest.kt | 96 +++++ .../ManageAuthorizedSpacesViewTest.kt | 109 +++++ 7 files changed, 752 insertions(+), 21 deletions(-) create mode 100644 features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNodeTest.kt create mode 100644 features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenterTest.kt create mode 100644 features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesViewTest.kt diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt index f7bdde7aa0..2c21964f48 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt @@ -9,6 +9,7 @@ package io.element.android.features.securityandprivacy.impl import android.os.Parcelable +import androidx.annotation.VisibleForTesting import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.lifecycle.Lifecycle @@ -71,7 +72,9 @@ class SecurityAndPrivacyFlowNode( } private val callback: SecurityAndPrivacyEntryPoint.Callback = callback() - private val navigator = BackstackSecurityAndPrivacyNavigator(callback, backstack) + + @VisibleForTesting + val navigator = BackstackSecurityAndPrivacyNavigator(callback, backstack) override fun onBuilt() { super.onBuilt() diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt index e232d02022..2e062c186a 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt @@ -20,14 +20,14 @@ open class ManageAuthorizedSpacesStateProvider : PreviewParameterProvider { } } -fun anAuthorizedSpacesData( +fun anAuthorizedSpaceSelection( joinedSpaces: List = aSpaceRoomList(5), unknownSpaceIds: List = emptyList(), - initialSelection: List = emptyList(), + initialSelectedIds: List = emptyList(), ) = AuthorizedSpacesSelection( joinedSpaces = joinedSpaces.toImmutableList(), unknownSpaceIds = unknownSpaceIds.toImmutableList(), - initialSelectedIds = initialSelection.toImmutableList(), + initialSelectedIds = initialSelectedIds.toImmutableList(), ) private fun aManageAuthorizedSpacesState( - authorizedSpacesData: AuthorizedSpacesSelection = anAuthorizedSpacesData(), - currentSelection: List = emptyList(), + authorizedSpacesSelection: AuthorizedSpacesSelection = anAuthorizedSpaceSelection(), + selectedIds: List = emptyList(), eventSink: (ManageAuthorizedSpacesEvent) -> Unit = {}, ) = ManageAuthorizedSpacesState( - selection = authorizedSpacesData, - selectedIds = currentSelection.toImmutableList(), + selection = authorizedSpacesSelection, + selectedIds = selectedIds.toImmutableList(), isSelectionComplete = false, eventSink = eventSink, ) diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNodeTest.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNodeTest.kt new file mode 100644 index 0000000000..cc32b59684 --- /dev/null +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNodeTest.kt @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * 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.securityandprivacy.impl + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.bumble.appyx.core.modality.AncestryInfo +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.activeElement +import com.bumble.appyx.utils.customisations.NodeCustomisationDirectoryImpl +import com.google.common.truth.Truth.assertThat +import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyEntryPoint +import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility +import io.element.android.libraries.matrix.api.room.join.JoinRule +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.room.aRoomInfo +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SecurityAndPrivacyFlowNodeTest { + + @Test + fun `initial backstack contains SecurityAndPrivacy`() = runTest { + val flowNode = createFlowNode() + assertThat(flowNode.currentNavTarget()).isEqualTo(SecurityAndPrivacyFlowNode.NavTarget.SecurityAndPrivacy) + } + + @Test + fun `openEditRoomAddress navigates to EditRoomAddress`() = runTest { + val flowNode = createFlowNode() + flowNode.navigator.openEditRoomAddress() + assertThat(flowNode.currentNavTarget()).isEqualTo(SecurityAndPrivacyFlowNode.NavTarget.EditRoomAddress) + } + + @Test + fun `closeEditRoomAddress pops backstack`() = runTest { + val flowNode = createFlowNode() + flowNode.navigator.openEditRoomAddress() + assertThat(flowNode.currentNavTarget()).isEqualTo(SecurityAndPrivacyFlowNode.NavTarget.EditRoomAddress) + flowNode.navigator.closeEditRoomAddress() + assertThat(flowNode.currentNavTarget()).isEqualTo(SecurityAndPrivacyFlowNode.NavTarget.SecurityAndPrivacy) + } + + @Test + fun `openManageAuthorizedSpaces navigates with forKnockRestricted false`() = runTest { + val flowNode = createFlowNode() + flowNode.navigator.openManageAuthorizedSpaces(forKnockRestricted = false) + assertThat(flowNode.currentNavTarget()).isEqualTo( + SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces(forKnockRestricted = false) + ) + } + + @Test + fun `openManageAuthorizedSpaces navigates with forKnockRestricted true`() = runTest { + val flowNode = createFlowNode() + flowNode.navigator.openManageAuthorizedSpaces(forKnockRestricted = true) + assertThat(flowNode.currentNavTarget()).isEqualTo( + SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces(forKnockRestricted = true) + ) + } + + @Test + fun `closeManageAuthorizedSpaces pops backstack`() = runTest { + val flowNode = createFlowNode() + flowNode.navigator.openManageAuthorizedSpaces(forKnockRestricted = false) + assertThat(flowNode.currentNavTarget()) + .isInstanceOf(SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces::class.java) + flowNode.navigator.closeManageAuthorizedSpaces() + assertThat(flowNode.currentNavTarget()).isEqualTo(SecurityAndPrivacyFlowNode.NavTarget.SecurityAndPrivacy) + } + + @Test + fun `onDone invokes callback`() = runTest { + var onDoneCalled = false + val callback = object : SecurityAndPrivacyEntryPoint.Callback { + override fun onDone() { + onDoneCalled = true + } + } + val flowNode = createFlowNode(callback = callback) + flowNode.navigator.onDone() + assertThat(onDoneCalled).isTrue() + } + + private fun createFlowNode( + callback: SecurityAndPrivacyEntryPoint.Callback = object : SecurityAndPrivacyEntryPoint.Callback { + override fun onDone() {} + }, + ): SecurityAndPrivacyFlowNode { + val buildContext = BuildContext( + ancestryInfo = AncestryInfo.Root, + savedStateMap = null, + customisations = NodeCustomisationDirectoryImpl() + ) + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + initialRoomInfo = aRoomInfo( + joinRule = JoinRule.Invite, + historyVisibility = RoomHistoryVisibility.Shared + ) + ) + ) + return SecurityAndPrivacyFlowNode( + buildContext = buildContext, + plugins = listOf(callback), + room = room, + ) + } + + private fun SecurityAndPrivacyFlowNode.currentNavTarget() = backstack.activeElement +} diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyPresenterTest.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyPresenterTest.kt index c035b5510d..9a166e4daf 100644 --- a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyPresenterTest.kt +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyPresenterTest.kt @@ -18,21 +18,29 @@ import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility +import io.element.android.libraries.matrix.api.room.join.AllowRule import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility import io.element.android.libraries.matrix.test.A_ROOM_ALIAS +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.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions +import io.element.android.libraries.matrix.test.spaces.FakeSpaceService +import io.element.android.libraries.previewutils.room.aSpaceRoom import io.element.android.tests.testutils.lambda.assert import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.test +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.test.runTest import org.junit.Test @@ -50,7 +58,6 @@ class SecurityAndPrivacyPresenterTest { assertThat(showRoomVisibilitySections).isFalse() assertThat(showHistoryVisibilitySection).isFalse() assertThat(showEncryptionSection).isFalse() - assertThat(isKnockEnabled).isFalse() } with(awaitItem()) { assertThat(editedSettings).isEqualTo(savedSettings) @@ -61,7 +68,6 @@ class SecurityAndPrivacyPresenterTest { assertThat(showRoomVisibilitySections).isFalse() assertThat(showHistoryVisibilitySection).isTrue() assertThat(showEncryptionSection).isTrue() - assertThat(isKnockEnabled).isFalse() } } } @@ -364,17 +370,364 @@ class SecurityAndPrivacyPresenterTest { } @Test - fun `present - isKnockEnabled is true if the Knock feature flag is enabled`() = runTest { + fun `present - Restricted join rule maps to SpaceMember`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + initialRoomInfo = aRoomInfo( + joinRule = JoinRule.Restricted( + rules = persistentListOf(AllowRule.RoomMembership(A_ROOM_ID)) + ), + historyVisibility = RoomHistoryVisibility.Shared, + ) + ) + ) + val presenter = createSecurityAndPrivacyPresenter(room = room) + presenter.test { + skipItems(1) + with(awaitItem()) { + assertThat(editedSettings.roomAccess).isInstanceOf(SecurityAndPrivacyRoomAccess.SpaceMember::class.java) + val access = editedSettings.roomAccess as SecurityAndPrivacyRoomAccess.SpaceMember + assertThat(access.spaceIds).containsExactly(A_ROOM_ID) + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - SelectSpaceMemberAccess with single space auto-selects`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + initialRoomInfo = aRoomInfo( + historyVisibility = RoomHistoryVisibility.Shared, + joinRule = JoinRule.Invite + ) + ) + ) + val client = FakeMatrixClient( + userIdServerNameLambda = { "matrix.org" }, + spaceService = FakeSpaceService( + joinedParentsResult = { _ -> + Result.success(listOf(aSpaceRoom(roomId = A_ROOM_ID))) + } + ) + ) val presenter = createSecurityAndPrivacyPresenter( + room = room, + matrixClient = client, featureFlagService = FakeFeatureFlagService( initialState = mapOf( - FeatureFlags.Knock.key to true, + FeatureFlags.SpaceSettings.key to true, ) ) ) presenter.test { - assertThat(awaitItem().isKnockEnabled).isFalse() - assertThat(awaitItem().isKnockEnabled).isTrue() + skipItems(1) + val state = awaitItem() + assertThat(state.isSpaceMemberSelectable).isTrue() + state.eventSink(SecurityAndPrivacyEvent.SelectSpaceMemberAccess) + with(awaitItem()) { + assertThat(editedSettings.roomAccess).isInstanceOf(SecurityAndPrivacyRoomAccess.SpaceMember::class.java) + val access = editedSettings.roomAccess as SecurityAndPrivacyRoomAccess.SpaceMember + assertThat(access.spaceIds).containsExactly(A_ROOM_ID) + } + } + } + + @Test + fun `present - SelectSpaceMemberAccess with multiple spaces opens ManageAuthorizedSpaces`() = runTest { + val openManageAuthorizedSpacesLambda = lambdaRecorder { } + val navigator = FakeSecurityAndPrivacyNavigator(openManageAuthorizedSpacesLambda = openManageAuthorizedSpacesLambda) + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + initialRoomInfo = aRoomInfo( + historyVisibility = RoomHistoryVisibility.Shared, + joinRule = JoinRule.Invite + ) + ) + ) + val client = FakeMatrixClient( + userIdServerNameLambda = { "matrix.org" }, + spaceService = FakeSpaceService( + joinedParentsResult = { _ -> + Result.success(listOf(aSpaceRoom(roomId = A_ROOM_ID), aSpaceRoom(roomId = RoomId("!space2:matrix.org")))) + } + ) + ) + val presenter = createSecurityAndPrivacyPresenter( + room = room, + navigator = navigator, + matrixClient = client, + featureFlagService = FakeFeatureFlagService( + initialState = mapOf( + FeatureFlags.SpaceSettings.key to true, + ) + ) + ) + presenter.test { + skipItems(1) + val state = awaitItem() + assertThat(state.isSpaceMemberSelectable).isTrue() + state.eventSink(SecurityAndPrivacyEvent.SelectSpaceMemberAccess) + assert(openManageAuthorizedSpacesLambda).isCalledOnce().with(value(false)) + } + } + + @Test + fun `present - SpaceMember saves as Restricted join rule`() = runTest { + val updateJoinRuleLambda = lambdaRecorder> { Result.success(Unit) } + val updateRoomVisibilityLambda = lambdaRecorder> { Result.success(Unit) } + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + getRoomVisibilityResult = { Result.success(RoomVisibility.Private) }, + initialRoomInfo = aRoomInfo( + historyVisibility = RoomHistoryVisibility.Shared, + joinRule = JoinRule.Invite + ) + ), + updateJoinRuleResult = updateJoinRuleLambda, + updateRoomVisibilityResult = updateRoomVisibilityLambda, + ) + val onDoneLambda = lambdaRecorder { } + val navigator = FakeSecurityAndPrivacyNavigator(onDoneLambda = onDoneLambda) + val presenter = createSecurityAndPrivacyPresenter(room = room, navigator = navigator) + presenter.test { + skipItems(1) + with(awaitItem()) { + assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly) + val spaceMemberAccess = SecurityAndPrivacyRoomAccess.SpaceMember( + spaceIds = persistentListOf(A_ROOM_ID) + ) + eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(spaceMemberAccess)) + } + with(awaitItem()) { + assertThat(editedSettings.roomAccess).isInstanceOf(SecurityAndPrivacyRoomAccess.SpaceMember::class.java) + assertThat(canBeSaved).isTrue() + eventSink(SecurityAndPrivacyEvent.Save) + } + with(awaitItem()) { + assertThat(saveAction).isEqualTo(AsyncAction.Loading) + } + room.givenRoomInfo( + aRoomInfo( + joinRule = JoinRule.Restricted( + rules = persistentListOf(AllowRule.RoomMembership(A_ROOM_ID)) + ), + historyVisibility = RoomHistoryVisibility.Shared, + ) + ) + skipItems(2) + with(awaitItem()) { + assertThat(saveAction).isEqualTo(AsyncAction.Success(Unit)) + } + assert(updateJoinRuleLambda).isCalledOnce().with( + value(JoinRule.Restricted(rules = persistentListOf(AllowRule.RoomMembership(A_ROOM_ID)))) + ) + onDoneLambda.assertions().isCalledOnce() + } + } + + @Test + fun `present - room visibility is NOT configurable for SpaceMember`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + getRoomVisibilityResult = { Result.success(RoomVisibility.Private) }, + initialRoomInfo = aRoomInfo( + historyVisibility = RoomHistoryVisibility.Shared, + joinRule = JoinRule.Restricted( + rules = persistentListOf(AllowRule.RoomMembership(A_ROOM_ID)) + ) + ) + ) + ) + val presenter = createSecurityAndPrivacyPresenter(room = room) + presenter.test { + skipItems(1) + with(awaitItem()) { + assertThat(editedSettings.roomAccess).isInstanceOf(SecurityAndPrivacyRoomAccess.SpaceMember::class.java) + assertThat(showRoomVisibilitySections).isFalse() + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - KnockRestricted join rule maps to AskToJoinWithSpaceMembers`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + initialRoomInfo = aRoomInfo( + joinRule = JoinRule.KnockRestricted( + rules = persistentListOf(AllowRule.RoomMembership(A_ROOM_ID)) + ), + historyVisibility = RoomHistoryVisibility.Shared, + ) + ) + ) + val presenter = createSecurityAndPrivacyPresenter(room = room) + presenter.test { + skipItems(1) + with(awaitItem()) { + assertThat(editedSettings.roomAccess).isInstanceOf(SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember::class.java) + val access = editedSettings.roomAccess as SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember + assertThat(access.spaceIds).containsExactly(A_ROOM_ID) + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - showAskToJoinWithSpaceMembersOption is true when both FFs enabled and spaces available`() = runTest { + val presenter = createSecurityAndPrivacyPresenter( + featureFlagService = FakeFeatureFlagService( + initialState = mapOf( + FeatureFlags.Knock.key to true, + FeatureFlags.SpaceSettings.key to true, + ) + ) + ) + presenter.test { + skipItems(1) + // Without spaces available, AskToJoinWithSpaceMembers should not be selectable + with(awaitItem()) { + assertThat(isAskToJoinWithSpaceMembersSelectable).isFalse() + assertThat(showAskToJoinWithSpaceMemberOption).isFalse() + // AskToJoin should be shown instead + assertThat(showAskToJoinOption).isTrue() + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - SelectAskToJoinWithSpaceMembersAccess with multiple spaces opens ManageAuthorizedSpaces`() = runTest { + val openManageAuthorizedSpacesLambda = lambdaRecorder { } + val navigator = FakeSecurityAndPrivacyNavigator(openManageAuthorizedSpacesLambda = openManageAuthorizedSpacesLambda) + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + initialRoomInfo = aRoomInfo( + historyVisibility = RoomHistoryVisibility.Shared, + joinRule = JoinRule.Invite + ) + ) + ) + val client = FakeMatrixClient( + userIdServerNameLambda = { "matrix.org" }, + spaceService = FakeSpaceService( + joinedParentsResult = { _ -> + Result.success(listOf(aSpaceRoom(roomId = A_ROOM_ID), aSpaceRoom(roomId = RoomId("!space2:matrix.org")))) + } + ) + ) + val presenter = createSecurityAndPrivacyPresenter( + room = room, + navigator = navigator, + matrixClient = client, + featureFlagService = FakeFeatureFlagService( + initialState = mapOf( + FeatureFlags.Knock.key to true, + FeatureFlags.SpaceSettings.key to true, + ) + ) + ) + presenter.test { + skipItems(1) + // Wait for space selection mode to be set + val state = awaitItem() + assertThat(state.isAskToJoinWithSpaceMembersSelectable).isTrue() + state.eventSink(SecurityAndPrivacyEvent.SelectAskToJoinWithSpaceMembersAccess) + assert(openManageAuthorizedSpacesLambda).isCalledOnce().with(value(true)) + } + } + + @Test + fun `present - AskToJoinWithSpaceMember saves as KnockRestricted join rule`() = runTest { + val updateJoinRuleLambda = lambdaRecorder> { Result.success(Unit) } + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + getRoomVisibilityResult = { Result.success(RoomVisibility.Private) }, + initialRoomInfo = aRoomInfo( + historyVisibility = RoomHistoryVisibility.Shared, + joinRule = JoinRule.Invite + ) + ), + updateJoinRuleResult = updateJoinRuleLambda, + ) + val onDoneLambda = lambdaRecorder { } + val navigator = FakeSecurityAndPrivacyNavigator(onDoneLambda = onDoneLambda) + val presenter = createSecurityAndPrivacyPresenter(room = room, navigator = navigator) + presenter.test { + skipItems(1) + with(awaitItem()) { + assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly) + val askToJoinAccess = SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember( + spaceIds = persistentListOf(A_ROOM_ID) + ) + eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(askToJoinAccess)) + } + with(awaitItem()) { + assertThat(editedSettings.roomAccess).isInstanceOf(SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember::class.java) + assertThat(canBeSaved).isTrue() + eventSink(SecurityAndPrivacyEvent.Save) + } + with(awaitItem()) { + assertThat(saveAction).isEqualTo(AsyncAction.Loading) + } + room.givenRoomInfo( + aRoomInfo( + joinRule = JoinRule.KnockRestricted( + rules = persistentListOf(AllowRule.RoomMembership(A_ROOM_ID)) + ), + historyVisibility = RoomHistoryVisibility.Shared, + ) + ) + // Saved settings are updated multiple times to match the edited settings + skipItems(2) + with(awaitItem()) { + assertThat(saveAction).isEqualTo(AsyncAction.Success(Unit)) + } + assert(updateJoinRuleLambda).isCalledOnce().with( + value(JoinRule.KnockRestricted(rules = persistentListOf(AllowRule.RoomMembership(A_ROOM_ID)))) + ) + onDoneLambda.assertions().isCalledOnce() + } + } + + @Test + fun `present - room visibility is configurable for AskToJoinWithSpaceMember`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + getRoomVisibilityResult = { Result.success(RoomVisibility.Private) }, + initialRoomInfo = aRoomInfo( + historyVisibility = RoomHistoryVisibility.Shared, + joinRule = JoinRule.KnockRestricted( + rules = persistentListOf(AllowRule.RoomMembership(A_ROOM_ID)) + ) + ) + ) + ) + val presenter = createSecurityAndPrivacyPresenter(room = room) + presenter.test { + skipItems(1) + with(awaitItem()) { + assertThat(editedSettings.roomAccess).isInstanceOf(SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember::class.java) + assertThat(showRoomVisibilitySections).isTrue() + } + with(awaitItem()) { + assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(false)) + eventSink(SecurityAndPrivacyEvent.ToggleRoomVisibility) + } + with(awaitItem()) { + assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(true)) + assertThat(canBeSaved).isTrue() + } } } @@ -408,12 +761,17 @@ class SecurityAndPrivacyPresenterTest { ), navigator: SecurityAndPrivacyNavigator = FakeSecurityAndPrivacyNavigator(), featureFlagService: FeatureFlagService = FakeFeatureFlagService(), + matrixClient: MatrixClient = FakeMatrixClient( + userIdServerNameLambda = { serverName }, + spaceService = FakeSpaceService( + joinedParentsResult = { Result.success(emptyList()) }, + getSpaceRoomResult = { null } + ), + ), ): SecurityAndPrivacyPresenter { return SecurityAndPrivacyPresenter( room = room, - matrixClient = FakeMatrixClient( - userIdServerNameLambda = { serverName }, - ), + matrixClient = matrixClient, navigator = navigator, featureFlagService = featureFlagService, ) diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyViewTest.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyViewTest.kt index b15bc2fe37..10dac2213b 100644 --- a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyViewTest.kt +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyViewTest.kt @@ -19,8 +19,11 @@ import io.element.android.features.securityandprivacy.impl.root.SecurityAndPriva import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyRoomAccess import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyState import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyView +import io.element.android.features.securityandprivacy.impl.root.SpaceSelectionMode import io.element.android.features.securityandprivacy.impl.root.aSecurityAndPrivacySettings import io.element.android.features.securityandprivacy.impl.root.aSecurityAndPrivacyState +import io.element.android.libraries.matrix.test.A_ROOM_ID +import kotlinx.collections.immutable.persistentListOf import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.ui.strings.CommonStrings @@ -179,6 +182,48 @@ class SecurityAndPrivacyViewTest { rule.clickOn(R.string.screen_security_and_privacy_enable_encryption_alert_confirm_button_title) recorder.assertSingle(SecurityAndPrivacyEvent.ConfirmEnableEncryption) } + + @Test + @Config(qualifiers = "h1024dp") + fun `click on space member access emits the expected event`() { + val recorder = EventsRecorder() + val state = aSecurityAndPrivacyState( + eventSink = recorder, + spaceSelectionMode = SpaceSelectionMode.Single(A_ROOM_ID, null), + ) + rule.setSecurityAndPrivacyView(state) + rule.clickOn(R.string.screen_security_and_privacy_room_access_space_members_option_title) + recorder.assertSingle(SecurityAndPrivacyEvent.SelectSpaceMemberAccess) + } + + @Test + @Config(qualifiers = "h1024dp") + fun `click on ask to join with space members emits the expected event`() { + val recorder = EventsRecorder() + val state = aSecurityAndPrivacyState( + eventSink = recorder, + spaceSelectionMode = SpaceSelectionMode.Single(A_ROOM_ID, null), + ) + rule.setSecurityAndPrivacyView(state) + rule.clickOn(R.string.screen_security_and_privacy_ask_to_join_option_title) + recorder.assertSingle(SecurityAndPrivacyEvent.SelectAskToJoinWithSpaceMembersAccess) + } + + @Test + @Config(qualifiers = "h1024dp") + fun `manage spaces footer is shown when space member access is selected`() { + val recorder = EventsRecorder(expectEvents = false) + val state = aSecurityAndPrivacyState( + eventSink = recorder, + spaceSelectionMode = SpaceSelectionMode.Multiple, + editedSettings = aSecurityAndPrivacySettings( + roomAccess = SecurityAndPrivacyRoomAccess.SpaceMember(persistentListOf(A_ROOM_ID)), + ), + ) + rule.setSecurityAndPrivacyView(state) + // The footer text uses AnnotatedString with a link. Verify the footer text is displayed. + rule.onNodeWithText("Choose which spaces", substring = true).assertExists() + } } private fun AndroidComposeTestRule.setSecurityAndPrivacyView( diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenterTest.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenterTest.kt new file mode 100644 index 0000000000..86a0b47fc5 --- /dev/null +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenterTest.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * 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.securityandprivacy.impl.manageauthorizedspaces + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.tests.testutils.test +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ManageAuthorizedSpacesPresenterTest { + @Test + fun `present - initial state has empty selection`() = runTest { + val presenter = ManageAuthorizedSpacesPresenter() + presenter.test { + with(awaitItem()) { + assertThat(selectedIds).isEmpty() + assertThat(isSelectionComplete).isFalse() + assertThat(isDoneButtonEnabled).isFalse() + } + } + } + + @Test + fun `present - SetData event updates selection and initial selectedIds`() = runTest { + val presenter = ManageAuthorizedSpacesPresenter() + presenter.test { + val initialState = awaitItem() + val roomId = A_ROOM_ID + val data = AuthorizedSpacesSelection( + joinedSpaces = persistentListOf(), + unknownSpaceIds = persistentListOf(), + initialSelectedIds = persistentListOf(roomId) + ) + initialState.eventSink(ManageAuthorizedSpacesEvent.SetData(data)) + // SetData updates two state variables, which may emit intermediate states + skipItems(1) + with(awaitItem()) { + assertThat(selection).isEqualTo(data) + assertThat(selectedIds).containsExactly(roomId) + assertThat(isDoneButtonEnabled).isTrue() + } + } + } + + @Test + fun `present - ToggleSpace event adds space to selectedIds`() = runTest { + val presenter = ManageAuthorizedSpacesPresenter() + presenter.test { + val initialState = awaitItem() + val roomId = A_ROOM_ID + initialState.eventSink(ManageAuthorizedSpacesEvent.ToggleSpace(roomId)) + with(awaitItem()) { + assertThat(selectedIds).containsExactly(roomId) + assertThat(isDoneButtonEnabled).isTrue() + } + } + } + + @Test + fun `present - ToggleSpace event removes space when already selected`() = runTest { + val presenter = ManageAuthorizedSpacesPresenter() + presenter.test { + val initialState = awaitItem() + val roomId = A_ROOM_ID + initialState.eventSink(ManageAuthorizedSpacesEvent.ToggleSpace(roomId)) + val stateWithSelection = awaitItem() + assertThat(stateWithSelection.selectedIds).containsExactly(roomId) + stateWithSelection.eventSink(ManageAuthorizedSpacesEvent.ToggleSpace(roomId)) + with(awaitItem()) { + assertThat(selectedIds).isEmpty() + assertThat(isDoneButtonEnabled).isFalse() + } + } + } + + @Test + fun `present - Done event sets isSelectionComplete to true`() = runTest { + val presenter = ManageAuthorizedSpacesPresenter() + presenter.test { + val initialState = awaitItem() + initialState.eventSink(ManageAuthorizedSpacesEvent.Done) + with(awaitItem()) { + assertThat(isSelectionComplete).isTrue() + } + } + } +} diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesViewTest.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesViewTest.kt new file mode 100644 index 0000000000..515fe590a3 --- /dev/null +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesViewTest.kt @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * 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.securityandprivacy.impl.manageauthorizedspaces + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.previewutils.room.aSpaceRoom +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.pressBack +import kotlinx.collections.immutable.toImmutableList +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ManageAuthorizedSpacesViewTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `clicking back invokes callback`() { + ensureCalledOnce { callback -> + rule.setManageAuthorizedSpacesView(onBackClick = callback) + rule.pressBack() + } + } + + @Test + fun `clicking space checkbox emits ToggleSpace event`() { + val roomId = A_ROOM_ID + val space = aSpaceRoom(roomId = roomId, displayName = "Test Space") + val recorder = EventsRecorder() + val state = aManageAuthorizedSpacesState( + selection = anAuthorizedSpaceSelection( + joinedSpaces = listOf(space) + ), + eventSink = recorder + ) + rule.setManageAuthorizedSpacesView(state) + rule.onNodeWithText("Test Space").performClick() + recorder.assertSingle(ManageAuthorizedSpacesEvent.ToggleSpace(roomId)) + } + + @Test + fun `clicking done button emits Done event`() { + val recorder = EventsRecorder() + val state = aManageAuthorizedSpacesState( + selectedIds = listOf(A_ROOM_ID), + eventSink = recorder + ) + rule.setManageAuthorizedSpacesView(state) + rule.clickOn(CommonStrings.action_done) + recorder.assertSingle(ManageAuthorizedSpacesEvent.Done) + } + + @Test + fun `done button is disabled when no spaces selected`() { + val recorder = EventsRecorder(expectEvents = false) + val state = aManageAuthorizedSpacesState( + selectedIds = emptyList(), + eventSink = recorder + ) + rule.setManageAuthorizedSpacesView(state) + rule.clickOn(CommonStrings.action_done) + recorder.assertEmpty() + } +} + +private fun AndroidComposeTestRule.setManageAuthorizedSpacesView( + state: ManageAuthorizedSpacesState = aManageAuthorizedSpacesState( + eventSink = EventsRecorder(expectEvents = false) + ), + onBackClick: () -> Unit = EnsureNeverCalled(), +) { + setContent { + ManageAuthorizedSpacesView( + state = state, + onBackClick = onBackClick, + ) + } +} + +private fun aManageAuthorizedSpacesState( + selection: AuthorizedSpacesSelection = AuthorizedSpacesSelection(), + selectedIds: List = emptyList(), + isSelectionComplete: Boolean = false, + eventSink: (ManageAuthorizedSpacesEvent) -> Unit = {}, +) = ManageAuthorizedSpacesState( + selection = selection, + selectedIds = selectedIds.toImmutableList(), + isSelectionComplete = isSelectionComplete, + eventSink = eventSink, +) From 9cbc8cfa6755f0257201f3283fd3356a852efac0 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 7 Jan 2026 20:19:13 +0100 Subject: [PATCH 14/22] Fix SecurityAndPrivacy preview state configuration --- .../impl/root/SecurityAndPrivacyStateProvider.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt index d253021143..95cb45d641 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt @@ -66,21 +66,24 @@ private fun commonSecurityAndPrivacyStates(isSpace: Boolean): Sequence Date: Wed, 7 Jan 2026 21:09:20 +0100 Subject: [PATCH 15/22] quality : format and clean --- .../impl/SecurityAndPrivacyFlowNode.kt | 8 ++++---- .../impl/SecurityAndPrivacyNavigator.kt | 2 +- .../ManageAuthorizedSpacesNode.kt | 1 - .../ManageAuthorizedSpacesPresenter.kt | 11 +++++------ .../ManageAuthorizedSpacesStateProvider.kt | 1 - .../impl/root/SecurityAndPrivacyEvent.kt | 2 ++ .../impl/root/SecurityAndPrivacyNode.kt | 3 +-- .../impl/root/SecurityAndPrivacyPresenter.kt | 7 +++---- .../impl/root/SecurityAndPrivacyState.kt | 7 ++++--- .../impl/root/SecurityAndPrivacyView.kt | 4 +--- .../impl/SecurityAndPrivacyFlowNodeTest.kt | 2 -- .../impl/SecurityAndPrivacyPresenterTest.kt | 1 + .../impl/SecurityAndPrivacyViewTest.kt | 4 ++-- .../ManageAuthorizedSpacesPresenterTest.kt | 1 - 14 files changed, 24 insertions(+), 30 deletions(-) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt index 2c21964f48..4628974295 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt @@ -21,7 +21,6 @@ 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.activeElement -import com.bumble.appyx.navmodel.backstack.operation.pop import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode @@ -35,7 +34,6 @@ import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.callback import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.RoomScope -import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.powerlevels.use import kotlinx.coroutines.NonCancellable @@ -92,9 +90,11 @@ class SecurityAndPrivacyFlowNode( callback.onDone() } } - whenChildrenAttached { commonLifecycle: Lifecycle, + whenChildrenAttached { + commonLifecycle: Lifecycle, securityAndPrivacyNode: SecurityAndPrivacyNode, - manageAuthorizedSpacesNode: ManageAuthorizedSpacesNode -> + manageAuthorizedSpacesNode: ManageAuthorizedSpacesNode + -> commonLifecycle.coroutineScope.launch { val authorizedSpacesData = securityAndPrivacyNode.getAuthorizedSpacesData() val selectedSpaces = manageAuthorizedSpacesNode.waitForCompletion(authorizedSpacesData) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyNavigator.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyNavigator.kt index 9690c915d5..0944ae92bd 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyNavigator.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyNavigator.kt @@ -18,7 +18,7 @@ interface SecurityAndPrivacyNavigator : Plugin { fun onDone() fun openEditRoomAddress() fun closeEditRoomAddress() - fun openManageAuthorizedSpaces(forKnockRestricted: Boolean = false) + fun openManageAuthorizedSpaces(forKnockRestricted: Boolean) fun closeManageAuthorizedSpaces() } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt index b8f7150d86..08c757b87d 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt @@ -33,7 +33,6 @@ class ManageAuthorizedSpacesNode( @Assisted plugins: List, presenter: ManageAuthorizedSpacesPresenter, ) : Node(buildContext, plugins = plugins) { - private val navigator = plugins().first() private val stateFlow = launchMolecule { presenter.present() } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt index 0679c72066..aa2251408f 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt @@ -18,11 +18,10 @@ import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.core.RoomId import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toPersistentList +import kotlinx.collections.immutable.toImmutableList @Inject -class ManageAuthorizedSpacesPresenter() : Presenter { - +class ManageAuthorizedSpacesPresenter : Presenter { @Composable override fun present(): ManageAuthorizedSpacesState { var selectedIds: ImmutableList by remember { mutableStateOf(persistentListOf()) } @@ -31,12 +30,12 @@ class ManageAuthorizedSpacesPresenter() : Presenter fun handleEvent(event: ManageAuthorizedSpacesEvent) { when (event) { - ManageAuthorizedSpacesEvent.Done ->isSelectionComplete = true + ManageAuthorizedSpacesEvent.Done -> isSelectionComplete = true is ManageAuthorizedSpacesEvent.ToggleSpace -> { selectedIds = if (selectedIds.contains(event.roomId)) { - selectedIds.minus(event.roomId).toPersistentList() + selectedIds.minus(event.roomId).toImmutableList() } else { - selectedIds.plus(event.roomId).toPersistentList() + selectedIds.plus(event.roomId).toImmutableList() } } is ManageAuthorizedSpacesEvent.SetData -> { diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt index 2e062c186a..d818e31317 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt @@ -69,4 +69,3 @@ private fun aManageAuthorizedSpacesState( isSelectionComplete = false, eventSink = eventSink, ) - diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyEvent.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyEvent.kt index 25045a72c1..d61f063a26 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyEvent.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyEvent.kt @@ -15,8 +15,10 @@ sealed interface SecurityAndPrivacyEvent { data object Exit : SecurityAndPrivacyEvent data object DismissExitConfirmation : SecurityAndPrivacyEvent data class ChangeRoomAccess(val roomAccess: SecurityAndPrivacyRoomAccess) : SecurityAndPrivacyEvent + // Special case for "Space Members" data object SelectSpaceMemberAccess : SecurityAndPrivacyEvent + // Special case for "Ask to join with Space Members" data object SelectAskToJoinWithSpaceMembersAccess : SecurityAndPrivacyEvent data object ToggleEncryptionState : SecurityAndPrivacyEvent diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyNode.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyNode.kt index 7d3c115be5..815b7bd35e 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyNode.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyNode.kt @@ -46,7 +46,7 @@ class SecurityAndPrivacyNode( activity.openUrlInChromeCustomTab(null, darkTheme, url) } - fun getAuthorizedSpacesData(): AuthorizedSpacesSelection{ + fun getAuthorizedSpacesData(): AuthorizedSpacesSelection { return stateFlow.value.getAuthorizedSpacesSelection() } @@ -72,5 +72,4 @@ class SecurityAndPrivacyNode( modifier = modifier ) } - } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt index a27884a1ab..f80b96ca00 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt @@ -35,7 +35,6 @@ import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomAlias -import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.RoomInfo import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility @@ -193,11 +192,12 @@ class SecurityAndPrivacyPresenter( saveAction.value = AsyncAction.Uninitialized } SecurityAndPrivacyEvent.ManageAuthorizedSpaces -> { - navigator.openManageAuthorizedSpaces() + navigator.openManageAuthorizedSpaces( + forKnockRestricted = editedRoomAccess.value is SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember + ) } SecurityAndPrivacyEvent.SelectSpaceMemberAccess -> handleSpaceMemberAccessSelection( spaceSelectionMode = spaceSelectionMode, - spaceIds = editedSettings.roomAccess.spaceIds(), editedAccess = editedRoomAccess, ) SecurityAndPrivacyEvent.SelectAskToJoinWithSpaceMembersAccess -> handleAskToJoinWithSpaceMembersAccessSelection( @@ -250,7 +250,6 @@ class SecurityAndPrivacyPresenter( private fun handleSpaceMemberAccessSelection( spaceSelectionMode: SpaceSelectionMode, - spaceIds: List, editedAccess: MutableState, ) { if (editedAccess.value is SecurityAndPrivacyRoomAccess.SpaceMember) { diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt index 9d47e57846..ff627f6a0e 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt @@ -38,7 +38,6 @@ data class SecurityAndPrivacyState( private val spaceSelectionMode: SpaceSelectionMode, val eventSink: (SecurityAndPrivacyEvent) -> Unit ) { - val isSpaceMemberSelectable = isSpaceSettingsEnabled && spaceSelectionMode != SpaceSelectionMode.None // Show SpaceMember option in two cases: @@ -58,7 +57,7 @@ data class SecurityAndPrivacyState( // - AskToJoin is the current saved value (legacy), OR // - Knock FF enabled BUT (SpaceSettings FF disabled OR no spaces available) val showAskToJoinOption = savedSettings.roomAccess == SecurityAndPrivacyRoomAccess.AskToJoin || - (isAskToJoinSelectable && !isAskToJoinWithSpaceMembersSelectable) + isAskToJoinSelectable && !isAskToJoinWithSpaceMembersSelectable // Show AskToJoinWithSpaceMember option when: // - It's the current saved value, OR @@ -98,7 +97,9 @@ data class SecurityAndPrivacyState( stringResource(R.string.screen_security_and_privacy_room_access_space_members_option_single_parent_description, spaceName) } is SpaceSelectionMode.None, - is SpaceSelectionMode.Multiple -> stringResource(R.string.screen_security_and_privacy_room_access_space_members_option_multiple_parents_description) + is SpaceSelectionMode.Multiple -> stringResource( + R.string.screen_security_and_privacy_room_access_space_members_option_multiple_parents_description + ) } } else { stringResource(R.string.screen_security_and_privacy_room_access_space_members_option_unavailable_description) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt index 991fa8c133..3ebb2de282 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt @@ -208,9 +208,7 @@ private fun RoomAccessSection( state: SecurityAndPrivacyState, modifier: Modifier = Modifier, ) { - val edited = state.editedSettings.roomAccess - val saved = state.savedSettings.roomAccess fun onSelectOption(option: SecurityAndPrivacyRoomAccess) { state.eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(option)) @@ -282,7 +280,7 @@ private fun RoomAccessSection( val footerText = stringWithLink( textRes = R.string.screen_security_and_privacy_room_access_footer, url = stringResource(R.string.screen_security_and_privacy_room_access_footer_manage_spaces_action), - onLinkClick = {onManageSpacesClick()}, + onLinkClick = { onManageSpacesClick() }, ) Text( text = footerText, diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNodeTest.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNodeTest.kt index cc32b59684..baae82a586 100644 --- a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNodeTest.kt +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNodeTest.kt @@ -11,7 +11,6 @@ package io.element.android.features.securityandprivacy.impl import androidx.test.ext.junit.runners.AndroidJUnit4 import com.bumble.appyx.core.modality.AncestryInfo import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.activeElement import com.bumble.appyx.utils.customisations.NodeCustomisationDirectoryImpl import com.google.common.truth.Truth.assertThat @@ -27,7 +26,6 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class SecurityAndPrivacyFlowNodeTest { - @Test fun `initial backstack contains SecurityAndPrivacy`() = runTest { val flowNode = createFlowNode() diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyPresenterTest.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyPresenterTest.kt index 9a166e4daf..70c86b9525 100644 --- a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyPresenterTest.kt +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyPresenterTest.kt @@ -44,6 +44,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.test.runTest import org.junit.Test +@Suppress("LargeClass") class SecurityAndPrivacyPresenterTest { @Test fun `present - initial states`() = runTest { diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyViewTest.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyViewTest.kt index 10dac2213b..7cd9b154cf 100644 --- a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyViewTest.kt +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyViewTest.kt @@ -22,15 +22,15 @@ import io.element.android.features.securityandprivacy.impl.root.SecurityAndPriva import io.element.android.features.securityandprivacy.impl.root.SpaceSelectionMode import io.element.android.features.securityandprivacy.impl.root.aSecurityAndPrivacySettings import io.element.android.features.securityandprivacy.impl.root.aSecurityAndPrivacyState -import io.element.android.libraries.matrix.test.A_ROOM_ID -import kotlinx.collections.immutable.persistentListOf import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.pressBack +import kotlinx.collections.immutable.persistentListOf import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenterTest.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenterTest.kt index 86a0b47fc5..7d678b23bb 100644 --- a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenterTest.kt +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenterTest.kt @@ -9,7 +9,6 @@ package io.element.android.features.securityandprivacy.impl.manageauthorizedspaces import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.tests.testutils.test import kotlinx.collections.immutable.persistentListOf From 13cce209ab7db136b13109e52f7fc98d1e4ca539 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 8 Jan 2026 13:49:53 +0000 Subject: [PATCH 16/22] Update screenshots --- ...geauthorizedspaces_ManageAuthorizedSpacesView_Day_0_en.png | 3 +++ ...geauthorizedspaces_ManageAuthorizedSpacesView_Day_1_en.png | 3 +++ ...geauthorizedspaces_ManageAuthorizedSpacesView_Day_2_en.png | 3 +++ ...authorizedspaces_ManageAuthorizedSpacesView_Night_0_en.png | 3 +++ ...authorizedspaces_ManageAuthorizedSpacesView_Night_1_en.png | 3 +++ ...authorizedspaces_ManageAuthorizedSpacesView_Night_2_en.png | 3 +++ ...yandprivacy.impl.root_SecurityAndPrivacyViewDark_10_en.png | 4 ++-- ...yandprivacy.impl.root_SecurityAndPrivacyViewDark_11_en.png | 2 +- ...yandprivacy.impl.root_SecurityAndPrivacyViewDark_12_en.png | 4 ++-- ...yandprivacy.impl.root_SecurityAndPrivacyViewDark_13_en.png | 4 ++-- ...yandprivacy.impl.root_SecurityAndPrivacyViewDark_14_en.png | 4 ++-- ...yandprivacy.impl.root_SecurityAndPrivacyViewDark_15_en.png | 4 ++-- ...yandprivacy.impl.root_SecurityAndPrivacyViewDark_16_en.png | 4 ++-- ...yandprivacy.impl.root_SecurityAndPrivacyViewDark_17_en.png | 4 ++-- ...yandprivacy.impl.root_SecurityAndPrivacyViewDark_18_en.png | 4 ++-- ...yandprivacy.impl.root_SecurityAndPrivacyViewDark_19_en.png | 4 ++-- ...yandprivacy.impl.root_SecurityAndPrivacyViewDark_20_en.png | 3 +++ ...yandprivacy.impl.root_SecurityAndPrivacyViewDark_21_en.png | 3 +++ ...yandprivacy.impl.root_SecurityAndPrivacyViewDark_22_en.png | 3 +++ ...yandprivacy.impl.root_SecurityAndPrivacyViewDark_23_en.png | 3 +++ ...tyandprivacy.impl.root_SecurityAndPrivacyViewDark_4_en.png | 4 ++-- ...tyandprivacy.impl.root_SecurityAndPrivacyViewDark_5_en.png | 4 ++-- ...tyandprivacy.impl.root_SecurityAndPrivacyViewDark_6_en.png | 4 ++-- ...tyandprivacy.impl.root_SecurityAndPrivacyViewDark_7_en.png | 4 ++-- ...tyandprivacy.impl.root_SecurityAndPrivacyViewDark_8_en.png | 4 ++-- ...tyandprivacy.impl.root_SecurityAndPrivacyViewDark_9_en.png | 4 ++-- ...andprivacy.impl.root_SecurityAndPrivacyViewLight_10_en.png | 4 ++-- ...andprivacy.impl.root_SecurityAndPrivacyViewLight_11_en.png | 4 ++-- ...andprivacy.impl.root_SecurityAndPrivacyViewLight_12_en.png | 4 ++-- ...andprivacy.impl.root_SecurityAndPrivacyViewLight_13_en.png | 4 ++-- ...andprivacy.impl.root_SecurityAndPrivacyViewLight_14_en.png | 4 ++-- ...andprivacy.impl.root_SecurityAndPrivacyViewLight_15_en.png | 4 ++-- ...andprivacy.impl.root_SecurityAndPrivacyViewLight_16_en.png | 4 ++-- ...andprivacy.impl.root_SecurityAndPrivacyViewLight_17_en.png | 4 ++-- ...andprivacy.impl.root_SecurityAndPrivacyViewLight_18_en.png | 4 ++-- ...andprivacy.impl.root_SecurityAndPrivacyViewLight_19_en.png | 4 ++-- ...andprivacy.impl.root_SecurityAndPrivacyViewLight_20_en.png | 3 +++ ...andprivacy.impl.root_SecurityAndPrivacyViewLight_21_en.png | 3 +++ ...andprivacy.impl.root_SecurityAndPrivacyViewLight_22_en.png | 3 +++ ...andprivacy.impl.root_SecurityAndPrivacyViewLight_23_en.png | 3 +++ ...yandprivacy.impl.root_SecurityAndPrivacyViewLight_4_en.png | 4 ++-- ...yandprivacy.impl.root_SecurityAndPrivacyViewLight_5_en.png | 4 ++-- ...yandprivacy.impl.root_SecurityAndPrivacyViewLight_6_en.png | 4 ++-- ...yandprivacy.impl.root_SecurityAndPrivacyViewLight_7_en.png | 4 ++-- ...yandprivacy.impl.root_SecurityAndPrivacyViewLight_8_en.png | 4 ++-- ...yandprivacy.impl.root_SecurityAndPrivacyViewLight_9_en.png | 4 ++-- 46 files changed, 105 insertions(+), 63 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_20_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_21_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_22_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_23_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_20_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_21_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_22_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_23_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_0_en.png new file mode 100644 index 0000000000..1fb6e305bc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e09986f8b500061cfc9108af757b581334d8babb3c5c17433dd53dd3a8f52f3 +size 48709 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_1_en.png new file mode 100644 index 0000000000..1fb6e305bc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e09986f8b500061cfc9108af757b581334d8babb3c5c17433dd53dd3a8f52f3 +size 48709 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_2_en.png new file mode 100644 index 0000000000..327f317576 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f953609cb044e439198f7072a4468c54269b646e3c39686440334903eb12797d +size 49279 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_0_en.png new file mode 100644 index 0000000000..0e84fd507e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c481049fcd33ca40aac538bf82ca56ef27cce2448c148cc45ba95493b668ef03 +size 47890 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_1_en.png new file mode 100644 index 0000000000..0e84fd507e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c481049fcd33ca40aac538bf82ca56ef27cce2448c148cc45ba95493b668ef03 +size 47890 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_2_en.png new file mode 100644 index 0000000000..85f1bc55ea --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15ed6659fbf148a23b4090ba8896bad91d82279df71552c2798755b190a0cdca +size 48369 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_10_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_10_en.png index 5015b5c995..46ad8a30d5 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bef6e6c4cdd362577cf5dc91914a84c72465014a71a08f48c1f4a526be44c85c -size 39535 +oid sha256:67bd76421dcb3e8a6c2c0a6aeda024dacb392b2f6d8bea278d6ffae7de5b4e8f +size 20023 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_11_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_11_en.png index fbef6afae3..6b5c72706b 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:39222fd299a2dec3cc60863878906eba5c9d86100a3e4bad781633a1a0372cc5 +oid sha256:48f50c8053f0f15228899e8bce1656a90cfaf936e6bc8ca725c676aff5578575 size 39874 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_12_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_12_en.png index 8abe7a325a..5015b5c995 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c9bf91f57bb68ae3341acecd62d418d1654a8d4cf271eeda9e87bc5e4998891 -size 20225 +oid sha256:bef6e6c4cdd362577cf5dc91914a84c72465014a71a08f48c1f4a526be44c85c +size 39535 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_13_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_13_en.png index 3989ce8d6e..fbef6afae3 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:495fff852c9549125b3d3a681a880147f680d4604eee3cb13a8fc3dc47e7c729 -size 40476 +oid sha256:39222fd299a2dec3cc60863878906eba5c9d86100a3e4bad781633a1a0372cc5 +size 39874 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_14_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_14_en.png index fbef6afae3..71f935b3c0 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_14_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:39222fd299a2dec3cc60863878906eba5c9d86100a3e4bad781633a1a0372cc5 -size 39874 +oid sha256:3b2a019695bb544f7bfbb2b2c5fddc5140a25a770695d1354c2b393e942ebba5 +size 24687 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_15_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_15_en.png index 8bb18a4153..8b34eb8946 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_15_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_15_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cdd805d857d590437bde9be2a7df252ff42571d3b08be8085aae81dc75e896c3 -size 40235 +oid sha256:adaadbcdcb1d281683fcd7cfb69604ed875ffd4a48233dfd2941d3997ab097de +size 51431 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_16_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_16_en.png index 81ed09d87e..8b34eb8946 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_16_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_16_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:507eec1e5125056ca4980ae67f904dd30bcb88ef6d66d5968aafbb3c1449e4b8 -size 33779 +oid sha256:adaadbcdcb1d281683fcd7cfb69604ed875ffd4a48233dfd2941d3997ab097de +size 51431 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_17_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_17_en.png index 14683ef0b0..3989ce8d6e 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_17_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_17_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d300a9854096634ea0e6722db487607a75f6e0c46d7341f044a190a212d95307 -size 32373 +oid sha256:495fff852c9549125b3d3a681a880147f680d4604eee3cb13a8fc3dc47e7c729 +size 40476 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_18_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_18_en.png index 810bb5531a..fbef6afae3 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_18_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_18_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b42e3df9fec259210b63431e295e8c7d1a7640149c9616b572b90c0b40392a60 -size 34637 +oid sha256:39222fd299a2dec3cc60863878906eba5c9d86100a3e4bad781633a1a0372cc5 +size 39874 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_19_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_19_en.png index 0bd3c0413d..8bb18a4153 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_19_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_19_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3bb107a37b14dcbc6aa530a8d47f58c809df7719a0c8a416a268ffd2df165681 -size 41602 +oid sha256:cdd805d857d590437bde9be2a7df252ff42571d3b08be8085aae81dc75e896c3 +size 40235 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_20_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_20_en.png new file mode 100644 index 0000000000..81ed09d87e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_20_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:507eec1e5125056ca4980ae67f904dd30bcb88ef6d66d5968aafbb3c1449e4b8 +size 33779 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_21_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_21_en.png new file mode 100644 index 0000000000..14683ef0b0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_21_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d300a9854096634ea0e6722db487607a75f6e0c46d7341f044a190a212d95307 +size 32373 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_22_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_22_en.png new file mode 100644 index 0000000000..810bb5531a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_22_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b42e3df9fec259210b63431e295e8c7d1a7640149c9616b572b90c0b40392a60 +size 34637 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_23_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_23_en.png new file mode 100644 index 0000000000..0bd3c0413d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_23_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3bb107a37b14dcbc6aa530a8d47f58c809df7719a0c8a416a268ffd2df165681 +size 41602 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_4_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_4_en.png index beeea22486..00573c56c2 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b67ebf4536e36571b0867e71533b2fe1953ca2b88f15fe3531f3ddb9043f69ec -size 39418 +oid sha256:1605f3269a888d8cbcab71615816b7564072f26846e481cf3576f9de4b61f2e1 +size 44182 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_5_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_5_en.png index b39a2f040c..9b03e75694 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5fb16ef839b9f93caddbc984c8be260846e7fd7d68ae3b31dcc97c7644b41b4 -size 56670 +oid sha256:77213c9b75eea8880c5fac86c3345e7056f304c8ad5beb6cf2057cb5d67b5415 +size 58621 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_6_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_6_en.png index f29bc630f1..9b03e75694 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9402ad99b181b99083abff78d078e9fbb010144e07f80ff995db7884a61fa853 -size 56060 +oid sha256:77213c9b75eea8880c5fac86c3345e7056f304c8ad5beb6cf2057cb5d67b5415 +size 58621 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_7_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_7_en.png index e5fda5dfd7..b39a2f040c 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cadacec9344f3ca2c029006c620cd8c309bb7f4b38cbb7b47c7c336ca265a141 -size 56424 +oid sha256:e5fb16ef839b9f93caddbc984c8be260846e7fd7d68ae3b31dcc97c7644b41b4 +size 56670 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_8_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_8_en.png index 46ad8a30d5..f29bc630f1 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67bd76421dcb3e8a6c2c0a6aeda024dacb392b2f6d8bea278d6ffae7de5b4e8f -size 20023 +oid sha256:9402ad99b181b99083abff78d078e9fbb010144e07f80ff995db7884a61fa853 +size 56060 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_9_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_9_en.png index 6b5c72706b..e5fda5dfd7 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48f50c8053f0f15228899e8bce1656a90cfaf936e6bc8ca725c676aff5578575 -size 39874 +oid sha256:cadacec9344f3ca2c029006c620cd8c309bb7f4b38cbb7b47c7c336ca265a141 +size 56424 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_10_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_10_en.png index da192161cb..83f51a3be7 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:342cc313605f43325a0720fe9c9fa969ed7edd41258ce0768dc624fc4e74d66d -size 40636 +oid sha256:8303e863849a00c16f00850854e8d4d7ceaf3fb097c7b20fe349127d8ed3b082 +size 20651 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_11_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_11_en.png index 4ddc1b913d..eea66871f9 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:adfd4213ac36d904a8a31b1c3bf5463ba72ad4760d084a4c412370d6d51cf250 -size 40996 +oid sha256:1626bffb649c204b0dcb1c92e7344d1b7f119b61ecac3c49840385f3a9a39515 +size 40998 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_12_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_12_en.png index 32138581f7..da192161cb 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ef8e92812f6b65fc09ea4e77d44f9c32a74daebe8e2ef2a9e499588e46a43db -size 20823 +oid sha256:342cc313605f43325a0720fe9c9fa969ed7edd41258ce0768dc624fc4e74d66d +size 40636 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_13_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_13_en.png index d274eddbe1..4ddc1b913d 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5547eb8b7f7ca596edf65bfd7703d1cb91fbf2e254374b9ea41d5c8754030e34 -size 41628 +oid sha256:adfd4213ac36d904a8a31b1c3bf5463ba72ad4760d084a4c412370d6d51cf250 +size 40996 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_14_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_14_en.png index 4ddc1b913d..e050bddd3e 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_14_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:adfd4213ac36d904a8a31b1c3bf5463ba72ad4760d084a4c412370d6d51cf250 -size 40996 +oid sha256:c251b0e7daf758b0031c1c81b130d06f295126cb6bbc8efd13f43c6ef581ad8e +size 25132 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_15_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_15_en.png index e939df2248..3a1c1d3bf9 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_15_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_15_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:40c6334e01cc858f343f9046cfbb5079b79369002f5194965b6c2e75dd37b134 -size 41437 +oid sha256:cfadbdeaf77af829c19ae33338e91679acc8b34d2c8493d20994a740e2d2f3ee +size 52901 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_16_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_16_en.png index 0ba776c5fd..3a1c1d3bf9 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_16_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_16_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:548950d915f6fbfdf1a17b81619d3d4cbefe93b79091d259bcec41ae6bbbc652 -size 35422 +oid sha256:cfadbdeaf77af829c19ae33338e91679acc8b34d2c8493d20994a740e2d2f3ee +size 52901 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_17_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_17_en.png index 5932151ec4..d274eddbe1 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_17_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_17_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b47bcab08cded6e857fe213ab2c90602e557a36feebb45b2b8d4d18115ac68b -size 34582 +oid sha256:5547eb8b7f7ca596edf65bfd7703d1cb91fbf2e254374b9ea41d5c8754030e34 +size 41628 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_18_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_18_en.png index 8919639fe0..4ddc1b913d 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_18_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_18_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5cd5de47e285702bf3d2ee325fc83a1168a0367d9c6b54018565daa0ed503a77 -size 36876 +oid sha256:adfd4213ac36d904a8a31b1c3bf5463ba72ad4760d084a4c412370d6d51cf250 +size 40996 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_19_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_19_en.png index bce9a377e8..e939df2248 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_19_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_19_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:59b6d7d5e484b34e1ce7814432144df34b1cd3323d31c90db439cbb5d9f8dc83 -size 43588 +oid sha256:40c6334e01cc858f343f9046cfbb5079b79369002f5194965b6c2e75dd37b134 +size 41437 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_20_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_20_en.png new file mode 100644 index 0000000000..0ba776c5fd --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_20_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:548950d915f6fbfdf1a17b81619d3d4cbefe93b79091d259bcec41ae6bbbc652 +size 35422 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_21_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_21_en.png new file mode 100644 index 0000000000..5932151ec4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_21_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b47bcab08cded6e857fe213ab2c90602e557a36feebb45b2b8d4d18115ac68b +size 34582 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_22_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_22_en.png new file mode 100644 index 0000000000..8919639fe0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_22_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5cd5de47e285702bf3d2ee325fc83a1168a0367d9c6b54018565daa0ed503a77 +size 36876 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_23_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_23_en.png new file mode 100644 index 0000000000..bce9a377e8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_23_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59b6d7d5e484b34e1ce7814432144df34b1cd3323d31c90db439cbb5d9f8dc83 +size 43588 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_4_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_4_en.png index a3ddf8b354..97f3d826b9 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3bebec99f2b870e7a38333590f5cb776b30f4f9b976d99601a7fb3bfd05a71f -size 40913 +oid sha256:3cf16df2caf7cdf9ca4a2da151ffa4c7b362b9d22bf9706522dacbd4edf520e5 +size 45867 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_5_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_5_en.png index 085ac47033..bd71e00714 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:853218e7ddc4d18b260f6ab047beb82778284851e230babc14113c6cb329d29a -size 58504 +oid sha256:b77f723d2c6c5eba85660d93cdcd63d785bccd61aab37f0784ca1fa646280743 +size 60462 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_6_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_6_en.png index 0914a151e4..bd71e00714 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e7caee4430244e294b75e3b6a5830088dd25397b1c272666eb2bec47ecd1f382 -size 57895 +oid sha256:b77f723d2c6c5eba85660d93cdcd63d785bccd61aab37f0784ca1fa646280743 +size 60462 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_7_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_7_en.png index c9a3183eea..085ac47033 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bbb4224047c88d838de227e61332775e5efe0ce1380a7446c701f0db5b2d2dcc -size 58323 +oid sha256:853218e7ddc4d18b260f6ab047beb82778284851e230babc14113c6cb329d29a +size 58504 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_8_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_8_en.png index 83f51a3be7..0914a151e4 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8303e863849a00c16f00850854e8d4d7ceaf3fb097c7b20fe349127d8ed3b082 -size 20651 +oid sha256:e7caee4430244e294b75e3b6a5830088dd25397b1c272666eb2bec47ecd1f382 +size 57895 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_9_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_9_en.png index eea66871f9..c9a3183eea 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1626bffb649c204b0dcb1c92e7344d1b7f119b61ecac3c49840385f3a9a39515 -size 40998 +oid sha256:bbb4224047c88d838de227e61332775e5efe0ce1380a7446c701f0db5b2d2dcc +size 58323 From 0f628bef50d7a2a791f691fc1a872dd62b7c297e Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 8 Jan 2026 17:16:45 +0100 Subject: [PATCH 17/22] Add comprehensive presenter tests for SecurityAndPrivacy feature --- .../impl/SecurityAndPrivacyPresenterTest.kt | 400 ++++++++++++++++++ 1 file changed, 400 insertions(+) diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyPresenterTest.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyPresenterTest.kt index 70c86b9525..bf8ab13bad 100644 --- a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyPresenterTest.kt +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyPresenterTest.kt @@ -732,6 +732,406 @@ class SecurityAndPrivacyPresenterTest { } } + @Test + fun `present - availableHistoryVisibilities includes WorldReadable for Anyone without encryption`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + initialRoomInfo = aRoomInfo( + joinRule = JoinRule.Public, + historyVisibility = RoomHistoryVisibility.Shared, + ) + ) + ) + val presenter = createSecurityAndPrivacyPresenter(room = room) + presenter.test { + skipItems(1) + with(awaitItem()) { + assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.Anyone) + assertThat(editedSettings.isEncrypted).isFalse() + assertThat(availableHistoryVisibilities).contains(SecurityAndPrivacyHistoryVisibility.WorldReadable) + assertThat(availableHistoryVisibilities).doesNotContain(SecurityAndPrivacyHistoryVisibility.Invited) + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - availableHistoryVisibilities includes Invited for InviteOnly access`() = runTest { + val presenter = createSecurityAndPrivacyPresenter() + presenter.test { + skipItems(1) + with(awaitItem()) { + assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly) + assertThat(availableHistoryVisibilities).contains(SecurityAndPrivacyHistoryVisibility.Invited) + assertThat(availableHistoryVisibilities).doesNotContain(SecurityAndPrivacyHistoryVisibility.WorldReadable) + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - availableHistoryVisibilities excludes WorldReadable when encrypted`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + initialRoomInfo = aRoomInfo( + joinRule = JoinRule.Public, + historyVisibility = RoomHistoryVisibility.Shared, + isEncrypted = true, + ) + ) + ) + val presenter = createSecurityAndPrivacyPresenter(room = room) + presenter.test { + skipItems(1) + with(awaitItem()) { + assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.Anyone) + assertThat(editedSettings.isEncrypted).isTrue() + assertThat(availableHistoryVisibilities).contains(SecurityAndPrivacyHistoryVisibility.Invited) + assertThat(availableHistoryVisibilities).doesNotContain(SecurityAndPrivacyHistoryVisibility.WorldReadable) + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - showSpaceMemberOption is true when savedSettings has SpaceMember`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + initialRoomInfo = aRoomInfo( + joinRule = JoinRule.Restricted( + rules = persistentListOf(AllowRule.RoomMembership(A_ROOM_ID)) + ), + historyVisibility = RoomHistoryVisibility.Shared, + ) + ) + ) + // No spaces available, so isSpaceMemberSelectable should be false + val presenter = createSecurityAndPrivacyPresenter(room = room) + presenter.test { + skipItems(1) + with(awaitItem()) { + assertThat(savedSettings.roomAccess).isInstanceOf(SecurityAndPrivacyRoomAccess.SpaceMember::class.java) + assertThat(isSpaceMemberSelectable).isFalse() + // showSpaceMemberOption should still be true because savedSettings has SpaceMember + assertThat(showSpaceMemberOption).isTrue() + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - showSpaceMemberOption is false when not selectable and savedSettings is not SpaceMember`() = runTest { + // No spaces available, default InviteOnly join rule + val presenter = createSecurityAndPrivacyPresenter() + presenter.test { + skipItems(1) + with(awaitItem()) { + assertThat(savedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly) + assertThat(isSpaceMemberSelectable).isFalse() + assertThat(showSpaceMemberOption).isFalse() + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - showManageSpaceFooter is true when Multiple mode and SpaceMember access`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + initialRoomInfo = aRoomInfo( + historyVisibility = RoomHistoryVisibility.Shared, + joinRule = JoinRule.Invite + ) + ) + ) + val client = FakeMatrixClient( + userIdServerNameLambda = { "matrix.org" }, + spaceService = FakeSpaceService( + joinedParentsResult = { _ -> + Result.success(listOf(aSpaceRoom(roomId = A_ROOM_ID), aSpaceRoom(roomId = RoomId("!space2:matrix.org")))) + } + ) + ) + val presenter = createSecurityAndPrivacyPresenter( + room = room, + matrixClient = client, + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.SpaceSettings.key to true) + ) + ) + presenter.test { + skipItems(1) + val state = awaitItem() + // Change to SpaceMember access + val spaceMemberAccess = SecurityAndPrivacyRoomAccess.SpaceMember( + spaceIds = persistentListOf(A_ROOM_ID) + ) + state.eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(spaceMemberAccess)) + with(awaitItem()) { + assertThat(editedSettings.roomAccess).isInstanceOf(SecurityAndPrivacyRoomAccess.SpaceMember::class.java) + assertThat(showManageSpaceFooter).isTrue() + } + } + } + + @Test + fun `present - showManageSpaceFooter is false when Single mode`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + initialRoomInfo = aRoomInfo( + historyVisibility = RoomHistoryVisibility.Shared, + joinRule = JoinRule.Invite + ) + ) + ) + // Single space available + val client = FakeMatrixClient( + userIdServerNameLambda = { "matrix.org" }, + spaceService = FakeSpaceService( + joinedParentsResult = { _ -> + Result.success(listOf(aSpaceRoom(roomId = A_ROOM_ID))) + } + ) + ) + val presenter = createSecurityAndPrivacyPresenter( + room = room, + matrixClient = client, + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.SpaceSettings.key to true) + ) + ) + presenter.test { + skipItems(1) + val state = awaitItem() + // Select SpaceMember access (single space auto-selects) + state.eventSink(SecurityAndPrivacyEvent.SelectSpaceMemberAccess) + with(awaitItem()) { + assertThat(editedSettings.roomAccess).isInstanceOf(SecurityAndPrivacyRoomAccess.SpaceMember::class.java) + // Single mode, so no footer + assertThat(showManageSpaceFooter).isFalse() + } + } + } + + @Test + fun `present - isAskToJoinSelectable is true when Knock FF enabled`() = runTest { + val presenter = createSecurityAndPrivacyPresenter( + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.Knock.key to true) + ) + ) + presenter.test { + skipItems(1) + with(awaitItem()) { + assertThat(isAskToJoinSelectable).isTrue() + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - isAskToJoinSelectable is false when Knock FF disabled`() = runTest { + val presenter = createSecurityAndPrivacyPresenter( + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.Knock.key to false) + ) + ) + presenter.test { + skipItems(1) + with(awaitItem()) { + assertThat(isAskToJoinSelectable).isFalse() + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - getAuthorizedSpacesSelection returns correct data for SpaceMember`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + initialRoomInfo = aRoomInfo( + historyVisibility = RoomHistoryVisibility.Shared, + joinRule = JoinRule.Restricted( + rules = persistentListOf(AllowRule.RoomMembership(A_ROOM_ID)) + ) + ) + ) + ) + val spaceRoom = aSpaceRoom(roomId = A_ROOM_ID) + val client = FakeMatrixClient( + userIdServerNameLambda = { "matrix.org" }, + spaceService = FakeSpaceService( + joinedParentsResult = { _ -> Result.success(listOf(spaceRoom)) }, + getSpaceRoomResult = { null } + ) + ) + val presenter = createSecurityAndPrivacyPresenter( + room = room, + matrixClient = client, + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.SpaceSettings.key to true) + ) + ) + presenter.test { + skipItems(1) + with(awaitItem()) { + val selection = getAuthorizedSpacesSelection() + assertThat(selection.joinedSpaces).containsExactly(spaceRoom) + assertThat(selection.initialSelectedIds).containsExactly(A_ROOM_ID) + assertThat(selection.unknownSpaceIds).isEmpty() + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - getAuthorizedSpacesSelection identifies unknown space IDs`() = runTest { + val unknownSpaceId = RoomId("!unknown:matrix.org") + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + initialRoomInfo = aRoomInfo( + historyVisibility = RoomHistoryVisibility.Shared, + joinRule = JoinRule.Restricted( + rules = persistentListOf(AllowRule.RoomMembership(unknownSpaceId)) + ) + ) + ) + ) + // No spaces available (the space in the join rule is unknown) + val presenter = createSecurityAndPrivacyPresenter(room = room) + presenter.test { + skipItems(1) + with(awaitItem()) { + val selection = getAuthorizedSpacesSelection() + assertThat(selection.joinedSpaces).isEmpty() + assertThat(selection.unknownSpaceIds).containsExactly(unknownSpaceId) + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - SelectAskToJoinWithSpaceMembersAccess with single space auto-selects`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + initialRoomInfo = aRoomInfo( + historyVisibility = RoomHistoryVisibility.Shared, + joinRule = JoinRule.Invite + ) + ) + ) + val client = FakeMatrixClient( + userIdServerNameLambda = { "matrix.org" }, + spaceService = FakeSpaceService( + joinedParentsResult = { _ -> + Result.success(listOf(aSpaceRoom(roomId = A_ROOM_ID))) + } + ) + ) + val presenter = createSecurityAndPrivacyPresenter( + room = room, + matrixClient = client, + featureFlagService = FakeFeatureFlagService( + initialState = mapOf( + FeatureFlags.Knock.key to true, + FeatureFlags.SpaceSettings.key to true, + ) + ) + ) + presenter.test { + skipItems(1) + val state = awaitItem() + assertThat(state.isAskToJoinWithSpaceMembersSelectable).isTrue() + state.eventSink(SecurityAndPrivacyEvent.SelectAskToJoinWithSpaceMembersAccess) + with(awaitItem()) { + assertThat(editedSettings.roomAccess).isInstanceOf(SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember::class.java) + val access = editedSettings.roomAccess as SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember + assertThat(access.spaceIds).containsExactly(A_ROOM_ID) + } + } + } + + @Test + fun `present - showAskToJoinOption is true when savedSettings is AskToJoin`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + initialRoomInfo = aRoomInfo( + joinRule = JoinRule.Knock, + historyVisibility = RoomHistoryVisibility.Shared, + ) + ) + ) + // Knock FF disabled, but showAskToJoinOption should still be true because savedSettings has AskToJoin + val presenter = createSecurityAndPrivacyPresenter( + room = room, + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.Knock.key to false) + ) + ) + presenter.test { + skipItems(1) + with(awaitItem()) { + assertThat(savedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.AskToJoin) + assertThat(isAskToJoinSelectable).isFalse() + assertThat(showAskToJoinOption).isTrue() + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - showHistoryVisibilitySection is false for space`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + initialRoomInfo = aRoomInfo( + historyVisibility = RoomHistoryVisibility.Shared, + joinRule = JoinRule.Invite, + isSpace = true, + ) + ) + ) + val presenter = createSecurityAndPrivacyPresenter(room = room) + presenter.test { + skipItems(1) + with(awaitItem()) { + assertThat(showHistoryVisibilitySection).isFalse() + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - showEncryptionSection is false for space`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + initialRoomInfo = aRoomInfo( + historyVisibility = RoomHistoryVisibility.Shared, + joinRule = JoinRule.Invite, + isSpace = true, + ) + ) + ) + val presenter = createSecurityAndPrivacyPresenter(room = room) + presenter.test { + skipItems(1) + with(awaitItem()) { + assertThat(showEncryptionSection).isFalse() + } + cancelAndIgnoreRemainingEvents() + } + } + private fun roomPermissions( canChangeRoomAccess: Boolean = true, canChangeHistoryVisibility: Boolean = true, From b8ab0491b8ce160fe2614d4ea3d15eeec72d2208 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 8 Jan 2026 17:24:55 +0100 Subject: [PATCH 18/22] Fix SecurityAndPrivacy "manage spaces" footer text --- .../securityandprivacy/impl/root/SecurityAndPrivacyView.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt index 3ebb2de282..f4207ae3af 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt @@ -279,7 +279,8 @@ private fun RoomAccessSection( if (state.showManageSpaceFooter) { val footerText = stringWithLink( textRes = R.string.screen_security_and_privacy_room_access_footer, - url = stringResource(R.string.screen_security_and_privacy_room_access_footer_manage_spaces_action), + url = "", + linkTextRes = R.string.screen_security_and_privacy_room_access_footer_manage_spaces_action, onLinkClick = { onManageSpacesClick() }, ) Text( From fb29ae14be247f035c6bc463c7acd124356a5bbf Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 8 Jan 2026 17:25:24 +0100 Subject: [PATCH 19/22] quality: move tests to matching package --- .../SecurityAndPrivacyPresenterTest.kt | 20 +++++++++---------- .../{ => root}/SecurityAndPrivacyViewTest.kt | 18 ++++++----------- 2 files changed, 16 insertions(+), 22 deletions(-) rename features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/{ => root}/SecurityAndPrivacyPresenterTest.kt (98%) rename features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/{ => root}/SecurityAndPrivacyViewTest.kt (90%) diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyPresenterTest.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenterTest.kt similarity index 98% rename from features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyPresenterTest.kt rename to features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenterTest.kt index bf8ab13bad..2c1611fec8 100644 --- a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyPresenterTest.kt +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenterTest.kt @@ -1,18 +1,15 @@ /* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. + * Copyright (c) 2026 Element Creations Ltd. * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.securityandprivacy.impl +package io.element.android.features.securityandprivacy.impl.root import com.google.common.truth.Truth.assertThat -import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyEvent -import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyHistoryVisibility -import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyPresenter -import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyRoomAccess +import io.element.android.features.securityandprivacy.impl.FakeSecurityAndPrivacyNavigator +import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNavigator import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.featureflag.api.FeatureFlagService @@ -214,7 +211,8 @@ class SecurityAndPrivacyPresenterTest { @Test fun `present - edit room address`() = runTest { val openEditRoomAddressLambda = lambdaRecorder { } - val navigator = FakeSecurityAndPrivacyNavigator(openEditRoomAddressLambda = openEditRoomAddressLambda) + val navigator = + FakeSecurityAndPrivacyNavigator(openEditRoomAddressLambda = openEditRoomAddressLambda) val presenter = createSecurityAndPrivacyPresenter(navigator = navigator) presenter.test { skipItems(1) @@ -439,7 +437,8 @@ class SecurityAndPrivacyPresenterTest { @Test fun `present - SelectSpaceMemberAccess with multiple spaces opens ManageAuthorizedSpaces`() = runTest { val openManageAuthorizedSpacesLambda = lambdaRecorder { } - val navigator = FakeSecurityAndPrivacyNavigator(openManageAuthorizedSpacesLambda = openManageAuthorizedSpacesLambda) + val navigator = + FakeSecurityAndPrivacyNavigator(openManageAuthorizedSpacesLambda = openManageAuthorizedSpacesLambda) val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( roomPermissions = roomPermissions(), @@ -607,7 +606,8 @@ class SecurityAndPrivacyPresenterTest { @Test fun `present - SelectAskToJoinWithSpaceMembersAccess with multiple spaces opens ManageAuthorizedSpaces`() = runTest { val openManageAuthorizedSpacesLambda = lambdaRecorder { } - val navigator = FakeSecurityAndPrivacyNavigator(openManageAuthorizedSpacesLambda = openManageAuthorizedSpacesLambda) + val navigator = + FakeSecurityAndPrivacyNavigator(openManageAuthorizedSpacesLambda = openManageAuthorizedSpacesLambda) val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( roomPermissions = roomPermissions(), diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyViewTest.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyViewTest.kt similarity index 90% rename from features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyViewTest.kt rename to features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyViewTest.kt index 7cd9b154cf..a1f46b2938 100644 --- a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyViewTest.kt +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyViewTest.kt @@ -1,12 +1,11 @@ /* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. + * Copyright (c) 2026 Element Creations Ltd. * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.securityandprivacy.impl +package io.element.android.features.securityandprivacy.impl.root import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.AndroidComposeTestRule @@ -14,14 +13,7 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyEvent -import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyHistoryVisibility -import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyRoomAccess -import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyState -import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyView -import io.element.android.features.securityandprivacy.impl.root.SpaceSelectionMode -import io.element.android.features.securityandprivacy.impl.root.aSecurityAndPrivacySettings -import io.element.android.features.securityandprivacy.impl.root.aSecurityAndPrivacyState +import io.element.android.features.securityandprivacy.impl.R import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.test.A_ROOM_ID @@ -222,7 +214,9 @@ class SecurityAndPrivacyViewTest { ) rule.setSecurityAndPrivacyView(state) // The footer text uses AnnotatedString with a link. Verify the footer text is displayed. - rule.onNodeWithText("Choose which spaces", substring = true).assertExists() + val actionFooterText = rule.activity.getString(R.string.screen_security_and_privacy_room_access_footer_manage_spaces_action) + val footerText = rule.activity.getString(R.string.screen_security_and_privacy_room_access_footer, actionFooterText) + rule.onNodeWithText(footerText).assertExists() } } From c66f8c8c3425e81d473f430b52f44f4f6fd43dbf Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 8 Jan 2026 16:48:16 +0000 Subject: [PATCH 20/22] Update screenshots --- ...yandprivacy.impl.root_SecurityAndPrivacyViewDark_14_en.png | 4 ++-- ...yandprivacy.impl.root_SecurityAndPrivacyViewDark_15_en.png | 4 ++-- ...yandprivacy.impl.root_SecurityAndPrivacyViewDark_16_en.png | 4 ++-- ...tyandprivacy.impl.root_SecurityAndPrivacyViewDark_4_en.png | 4 ++-- ...tyandprivacy.impl.root_SecurityAndPrivacyViewDark_5_en.png | 4 ++-- ...tyandprivacy.impl.root_SecurityAndPrivacyViewDark_6_en.png | 4 ++-- ...andprivacy.impl.root_SecurityAndPrivacyViewLight_14_en.png | 4 ++-- ...andprivacy.impl.root_SecurityAndPrivacyViewLight_15_en.png | 4 ++-- ...andprivacy.impl.root_SecurityAndPrivacyViewLight_16_en.png | 4 ++-- ...yandprivacy.impl.root_SecurityAndPrivacyViewLight_4_en.png | 4 ++-- ...yandprivacy.impl.root_SecurityAndPrivacyViewLight_5_en.png | 4 ++-- ...yandprivacy.impl.root_SecurityAndPrivacyViewLight_6_en.png | 4 ++-- 12 files changed, 24 insertions(+), 24 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_14_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_14_en.png index 71f935b3c0..04d7a13388 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_14_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3b2a019695bb544f7bfbb2b2c5fddc5140a25a770695d1354c2b393e942ebba5 -size 24687 +oid sha256:da260572a274c3fedcc81b055e1c52b8cf7467692287aa176b0dd9171be76937 +size 25139 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_15_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_15_en.png index 8b34eb8946..8eb93a77e0 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_15_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_15_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:adaadbcdcb1d281683fcd7cfb69604ed875ffd4a48233dfd2941d3997ab097de -size 51431 +oid sha256:da888123f37a0dcc7d80849216044b82a442052c900d636d006020ad42469a71 +size 51849 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_16_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_16_en.png index 8b34eb8946..8eb93a77e0 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_16_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_16_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:adaadbcdcb1d281683fcd7cfb69604ed875ffd4a48233dfd2941d3997ab097de -size 51431 +oid sha256:da888123f37a0dcc7d80849216044b82a442052c900d636d006020ad42469a71 +size 51849 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_4_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_4_en.png index 00573c56c2..d320c0dc11 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1605f3269a888d8cbcab71615816b7564072f26846e481cf3576f9de4b61f2e1 -size 44182 +oid sha256:64134bc23ef38b73a087ec5679784516bfd66f23dd675a3771e1aa8cf6159836 +size 44729 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_5_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_5_en.png index 9b03e75694..9de784287c 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77213c9b75eea8880c5fac86c3345e7056f304c8ad5beb6cf2057cb5d67b5415 -size 58621 +oid sha256:56d1614ce5e0da0bc29f8184f045339e263087676e9d7908a94007f410924b5d +size 59154 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_6_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_6_en.png index 9b03e75694..9de784287c 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77213c9b75eea8880c5fac86c3345e7056f304c8ad5beb6cf2057cb5d67b5415 -size 58621 +oid sha256:56d1614ce5e0da0bc29f8184f045339e263087676e9d7908a94007f410924b5d +size 59154 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_14_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_14_en.png index e050bddd3e..c200e6a21c 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_14_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c251b0e7daf758b0031c1c81b130d06f295126cb6bbc8efd13f43c6ef581ad8e -size 25132 +oid sha256:20a302e03d4f10c912c249f7c26f8ecc06472dd2a2ad4abe91607bb7527de732 +size 25577 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_15_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_15_en.png index 3a1c1d3bf9..e0f67fd111 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_15_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_15_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cfadbdeaf77af829c19ae33338e91679acc8b34d2c8493d20994a740e2d2f3ee -size 52901 +oid sha256:9939e7a38e0e280d42a996576a3cfbeb773625cf43ddf8afd04aad6ab35fd955 +size 53555 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_16_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_16_en.png index 3a1c1d3bf9..e0f67fd111 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_16_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_16_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cfadbdeaf77af829c19ae33338e91679acc8b34d2c8493d20994a740e2d2f3ee -size 52901 +oid sha256:9939e7a38e0e280d42a996576a3cfbeb773625cf43ddf8afd04aad6ab35fd955 +size 53555 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_4_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_4_en.png index 97f3d826b9..264166a069 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3cf16df2caf7cdf9ca4a2da151ffa4c7b362b9d22bf9706522dacbd4edf520e5 -size 45867 +oid sha256:093bb9672ecd80432c7e6102bd5ed2d1184cd384c91e479d99d13c0ba773bf78 +size 46448 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_5_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_5_en.png index bd71e00714..920c64d380 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b77f723d2c6c5eba85660d93cdcd63d785bccd61aab37f0784ca1fa646280743 -size 60462 +oid sha256:ee80f7a765a453c3704af6d1f38f90637528bcf1e5966dc1bd016f4d34f21fa4 +size 61069 diff --git a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_6_en.png b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_6_en.png index bd71e00714..920c64d380 100644 --- a/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b77f723d2c6c5eba85660d93cdcd63d785bccd61aab37f0784ca1fa646280743 -size 60462 +oid sha256:ee80f7a765a453c3704af6d1f38f90637528bcf1e5966dc1bd016f4d34f21fa4 +size 61069 From 993cf838a05b80bf0ee04f1a2c1fee5d35196dec Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 9 Jan 2026 12:00:54 +0100 Subject: [PATCH 21/22] Refactor space selection to use SpaceSelectionStateHolder Move authorized space selection state to a shared StateHolder scoped to RoomScope. This simplifies communication between SecurityAndPrivacy and ManageAuthorizedSpaces nodes by replacing the complex coroutine-based parent-child coordination with a reactive state flow pattern. --- .../impl/SecurityAndPrivacyFlowNode.kt | 23 +---- .../impl/SecurityAndPrivacyNavigator.kt | 6 +- .../ManageAuthorizedSpacesEvent.kt | 2 +- .../ManageAuthorizedSpacesNode.kt | 11 --- .../ManageAuthorizedSpacesPresenter.kt | 39 ++++---- .../ManageAuthorizedSpacesState.kt | 12 +-- .../ManageAuthorizedSpacesStateProvider.kt | 25 ++--- .../ManageAuthorizedSpacesView.kt | 29 +++--- .../SpaceSelectionState.kt | 64 ++++++++++++ .../impl/root/SecurityAndPrivacyNode.kt | 16 --- .../impl/root/SecurityAndPrivacyPresenter.kt | 98 ++++++++++++++++--- .../impl/root/SecurityAndPrivacyState.kt | 15 --- .../impl/FakeSecurityAndPrivacyNavigator.kt | 6 +- .../impl/SecurityAndPrivacyFlowNodeTest.kt | 19 +--- .../ManageAuthorizedSpacesPresenterTest.kt | 85 +++++++++++----- .../ManageAuthorizedSpacesViewTest.kt | 33 +++---- .../root/SecurityAndPrivacyPresenterTest.kt | 78 ++------------- 17 files changed, 283 insertions(+), 278 deletions(-) create mode 100644 features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/SpaceSelectionState.kt diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt index 4628974295..c306264773 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt @@ -13,14 +13,12 @@ import androidx.annotation.VisibleForTesting import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.lifecycle.Lifecycle -import androidx.lifecycle.coroutineScope import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle 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.activeElement import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode @@ -36,12 +34,10 @@ import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.powerlevels.use -import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize @ContributesNode(RoomScope::class) @@ -66,7 +62,7 @@ class SecurityAndPrivacyFlowNode( data object EditRoomAddress : NavTarget @Parcelize - data class ManageAuthorizedSpaces(val forKnockRestricted: Boolean = false) : NavTarget + data object ManageAuthorizedSpaces : NavTarget } private val callback: SecurityAndPrivacyEntryPoint.Callback = callback() @@ -90,21 +86,6 @@ class SecurityAndPrivacyFlowNode( callback.onDone() } } - whenChildrenAttached { - commonLifecycle: Lifecycle, - securityAndPrivacyNode: SecurityAndPrivacyNode, - manageAuthorizedSpacesNode: ManageAuthorizedSpacesNode - -> - commonLifecycle.coroutineScope.launch { - val authorizedSpacesData = securityAndPrivacyNode.getAuthorizedSpacesData() - val selectedSpaces = manageAuthorizedSpacesNode.waitForCompletion(authorizedSpacesData) - val forKnock = (backstack.activeElement as? NavTarget.ManageAuthorizedSpaces)?.forKnockRestricted ?: false - withContext(NonCancellable) { - navigator.closeManageAuthorizedSpaces() - securityAndPrivacyNode.onAuthorizedSpacesSelected(selectedSpaces, forKnock = forKnock) - } - } - } } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -115,7 +96,7 @@ class SecurityAndPrivacyFlowNode( NavTarget.EditRoomAddress -> { createNode(buildContext, plugins = listOf(navigator)) } - is NavTarget.ManageAuthorizedSpaces -> { + NavTarget.ManageAuthorizedSpaces -> { createNode(buildContext, plugins = listOf(navigator)) } } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyNavigator.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyNavigator.kt index 0944ae92bd..274bf0b823 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyNavigator.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyNavigator.kt @@ -18,7 +18,7 @@ interface SecurityAndPrivacyNavigator : Plugin { fun onDone() fun openEditRoomAddress() fun closeEditRoomAddress() - fun openManageAuthorizedSpaces(forKnockRestricted: Boolean) + fun openManageAuthorizedSpaces() fun closeManageAuthorizedSpaces() } @@ -38,8 +38,8 @@ class BackstackSecurityAndPrivacyNavigator( backStack.pop() } - override fun openManageAuthorizedSpaces(forKnockRestricted: Boolean) { - backStack.push(SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces(forKnockRestricted)) + override fun openManageAuthorizedSpaces() { + backStack.push(SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces) } override fun closeManageAuthorizedSpaces() { diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesEvent.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesEvent.kt index 47abe2fbce..37d70b65f5 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesEvent.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesEvent.kt @@ -11,7 +11,7 @@ package io.element.android.features.securityandprivacy.impl.manageauthorizedspac import io.element.android.libraries.matrix.api.core.RoomId sealed interface ManageAuthorizedSpacesEvent { - data class SetData(val data: AuthorizedSpacesSelection) : ManageAuthorizedSpacesEvent + data object Cancel : ManageAuthorizedSpacesEvent data object Done : ManageAuthorizedSpacesEvent data class ToggleSpace(val roomId: RoomId) : ManageAuthorizedSpacesEvent } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt index 08c757b87d..709f2189bc 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt @@ -19,12 +19,8 @@ import com.bumble.appyx.core.plugin.plugins import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode -import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNavigator import io.element.android.libraries.architecture.appyx.launchMolecule import io.element.android.libraries.di.RoomScope -import io.element.android.libraries.matrix.api.core.RoomId -import kotlinx.collections.immutable.ImmutableList -import kotlinx.coroutines.flow.first @ContributesNode(RoomScope::class) @AssistedInject @@ -33,20 +29,13 @@ class ManageAuthorizedSpacesNode( @Assisted plugins: List, presenter: ManageAuthorizedSpacesPresenter, ) : Node(buildContext, plugins = plugins) { - private val navigator = plugins().first() private val stateFlow = launchMolecule { presenter.present() } - suspend fun waitForCompletion(data: AuthorizedSpacesSelection): ImmutableList { - stateFlow.value.eventSink(ManageAuthorizedSpacesEvent.SetData(data)) - return stateFlow.first { it.isSelectionComplete }.selectedIds - } - @Composable override fun View(modifier: Modifier) { val state by stateFlow.collectAsState() ManageAuthorizedSpacesView( state = state, - onBackClick = { navigator.closeManageAuthorizedSpaces() }, modifier = modifier ) } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt index aa2251408f..5922e910a2 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt @@ -9,46 +9,43 @@ package io.element.android.features.securityandprivacy.impl.manageauthorizedspaces import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.matrix.api.core.RoomId -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @Inject -class ManageAuthorizedSpacesPresenter : Presenter { +class ManageAuthorizedSpacesPresenter( + private val spaceSelectionStateHolder: SpaceSelectionStateHolder, +) : Presenter { @Composable override fun present(): ManageAuthorizedSpacesState { - var selectedIds: ImmutableList by remember { mutableStateOf(persistentListOf()) } - var spacesSelection by remember { mutableStateOf(AuthorizedSpacesSelection()) } - var isSelectionComplete by remember { mutableStateOf(false) } - + val spaceSelectionState by spaceSelectionStateHolder.state.collectAsState() fun handleEvent(event: ManageAuthorizedSpacesEvent) { when (event) { - ManageAuthorizedSpacesEvent.Done -> isSelectionComplete = true is ManageAuthorizedSpacesEvent.ToggleSpace -> { - selectedIds = if (selectedIds.contains(event.roomId)) { - selectedIds.minus(event.roomId).toImmutableList() + val currentSelectedIds = spaceSelectionState.selectedSpaceIds + val newSelectedIds = if (currentSelectedIds.contains(event.roomId)) { + currentSelectedIds.minus(event.roomId).toImmutableList() } else { - selectedIds.plus(event.roomId).toImmutableList() + currentSelectedIds.plus(event.roomId).toImmutableList() } + spaceSelectionStateHolder.updateSelectedSpaceIds(newSelectedIds) } - is ManageAuthorizedSpacesEvent.SetData -> { - spacesSelection = event.data - selectedIds = event.data.initialSelectedIds + ManageAuthorizedSpacesEvent.Done -> { + spaceSelectionStateHolder.setCompletion(SpaceSelectionState.Completion.Completed) + } + ManageAuthorizedSpacesEvent.Cancel -> { + spaceSelectionStateHolder.setCompletion(SpaceSelectionState.Completion.Cancelled) } } } return ManageAuthorizedSpacesState( - selection = spacesSelection, - selectedIds = selectedIds, - isSelectionComplete = isSelectionComplete, + selectableSpaces = spaceSelectionState.selectableSpaces, + unknownSpaceIds = spaceSelectionState.unknownSpaceIds, + selectedIds = spaceSelectionState.selectedSpaceIds, eventSink = ::handleEvent, ) } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt index 291cd7c760..b729fc12fa 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt @@ -11,19 +11,13 @@ package io.element.android.features.securityandprivacy.impl.manageauthorizedspac 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.persistentListOf +import kotlinx.collections.immutable.ImmutableSet data class ManageAuthorizedSpacesState( - val selection: AuthorizedSpacesSelection, + val selectableSpaces: ImmutableSet, + val unknownSpaceIds: ImmutableList, val selectedIds: ImmutableList, - val isSelectionComplete: Boolean, val eventSink: (ManageAuthorizedSpacesEvent) -> Unit ) { val isDoneButtonEnabled = selectedIds.isNotEmpty() } - -data class AuthorizedSpacesSelection( - val joinedSpaces: ImmutableList = persistentListOf(), - val unknownSpaceIds: ImmutableList = persistentListOf(), - val initialSelectedIds: ImmutableList = persistentListOf() -) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt index d818e31317..8c55af54e5 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt @@ -14,21 +14,17 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.spaces.SpaceRoom import io.element.android.libraries.previewutils.room.aSpaceRoom import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableSet open class ManageAuthorizedSpacesStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aManageAuthorizedSpacesState(), aManageAuthorizedSpacesState( - authorizedSpacesSelection = anAuthorizedSpaceSelection( - unknownSpaceIds = listOf(aRoomId(99)) - ) + unknownSpaceIds = listOf(aRoomId(99)) ), aManageAuthorizedSpacesState( selectedIds = listOf(aRoomId(1), aRoomId(3)), - authorizedSpacesSelection = anAuthorizedSpaceSelection( - initialSelectedIds = listOf(aRoomId(1)), - ), ), ) } @@ -49,23 +45,14 @@ private fun aSpaceRoomList(count: Int): List { } } -fun anAuthorizedSpaceSelection( - joinedSpaces: List = aSpaceRoomList(5), +fun aManageAuthorizedSpacesState( + selectableSpaces: List = aSpaceRoomList(5), unknownSpaceIds: List = emptyList(), - initialSelectedIds: List = emptyList(), -) = AuthorizedSpacesSelection( - joinedSpaces = joinedSpaces.toImmutableList(), - unknownSpaceIds = unknownSpaceIds.toImmutableList(), - initialSelectedIds = initialSelectedIds.toImmutableList(), -) - -private fun aManageAuthorizedSpacesState( - authorizedSpacesSelection: AuthorizedSpacesSelection = anAuthorizedSpaceSelection(), selectedIds: List = emptyList(), eventSink: (ManageAuthorizedSpacesEvent) -> Unit = {}, ) = ManageAuthorizedSpacesState( - selection = authorizedSpacesSelection, + selectableSpaces = selectableSpaces.toImmutableSet(), + unknownSpaceIds = unknownSpaceIds.toImmutableList(), selectedIds = selectedIds.toImmutableList(), - isSelectionComplete = false, eventSink = eventSink, ) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt index 022010f267..9b839b80db 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt @@ -8,6 +8,7 @@ package io.element.android.features.securityandprivacy.impl.manageauthorizedspaces +import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope @@ -42,17 +43,24 @@ import io.element.android.libraries.ui.strings.CommonStrings @Composable fun ManageAuthorizedSpacesView( state: ManageAuthorizedSpacesState, - onBackClick: () -> Unit, modifier: Modifier = Modifier, ) { + fun onCancel() { + state.eventSink(ManageAuthorizedSpacesEvent.Cancel) + } + + fun onDone() { + state.eventSink(ManageAuthorizedSpacesEvent.Done) + } + + BackHandler(onBack = ::onCancel) + Scaffold( modifier = modifier, topBar = { ManageAuthorizedSpacesTopBar( - onBackClick = onBackClick, - onDoneClick = { - state.eventSink(ManageAuthorizedSpacesEvent.Done) - }, + onBackClick = ::onCancel, + onDoneClick = ::onDone, isDoneButtonEnabled = state.isDoneButtonEnabled ) } @@ -67,7 +75,7 @@ fun ManageAuthorizedSpacesView( hasDivider = false, ) } - items(items = state.selection.joinedSpaces) { space -> + items(items = state.selectableSpaces.toList()) { space -> CheckableSpaceListItem( headlineText = space.displayName, supportingText = space.canonicalAlias?.value, @@ -80,14 +88,14 @@ fun ManageAuthorizedSpacesView( } ) } - if (state.selection.unknownSpaceIds.isNotEmpty()) { + if (state.unknownSpaceIds.isNotEmpty()) { item { ListSectionHeader( title = stringResource(R.string.screen_manage_authorized_spaces_unknown_spaces_section_title), hasDivider = true, ) } - items(items = state.selection.unknownSpaceIds) { + items(items = state.unknownSpaceIds) { CheckableSpaceListItem( headlineText = stringResource(R.string.screen_manage_authorized_spaces_unknown_space), supportingText = it.value, @@ -185,8 +193,5 @@ private fun ManageAuthorizedSpacesTopBar( internal fun ManageAuthorizedSpacesViewPreview( @PreviewParameter(ManageAuthorizedSpacesStateProvider::class) state: ManageAuthorizedSpacesState ) = ElementPreview { - ManageAuthorizedSpacesView( - state = state, - onBackClick = {}, - ) + ManageAuthorizedSpacesView(state = state) } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/SpaceSelectionState.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/SpaceSelectionState.kt new file mode 100644 index 0000000000..f24d1621b0 --- /dev/null +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/SpaceSelectionState.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * 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.securityandprivacy.impl.manageauthorizedspaces + +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn +import io.element.android.libraries.di.RoomScope +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.persistentListOf +import kotlinx.collections.immutable.persistentSetOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +data class SpaceSelectionState( + val selectableSpaces: ImmutableSet, + val unknownSpaceIds: ImmutableList, + val selectedSpaceIds: ImmutableList, + val completion: Completion, +) { + enum class Completion { + Initial, + Completed, + Cancelled, + } + + companion object { + val INITIAL = SpaceSelectionState( + selectableSpaces = persistentSetOf(), + unknownSpaceIds = persistentListOf(), + selectedSpaceIds = persistentListOf(), + completion = Completion.Initial, + ) + } +} + +@Inject +@SingleIn(RoomScope::class) +class SpaceSelectionStateHolder { + private val _state = MutableStateFlow(SpaceSelectionState.INITIAL) + val state: StateFlow = _state.asStateFlow() + + fun update(transform: (SpaceSelectionState) -> SpaceSelectionState) { + _state.update(transform) + } + + fun updateSelectedSpaceIds(selectedSpaceIds: ImmutableList) { + update { it.copy(selectedSpaceIds = selectedSpaceIds) } + } + + fun setCompletion(completion: SpaceSelectionState.Completion) { + update { it.copy(completion = completion) } + } +} diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyNode.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyNode.kt index 815b7bd35e..d5fb72e72e 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyNode.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyNode.kt @@ -23,12 +23,9 @@ import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode import io.element.android.compound.theme.ElementTheme import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNavigator -import io.element.android.features.securityandprivacy.impl.manageauthorizedspaces.AuthorizedSpacesSelection import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.architecture.appyx.launchMolecule import io.element.android.libraries.di.RoomScope -import io.element.android.libraries.matrix.api.core.RoomId -import kotlinx.collections.immutable.ImmutableList @ContributesNode(RoomScope::class) @AssistedInject @@ -46,19 +43,6 @@ class SecurityAndPrivacyNode( activity.openUrlInChromeCustomTab(null, darkTheme, url) } - fun getAuthorizedSpacesData(): AuthorizedSpacesSelection { - return stateFlow.value.getAuthorizedSpacesSelection() - } - - fun onAuthorizedSpacesSelected(selectedSpaces: ImmutableList, forKnock: Boolean) { - val roomAccess = if (forKnock) { - SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember(selectedSpaces) - } else { - SecurityAndPrivacyRoomAccess.SpaceMember(selectedSpaces) - } - stateFlow.value.eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(roomAccess)) - } - @Composable override fun View(modifier: Modifier) { val activity = requireNotNull(LocalActivity.current) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt index f80b96ca00..09873b6b86 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt @@ -26,6 +26,8 @@ import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyPerm import io.element.android.features.securityandprivacy.api.securityAndPrivacyPermissions import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNavigator import io.element.android.features.securityandprivacy.impl.editroomaddress.matchesServer +import io.element.android.features.securityandprivacy.impl.manageauthorizedspaces.SpaceSelectionState +import io.element.android.features.securityandprivacy.impl.manageauthorizedspaces.SpaceSelectionStateHolder import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter @@ -51,18 +53,22 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @AssistedInject class SecurityAndPrivacyPresenter( @Assisted private val navigator: SecurityAndPrivacyNavigator, + private val spaceSelectionStateHolder: SpaceSelectionStateHolder, private val matrixClient: MatrixClient, private val room: JoinedRoom, private val featureFlagService: FeatureFlagService, ) : Presenter { @AssistedFactory interface Factory { - fun create(navigator: SecurityAndPrivacyNavigator): SecurityAndPrivacyPresenter + fun create( + navigator: SecurityAndPrivacyNavigator, + ): SecurityAndPrivacyPresenter } @Composable @@ -136,6 +142,18 @@ class SecurityAndPrivacyPresenter( } } + LaunchedEffect(selectableJoinedSpaces, savedSettings.roomAccess) { + val unknownSpaceIds = savedSettings.roomAccess.spaceIds().filter { spaceId -> + selectableJoinedSpaces.none { it.roomId == spaceId } + }.toImmutableList() + spaceSelectionStateHolder.update { state -> + state.copy( + selectableSpaces = selectableJoinedSpaces, + unknownSpaceIds = unknownSpaceIds, + ) + } + } + var showEnableEncryptionConfirmation by remember(savedSettings.isEncrypted) { mutableStateOf(false) } val permissions by room.permissionsAsState(SecurityAndPrivacyPermissions.DEFAULT) { perms -> perms.securityAndPrivacyPermissions() @@ -191,19 +209,27 @@ class SecurityAndPrivacyPresenter( SecurityAndPrivacyEvent.DismissExitConfirmation -> { saveAction.value = AsyncAction.Uninitialized } - SecurityAndPrivacyEvent.ManageAuthorizedSpaces -> { - navigator.openManageAuthorizedSpaces( + SecurityAndPrivacyEvent.ManageAuthorizedSpaces -> coroutineScope.launch { + handleMultipleSelection( + savedAccess = savedSettings.roomAccess, + editedRoomAccess = editedRoomAccess, forKnockRestricted = editedRoomAccess.value is SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember ) } - SecurityAndPrivacyEvent.SelectSpaceMemberAccess -> handleSpaceMemberAccessSelection( - spaceSelectionMode = spaceSelectionMode, - editedAccess = editedRoomAccess, - ) - SecurityAndPrivacyEvent.SelectAskToJoinWithSpaceMembersAccess -> handleAskToJoinWithSpaceMembersAccessSelection( - spaceSelectionMode = spaceSelectionMode, - editedAccess = editedRoomAccess, - ) + SecurityAndPrivacyEvent.SelectSpaceMemberAccess -> coroutineScope.launch { + handleSpaceMemberAccessSelection( + spaceSelectionMode = spaceSelectionMode, + savedAccess = savedSettings.roomAccess, + editedAccess = editedRoomAccess, + ) + } + SecurityAndPrivacyEvent.SelectAskToJoinWithSpaceMembersAccess -> coroutineScope.launch { + handleAskToJoinWithSpaceMembersAccessSelection( + spaceSelectionMode = spaceSelectionMode, + savedAccess = savedSettings.roomAccess, + editedAccess = editedRoomAccess, + ) + } } } @@ -248,8 +274,9 @@ class SecurityAndPrivacyPresenter( return state } - private fun handleSpaceMemberAccessSelection( + private suspend fun handleSpaceMemberAccessSelection( spaceSelectionMode: SpaceSelectionMode, + savedAccess: SecurityAndPrivacyRoomAccess, editedAccess: MutableState, ) { if (editedAccess.value is SecurityAndPrivacyRoomAccess.SpaceMember) { @@ -257,7 +284,11 @@ class SecurityAndPrivacyPresenter( } when (spaceSelectionMode) { is SpaceSelectionMode.None -> Unit - is SpaceSelectionMode.Multiple -> navigator.openManageAuthorizedSpaces(forKnockRestricted = false) + is SpaceSelectionMode.Multiple -> handleMultipleSelection( + savedAccess = savedAccess, + editedRoomAccess = editedAccess, + forKnockRestricted = false, + ) is SpaceSelectionMode.Single -> { val newRoomAccess = SecurityAndPrivacyRoomAccess.SpaceMember( spaceIds = persistentListOf(spaceSelectionMode.spaceId) @@ -267,8 +298,9 @@ class SecurityAndPrivacyPresenter( } } - private fun handleAskToJoinWithSpaceMembersAccessSelection( + private suspend fun handleAskToJoinWithSpaceMembersAccessSelection( spaceSelectionMode: SpaceSelectionMode, + savedAccess: SecurityAndPrivacyRoomAccess, editedAccess: MutableState, ) { if (editedAccess.value is SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember) { @@ -276,7 +308,11 @@ class SecurityAndPrivacyPresenter( } when (spaceSelectionMode) { is SpaceSelectionMode.None -> Unit - is SpaceSelectionMode.Multiple -> navigator.openManageAuthorizedSpaces(forKnockRestricted = true) + is SpaceSelectionMode.Multiple -> handleMultipleSelection( + savedAccess = savedAccess, + editedRoomAccess = editedAccess, + forKnockRestricted = true, + ) is SpaceSelectionMode.Single -> { val newRoomAccess = SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember( spaceIds = persistentListOf(spaceSelectionMode.spaceId) @@ -286,6 +322,38 @@ class SecurityAndPrivacyPresenter( } } + private suspend fun handleMultipleSelection( + savedAccess: SecurityAndPrivacyRoomAccess, + editedRoomAccess: MutableState, + forKnockRestricted: Boolean + ) { + val initialSelection = when (val currentRoomAccess = editedRoomAccess.value) { + is SecurityAndPrivacyRoomAccess.SpaceMember -> currentRoomAccess.spaceIds + is SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember -> currentRoomAccess.spaceIds + else -> savedAccess.spaceIds() + } + spaceSelectionStateHolder.update { state -> + state.copy(selectedSpaceIds = initialSelection, completion = SpaceSelectionState.Completion.Initial) + } + navigator.openManageAuthorizedSpaces() + val newState = spaceSelectionStateHolder.state.first { it.completion != SpaceSelectionState.Completion.Initial } + when (newState.completion) { + SpaceSelectionState.Completion.Initial -> Unit + SpaceSelectionState.Completion.Cancelled -> { + navigator.closeManageAuthorizedSpaces() + } + SpaceSelectionState.Completion.Completed -> { + val selectedIds = newState.selectedSpaceIds + editedRoomAccess.value = if (forKnockRestricted) { + SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember(spaceIds = selectedIds) + } else { + SecurityAndPrivacyRoomAccess.SpaceMember(spaceIds = selectedIds) + } + navigator.closeManageAuthorizedSpaces() + } + } + } + private fun getSpaceSelectionMode( selectableJoinedSpaces: Set, savedAccess: SecurityAndPrivacyRoomAccess, diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt index ff627f6a0e..6ec47ba183 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt @@ -12,7 +12,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyPermissions import io.element.android.features.securityandprivacy.impl.R -import io.element.android.features.securityandprivacy.impl.manageauthorizedspaces.AuthorizedSpacesSelection import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.core.RoomId @@ -121,20 +120,6 @@ data class SecurityAndPrivacyState( stringResource(R.string.screen_security_and_privacy_ask_to_join_option_description) } } - - fun getAuthorizedSpacesSelection(): AuthorizedSpacesSelection { - return AuthorizedSpacesSelection( - joinedSpaces = selectableJoinedSpaces.toImmutableList(), - unknownSpaceIds = savedSettings.roomAccess.spaceIds().filter { spaceId -> - selectableJoinedSpaces.none { it.roomId == spaceId } - }.toImmutableList(), - initialSelectedIds = when (editedSettings.roomAccess) { - is SecurityAndPrivacyRoomAccess.SpaceMember -> editedSettings.roomAccess.spaceIds - is SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember -> editedSettings.roomAccess.spaceIds - else -> savedSettings.roomAccess.spaceIds() - } - ) - } } data class SecurityAndPrivacySettings( diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/FakeSecurityAndPrivacyNavigator.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/FakeSecurityAndPrivacyNavigator.kt index 5c5366caba..c0a7ca8e7f 100644 --- a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/FakeSecurityAndPrivacyNavigator.kt +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/FakeSecurityAndPrivacyNavigator.kt @@ -14,7 +14,7 @@ class FakeSecurityAndPrivacyNavigator( private val onDoneLambda: () -> Unit = { lambdaError() }, private val openEditRoomAddressLambda: () -> Unit = { lambdaError() }, private val closeEditRoomAddressLambda: () -> Unit = { lambdaError() }, - private val openManageAuthorizedSpacesLambda: (Boolean) -> Unit = { lambdaError() }, + private val openManageAuthorizedSpacesLambda: () -> Unit = { lambdaError() }, private val closeManageAuthorizedSpacesLambda: () -> Unit = { lambdaError() }, ) : SecurityAndPrivacyNavigator { override fun onDone() { @@ -29,8 +29,8 @@ class FakeSecurityAndPrivacyNavigator( closeEditRoomAddressLambda() } - override fun openManageAuthorizedSpaces(forKnockRestricted: Boolean) { - openManageAuthorizedSpacesLambda(forKnockRestricted) + override fun openManageAuthorizedSpaces() { + openManageAuthorizedSpacesLambda() } override fun closeManageAuthorizedSpaces() { diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNodeTest.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNodeTest.kt index baae82a586..a6f21c0162 100644 --- a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNodeTest.kt +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNodeTest.kt @@ -49,27 +49,16 @@ class SecurityAndPrivacyFlowNodeTest { } @Test - fun `openManageAuthorizedSpaces navigates with forKnockRestricted false`() = runTest { + fun `openManageAuthorizedSpaces navigates to ManageAuthorizedSpaces`() = runTest { val flowNode = createFlowNode() - flowNode.navigator.openManageAuthorizedSpaces(forKnockRestricted = false) - assertThat(flowNode.currentNavTarget()).isEqualTo( - SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces(forKnockRestricted = false) - ) - } - - @Test - fun `openManageAuthorizedSpaces navigates with forKnockRestricted true`() = runTest { - val flowNode = createFlowNode() - flowNode.navigator.openManageAuthorizedSpaces(forKnockRestricted = true) - assertThat(flowNode.currentNavTarget()).isEqualTo( - SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces(forKnockRestricted = true) - ) + flowNode.navigator.openManageAuthorizedSpaces() + assertThat(flowNode.currentNavTarget()).isEqualTo(SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces) } @Test fun `closeManageAuthorizedSpaces pops backstack`() = runTest { val flowNode = createFlowNode() - flowNode.navigator.openManageAuthorizedSpaces(forKnockRestricted = false) + flowNode.navigator.openManageAuthorizedSpaces() assertThat(flowNode.currentNavTarget()) .isInstanceOf(SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces::class.java) flowNode.navigator.closeManageAuthorizedSpaces() diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenterTest.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenterTest.kt index 7d678b23bb..8314aca4cd 100644 --- a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenterTest.kt +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenterTest.kt @@ -12,38 +12,33 @@ import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.tests.testutils.test import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentSetOf import kotlinx.coroutines.test.runTest import org.junit.Test class ManageAuthorizedSpacesPresenterTest { @Test - fun `present - initial state has empty selection`() = runTest { - val presenter = ManageAuthorizedSpacesPresenter() + fun `present - initial state reflects shared state`() = runTest { + val sharedStateHolder = SpaceSelectionStateHolder() + val presenter = ManageAuthorizedSpacesPresenter(sharedStateHolder) presenter.test { with(awaitItem()) { assertThat(selectedIds).isEmpty() - assertThat(isSelectionComplete).isFalse() assertThat(isDoneButtonEnabled).isFalse() } } } @Test - fun `present - SetData event updates selection and initial selectedIds`() = runTest { - val presenter = ManageAuthorizedSpacesPresenter() + fun `present - state reflects shared state with pre-selected spaces`() = runTest { + val sharedStateHolder = SpaceSelectionStateHolder() + val roomId = A_ROOM_ID + sharedStateHolder.update { + it.copy(selectedSpaceIds = persistentListOf(roomId)) + } + val presenter = ManageAuthorizedSpacesPresenter(sharedStateHolder) presenter.test { - val initialState = awaitItem() - val roomId = A_ROOM_ID - val data = AuthorizedSpacesSelection( - joinedSpaces = persistentListOf(), - unknownSpaceIds = persistentListOf(), - initialSelectedIds = persistentListOf(roomId) - ) - initialState.eventSink(ManageAuthorizedSpacesEvent.SetData(data)) - // SetData updates two state variables, which may emit intermediate states - skipItems(1) with(awaitItem()) { - assertThat(selection).isEqualTo(data) assertThat(selectedIds).containsExactly(roomId) assertThat(isDoneButtonEnabled).isTrue() } @@ -51,8 +46,9 @@ class ManageAuthorizedSpacesPresenterTest { } @Test - fun `present - ToggleSpace event adds space to selectedIds`() = runTest { - val presenter = ManageAuthorizedSpacesPresenter() + fun `present - ToggleSpace event adds space to selectedIds in shared state`() = runTest { + val sharedStateHolder = SpaceSelectionStateHolder() + val presenter = ManageAuthorizedSpacesPresenter(sharedStateHolder) presenter.test { val initialState = awaitItem() val roomId = A_ROOM_ID @@ -61,34 +57,69 @@ class ManageAuthorizedSpacesPresenterTest { assertThat(selectedIds).containsExactly(roomId) assertThat(isDoneButtonEnabled).isTrue() } + // Verify the shared state is also updated + assertThat(sharedStateHolder.state.value.selectedSpaceIds).containsExactly(roomId) } } @Test fun `present - ToggleSpace event removes space when already selected`() = runTest { - val presenter = ManageAuthorizedSpacesPresenter() + val sharedStateHolder = SpaceSelectionStateHolder() + sharedStateHolder.updateSelectedSpaceIds(persistentListOf(A_ROOM_ID)) + val presenter = ManageAuthorizedSpacesPresenter(sharedStateHolder) presenter.test { val initialState = awaitItem() - val roomId = A_ROOM_ID - initialState.eventSink(ManageAuthorizedSpacesEvent.ToggleSpace(roomId)) - val stateWithSelection = awaitItem() - assertThat(stateWithSelection.selectedIds).containsExactly(roomId) - stateWithSelection.eventSink(ManageAuthorizedSpacesEvent.ToggleSpace(roomId)) + assertThat(initialState.selectedIds).containsExactly(A_ROOM_ID) + initialState.eventSink(ManageAuthorizedSpacesEvent.ToggleSpace(A_ROOM_ID)) with(awaitItem()) { assertThat(selectedIds).isEmpty() assertThat(isDoneButtonEnabled).isFalse() } + // Verify the shared state is also updated + assertThat(sharedStateHolder.state.value.selectedSpaceIds).isEmpty() } } @Test - fun `present - Done event sets isSelectionComplete to true`() = runTest { - val presenter = ManageAuthorizedSpacesPresenter() + fun `present - Done event sets completion to Completed`() = runTest { + val sharedStateHolder = SpaceSelectionStateHolder() + val presenter = ManageAuthorizedSpacesPresenter(sharedStateHolder) presenter.test { val initialState = awaitItem() initialState.eventSink(ManageAuthorizedSpacesEvent.Done) + cancelAndIgnoreRemainingEvents() + assertThat(sharedStateHolder.state.value.completion) + .isEqualTo(SpaceSelectionState.Completion.Completed) + } + } + + @Test + fun `present - Cancel event sets completion to Cancelled`() = runTest { + val sharedStateHolder = SpaceSelectionStateHolder() + val presenter = ManageAuthorizedSpacesPresenter(sharedStateHolder) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(ManageAuthorizedSpacesEvent.Cancel) + cancelAndIgnoreRemainingEvents() + assertThat(sharedStateHolder.state.value.completion) + .isEqualTo(SpaceSelectionState.Completion.Cancelled) + } + } + + @Test + fun `present - displays spaces from shared state`() = runTest { + val sharedStateHolder = SpaceSelectionStateHolder() + sharedStateHolder.update { + it.copy( + selectableSpaces = persistentSetOf(), + unknownSpaceIds = persistentListOf(A_ROOM_ID), + ) + } + val presenter = ManageAuthorizedSpacesPresenter(sharedStateHolder) + presenter.test { with(awaitItem()) { - assertThat(isSelectionComplete).isTrue() + assertThat(selectableSpaces).isEmpty() + assertThat(unknownSpaceIds).containsExactly(A_ROOM_ID) } } } diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesViewTest.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesViewTest.kt index 515fe590a3..c732df6df0 100644 --- a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesViewTest.kt +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesViewTest.kt @@ -15,15 +15,15 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.spaces.SpaceRoom import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.previewutils.room.aSpaceRoom import io.element.android.libraries.ui.strings.CommonStrings -import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn -import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableSet import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule @@ -34,11 +34,12 @@ class ManageAuthorizedSpacesViewTest { @get:Rule val rule = createAndroidComposeRule() @Test - fun `clicking back invokes callback`() { - ensureCalledOnce { callback -> - rule.setManageAuthorizedSpacesView(onBackClick = callback) - rule.pressBack() - } + fun `clicking back emits Cancel event`() { + val recorder = EventsRecorder() + val state = aManageAuthorizedSpacesState(eventSink = recorder) + rule.setManageAuthorizedSpacesView(state) + rule.pressBack() + recorder.assertSingle(ManageAuthorizedSpacesEvent.Cancel) } @Test @@ -47,9 +48,7 @@ class ManageAuthorizedSpacesViewTest { val space = aSpaceRoom(roomId = roomId, displayName = "Test Space") val recorder = EventsRecorder() val state = aManageAuthorizedSpacesState( - selection = anAuthorizedSpaceSelection( - joinedSpaces = listOf(space) - ), + selectableSpaces = listOf(space), eventSink = recorder ) rule.setManageAuthorizedSpacesView(state) @@ -86,24 +85,20 @@ private fun AndroidComposeTestRule.setManag state: ManageAuthorizedSpacesState = aManageAuthorizedSpacesState( eventSink = EventsRecorder(expectEvents = false) ), - onBackClick: () -> Unit = EnsureNeverCalled(), ) { setContent { - ManageAuthorizedSpacesView( - state = state, - onBackClick = onBackClick, - ) + ManageAuthorizedSpacesView(state = state) } } private fun aManageAuthorizedSpacesState( - selection: AuthorizedSpacesSelection = AuthorizedSpacesSelection(), + selectableSpaces: List = emptyList(), + unknownSpaceIds: List = emptyList(), selectedIds: List = emptyList(), - isSelectionComplete: Boolean = false, eventSink: (ManageAuthorizedSpacesEvent) -> Unit = {}, ) = ManageAuthorizedSpacesState( - selection = selection, + selectableSpaces = selectableSpaces.toImmutableSet(), + unknownSpaceIds = unknownSpaceIds.toImmutableList(), selectedIds = selectedIds.toImmutableList(), - isSelectionComplete = isSelectionComplete, eventSink = eventSink, ) diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenterTest.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenterTest.kt index 2c1611fec8..e9cd49cc94 100644 --- a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenterTest.kt +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenterTest.kt @@ -10,6 +10,7 @@ package io.element.android.features.securityandprivacy.impl.root import com.google.common.truth.Truth.assertThat import io.element.android.features.securityandprivacy.impl.FakeSecurityAndPrivacyNavigator import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNavigator +import io.element.android.features.securityandprivacy.impl.manageauthorizedspaces.SpaceSelectionStateHolder import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.featureflag.api.FeatureFlagService @@ -436,7 +437,7 @@ class SecurityAndPrivacyPresenterTest { @Test fun `present - SelectSpaceMemberAccess with multiple spaces opens ManageAuthorizedSpaces`() = runTest { - val openManageAuthorizedSpacesLambda = lambdaRecorder { } + val openManageAuthorizedSpacesLambda = lambdaRecorder { } val navigator = FakeSecurityAndPrivacyNavigator(openManageAuthorizedSpacesLambda = openManageAuthorizedSpacesLambda) val room = FakeJoinedRoom( @@ -471,7 +472,7 @@ class SecurityAndPrivacyPresenterTest { val state = awaitItem() assertThat(state.isSpaceMemberSelectable).isTrue() state.eventSink(SecurityAndPrivacyEvent.SelectSpaceMemberAccess) - assert(openManageAuthorizedSpacesLambda).isCalledOnce().with(value(false)) + assert(openManageAuthorizedSpacesLambda).isCalledOnce() } } @@ -605,7 +606,7 @@ class SecurityAndPrivacyPresenterTest { @Test fun `present - SelectAskToJoinWithSpaceMembersAccess with multiple spaces opens ManageAuthorizedSpaces`() = runTest { - val openManageAuthorizedSpacesLambda = lambdaRecorder { } + val openManageAuthorizedSpacesLambda = lambdaRecorder { } val navigator = FakeSecurityAndPrivacyNavigator(openManageAuthorizedSpacesLambda = openManageAuthorizedSpacesLambda) val room = FakeJoinedRoom( @@ -642,7 +643,7 @@ class SecurityAndPrivacyPresenterTest { val state = awaitItem() assertThat(state.isAskToJoinWithSpaceMembersSelectable).isTrue() state.eventSink(SecurityAndPrivacyEvent.SelectAskToJoinWithSpaceMembersAccess) - assert(openManageAuthorizedSpacesLambda).isCalledOnce().with(value(true)) + assert(openManageAuthorizedSpacesLambda).isCalledOnce() } } @@ -950,73 +951,6 @@ class SecurityAndPrivacyPresenterTest { } } - @Test - fun `present - getAuthorizedSpacesSelection returns correct data for SpaceMember`() = runTest { - val room = FakeJoinedRoom( - baseRoom = FakeBaseRoom( - roomPermissions = roomPermissions(), - initialRoomInfo = aRoomInfo( - historyVisibility = RoomHistoryVisibility.Shared, - joinRule = JoinRule.Restricted( - rules = persistentListOf(AllowRule.RoomMembership(A_ROOM_ID)) - ) - ) - ) - ) - val spaceRoom = aSpaceRoom(roomId = A_ROOM_ID) - val client = FakeMatrixClient( - userIdServerNameLambda = { "matrix.org" }, - spaceService = FakeSpaceService( - joinedParentsResult = { _ -> Result.success(listOf(spaceRoom)) }, - getSpaceRoomResult = { null } - ) - ) - val presenter = createSecurityAndPrivacyPresenter( - room = room, - matrixClient = client, - featureFlagService = FakeFeatureFlagService( - initialState = mapOf(FeatureFlags.SpaceSettings.key to true) - ) - ) - presenter.test { - skipItems(1) - with(awaitItem()) { - val selection = getAuthorizedSpacesSelection() - assertThat(selection.joinedSpaces).containsExactly(spaceRoom) - assertThat(selection.initialSelectedIds).containsExactly(A_ROOM_ID) - assertThat(selection.unknownSpaceIds).isEmpty() - } - cancelAndIgnoreRemainingEvents() - } - } - - @Test - fun `present - getAuthorizedSpacesSelection identifies unknown space IDs`() = runTest { - val unknownSpaceId = RoomId("!unknown:matrix.org") - val room = FakeJoinedRoom( - baseRoom = FakeBaseRoom( - roomPermissions = roomPermissions(), - initialRoomInfo = aRoomInfo( - historyVisibility = RoomHistoryVisibility.Shared, - joinRule = JoinRule.Restricted( - rules = persistentListOf(AllowRule.RoomMembership(unknownSpaceId)) - ) - ) - ) - ) - // No spaces available (the space in the join rule is unknown) - val presenter = createSecurityAndPrivacyPresenter(room = room) - presenter.test { - skipItems(1) - with(awaitItem()) { - val selection = getAuthorizedSpacesSelection() - assertThat(selection.joinedSpaces).isEmpty() - assertThat(selection.unknownSpaceIds).containsExactly(unknownSpaceId) - } - cancelAndIgnoreRemainingEvents() - } - } - @Test fun `present - SelectAskToJoinWithSpaceMembersAccess with single space auto-selects`() = runTest { val room = FakeJoinedRoom( @@ -1169,12 +1103,14 @@ class SecurityAndPrivacyPresenterTest { getSpaceRoomResult = { null } ), ), + spaceSelectionStateHolder: SpaceSelectionStateHolder = SpaceSelectionStateHolder(), ): SecurityAndPrivacyPresenter { return SecurityAndPrivacyPresenter( room = room, matrixClient = matrixClient, navigator = navigator, featureFlagService = featureFlagService, + spaceSelectionStateHolder = spaceSelectionStateHolder, ) } } From a206de5a1537df7f7030930470db53ea79fa02cf Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 9 Jan 2026 17:52:52 +0100 Subject: [PATCH 22/22] quality : fix PR remarks --- .../impl/manageauthorizedspaces/ManageAuthorizedSpacesEvent.kt | 1 - .../impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt | 1 - .../manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt | 1 - .../impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt | 1 - .../ManageAuthorizedSpacesStateProvider.kt | 1 - .../impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt | 2 +- .../impl/manageauthorizedspaces/SpaceSelectionState.kt | 1 - 7 files changed, 1 insertion(+), 7 deletions(-) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesEvent.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesEvent.kt index 37d70b65f5..3b7460721c 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesEvent.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesEvent.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2025 Element Creations Ltd. - * 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. diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt index 709f2189bc..8414826d39 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2025 Element Creations Ltd. - * 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. diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt index 5922e910a2..cdb0d9801f 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2025 Element Creations Ltd. - * 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. diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt index b729fc12fa..bfea7d200c 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2025 Element Creations Ltd. - * 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. diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt index 8c55af54e5..d2fec941ff 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2025 Element Creations Ltd. - * 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. diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt index 9b839b80db..7208ee6115 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2025 Element Creations Ltd. - * 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. @@ -40,6 +39,7 @@ import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.ui.strings.CommonStrings +// Figma design: https://www.figma.com/design/kcnHxunG1LDWXsJhaNuiHz/ER-145--Spaces-on-Element-X?node-id=6361-86274&m=dev @Composable fun ManageAuthorizedSpacesView( state: ManageAuthorizedSpacesState, diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/SpaceSelectionState.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/SpaceSelectionState.kt index f24d1621b0..3df9e9bf1c 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/SpaceSelectionState.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/SpaceSelectionState.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2025 Element Creations Ltd. - * 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.