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 3d62e7b5d7d..c306264773b 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 @@ -24,6 +25,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,10 +60,15 @@ class SecurityAndPrivacyFlowNode( @Parcelize data object EditRoomAddress : NavTarget + + @Parcelize + data object ManageAuthorizedSpaces : NavTarget } private val callback: SecurityAndPrivacyEntryPoint.Callback = callback() - private val navigator = BackstackSecurityAndPrivacyNavigator(callback, backstack) + + @VisibleForTesting + val navigator = BackstackSecurityAndPrivacyNavigator(callback, backstack) override fun onBuilt() { super.onBuilt() @@ -89,6 +96,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 092da879434..274bf0b823f 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/ManageAuthorizedSpacesEvent.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesEvent.kt new file mode 100644 index 00000000000..3b7460721c6 --- /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. + * + * 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 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 new file mode 100644 index 00000000000..8414826d398 --- /dev/null +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2025 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.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 +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.libraries.architecture.appyx.launchMolecule +import io.element.android.libraries.di.RoomScope + +@ContributesNode(RoomScope::class) +@AssistedInject +class ManageAuthorizedSpacesNode( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenter: ManageAuthorizedSpacesPresenter, +) : Node(buildContext, plugins = plugins) { + private val stateFlow = launchMolecule { presenter.present() } + + @Composable + override fun View(modifier: Modifier) { + val state by stateFlow.collectAsState() + ManageAuthorizedSpacesView( + state = state, + 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 00000000000..cdb0d9801fe --- /dev/null +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2025 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.manageauthorizedspaces + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import dev.zacsweers.metro.Inject +import io.element.android.libraries.architecture.Presenter +import kotlinx.collections.immutable.toImmutableList + +@Inject +class ManageAuthorizedSpacesPresenter( + private val spaceSelectionStateHolder: SpaceSelectionStateHolder, +) : Presenter { + @Composable + override fun present(): ManageAuthorizedSpacesState { + val spaceSelectionState by spaceSelectionStateHolder.state.collectAsState() + fun handleEvent(event: ManageAuthorizedSpacesEvent) { + when (event) { + is ManageAuthorizedSpacesEvent.ToggleSpace -> { + val currentSelectedIds = spaceSelectionState.selectedSpaceIds + val newSelectedIds = if (currentSelectedIds.contains(event.roomId)) { + currentSelectedIds.minus(event.roomId).toImmutableList() + } else { + currentSelectedIds.plus(event.roomId).toImmutableList() + } + spaceSelectionStateHolder.updateSelectedSpaceIds(newSelectedIds) + } + ManageAuthorizedSpacesEvent.Done -> { + spaceSelectionStateHolder.setCompletion(SpaceSelectionState.Completion.Completed) + } + ManageAuthorizedSpacesEvent.Cancel -> { + spaceSelectionStateHolder.setCompletion(SpaceSelectionState.Completion.Cancelled) + } + } + } + + return ManageAuthorizedSpacesState( + 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 new file mode 100644 index 00000000000..bfea7d200c3 --- /dev/null +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2025 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.manageauthorizedspaces + +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 + +data class ManageAuthorizedSpacesState( + val selectableSpaces: ImmutableSet, + val unknownSpaceIds: ImmutableList, + val selectedIds: ImmutableList, + val eventSink: (ManageAuthorizedSpacesEvent) -> Unit +) { + val isDoneButtonEnabled = selectedIds.isNotEmpty() +} 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 00000000000..d2fec941ffe --- /dev/null +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2025 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.manageauthorizedspaces + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +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 +import kotlinx.collections.immutable.toImmutableSet + +open class ManageAuthorizedSpacesStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aManageAuthorizedSpacesState(), + aManageAuthorizedSpacesState( + unknownSpaceIds = listOf(aRoomId(99)) + ), + aManageAuthorizedSpacesState( + selectedIds = listOf(aRoomId(1), aRoomId(3)), + ), + ) +} + +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 + } + ) + } +} + +fun aManageAuthorizedSpacesState( + selectableSpaces: List = aSpaceRoomList(5), + unknownSpaceIds: List = emptyList(), + selectedIds: List = emptyList(), + eventSink: (ManageAuthorizedSpacesEvent) -> Unit = {}, +) = ManageAuthorizedSpacesState( + selectableSpaces = selectableSpaces.toImmutableSet(), + unknownSpaceIds = unknownSpaceIds.toImmutableList(), + selectedIds = selectedIds.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 new file mode 100644 index 00000000000..7208ee61157 --- /dev/null +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2025 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.manageauthorizedspaces + +import androidx.activity.compose.BackHandler +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.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, + modifier: Modifier = Modifier, +) { + fun onCancel() { + state.eventSink(ManageAuthorizedSpacesEvent.Cancel) + } + + fun onDone() { + state.eventSink(ManageAuthorizedSpacesEvent.Done) + } + + BackHandler(onBack = ::onCancel) + + Scaffold( + modifier = modifier, + topBar = { + ManageAuthorizedSpacesTopBar( + onBackClick = ::onCancel, + onDoneClick = ::onDone, + isDoneButtonEnabled = state.isDoneButtonEnabled + ) + } + ) { padding -> + 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.selectableSpaces.toList()) { space -> + CheckableSpaceListItem( + headlineText = space.displayName, + supportingText = space.canonicalAlias?.value, + avatarData = space.getAvatarData(AvatarSize.SpaceMember), + checked = state.selectedIds.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 = true, + ) + } + items(items = state.unknownSpaceIds) { + CheckableSpaceListItem( + headlineText = stringResource(R.string.screen_manage_authorized_spaces_unknown_space), + supportingText = it.value, + avatarData = null, + checked = state.selectedIds.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( + isDoneButtonEnabled: Boolean, + onBackClick: () -> Unit, + onDoneClick: () -> Unit, + modifier: Modifier = Modifier, +) { + TopAppBar( + modifier = modifier, + titleStr = stringResource(R.string.screen_manage_authorized_spaces_title), + navigationIcon = { BackButton(onClick = onBackClick) }, + actions = { + TextButton( + enabled = isDoneButtonEnabled, + text = stringResource(CommonStrings.action_done), + onClick = onDoneClick, + ) + } + ) +} + +@PreviewsDayNight +@Composable +internal fun ManageAuthorizedSpacesViewPreview( + @PreviewParameter(ManageAuthorizedSpacesStateProvider::class) state: ManageAuthorizedSpacesState +) = ElementPreview { + 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 00000000000..3df9e9bf1cf --- /dev/null +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/SpaceSelectionState.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2025 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.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/SecurityAndPrivacyEvent.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyEvent.kt index 39abedab8c7..d61f063a26d 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,17 @@ 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 + + // 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/SecurityAndPrivacyPresenter.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt index e627fef40c1..09873b6b86a 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 @@ -25,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 @@ -37,25 +40,35 @@ 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 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 @@ -65,6 +78,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() @@ -86,7 +103,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 +116,44 @@ 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(initialValue = persistentSetOf(), key1 = savedSettings.roomAccess.spaceIds()) { + val joinedParentSpaces = matrixClient + .spaceService + .joinedParents(room.roomId) + .getOrDefault(emptyList()) + + val nonParentJoinedSpaces = savedSettings.roomAccess + .spaceIds() + .mapNotNull { spaceId -> matrixClient.spaceService.getSpaceRoom(spaceId) } + + value = (joinedParentSpaces + nonParentJoinedSpaces).toImmutableSet() + } + + val spaceSelectionMode by remember { + derivedStateOf { + getSpaceSelectionMode(selectableJoinedSpaces, savedSettings.roomAccess) + } + } + + 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() @@ -122,7 +170,7 @@ class SecurityAndPrivacyPresenter( ) } is SecurityAndPrivacyEvent.ChangeRoomAccess -> { - editedRoomAccess = event.roomAccess + editedRoomAccess.value = event.roomAccess } is SecurityAndPrivacyEvent.ToggleEncryptionState -> { if (editedIsEncrypted) { @@ -161,6 +209,27 @@ class SecurityAndPrivacyPresenter( SecurityAndPrivacyEvent.DismissExitConfirmation -> { saveAction.value = AsyncAction.Uninitialized } + SecurityAndPrivacyEvent.ManageAuthorizedSpaces -> coroutineScope.launch { + handleMultipleSelection( + savedAccess = savedSettings.roomAccess, + editedRoomAccess = editedRoomAccess, + forKnockRestricted = editedRoomAccess.value is SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember + ) + } + SecurityAndPrivacyEvent.SelectSpaceMemberAccess -> coroutineScope.launch { + handleSpaceMemberAccessSelection( + spaceSelectionMode = spaceSelectionMode, + savedAccess = savedSettings.roomAccess, + editedAccess = editedRoomAccess, + ) + } + SecurityAndPrivacyEvent.SelectAskToJoinWithSpaceMembersAccess -> coroutineScope.launch { + handleAskToJoinWithSpaceMembersAccessSelection( + spaceSelectionMode = spaceSelectionMode, + savedAccess = savedSettings.roomAccess, + editedAccess = editedRoomAccess, + ) + } } } @@ -179,13 +248,16 @@ class SecurityAndPrivacyPresenter( saveAction = saveAction.value, permissions = permissions, isSpace = roomInfo.isSpace, + isSpaceSettingsEnabled = isSpaceSettingsEnabled, + selectableJoinedSpaces = selectableJoinedSpaces, + spaceSelectionMode = spaceSelectionMode, 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 +274,110 @@ class SecurityAndPrivacyPresenter( return state } + private suspend fun handleSpaceMemberAccessSelection( + spaceSelectionMode: SpaceSelectionMode, + savedAccess: SecurityAndPrivacyRoomAccess, + editedAccess: MutableState, + ) { + if (editedAccess.value is SecurityAndPrivacyRoomAccess.SpaceMember) { + return + } + when (spaceSelectionMode) { + is SpaceSelectionMode.None -> Unit + is SpaceSelectionMode.Multiple -> handleMultipleSelection( + savedAccess = savedAccess, + editedRoomAccess = editedAccess, + forKnockRestricted = false, + ) + is SpaceSelectionMode.Single -> { + val newRoomAccess = SecurityAndPrivacyRoomAccess.SpaceMember( + spaceIds = persistentListOf(spaceSelectionMode.spaceId) + ) + editedAccess.value = newRoomAccess + } + } + } + + private suspend fun handleAskToJoinWithSpaceMembersAccessSelection( + spaceSelectionMode: SpaceSelectionMode, + savedAccess: SecurityAndPrivacyRoomAccess, + editedAccess: MutableState, + ) { + if (editedAccess.value is SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember) { + return + } + when (spaceSelectionMode) { + is SpaceSelectionMode.None -> Unit + is SpaceSelectionMode.Multiple -> handleMultipleSelection( + savedAccess = savedAccess, + editedRoomAccess = editedAccess, + forKnockRestricted = true, + ) + is SpaceSelectionMode.Single -> { + val newRoomAccess = SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember( + spaceIds = persistentListOf(spaceSelectionMode.spaceId) + ) + editedAccess.value = newRoomAccess + } + } + } + + 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, + ): SpaceSelectionMode { + val selectableSpacesCount = (selectableJoinedSpaces.map { it.roomId } + savedAccess.spaceIds()).toSet().size + return when { + selectableSpacesCount == 0 -> SpaceSelectionMode.None + selectableSpacesCount > 1 -> SpaceSelectionMode.Multiple + else -> { + val joinedSpace = selectableJoinedSpaces.firstOrNull() + if (joinedSpace != null) { + SpaceSelectionMode.Single(joinedSpace.roomId, joinedSpace) + } else { + val spaceId = savedAccess.spaceIds().firstOrNull() + if (spaceId == null) { + SpaceSelectionMode.None + } else { + SpaceSelectionMode.Single(spaceId, null) + } + } + } + } + } + private fun CoroutineScope.isRoomVisibleInRoomDirectory(isRoomVisible: MutableState>) = launch { isRoomVisible.runUpdatingState { room.getRoomVisibility().map { it == RoomVisibility.Public } @@ -242,6 +418,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 } @@ -279,8 +456,19 @@ class SecurityAndPrivacyPresenter( private fun JoinRule?.map(): SecurityAndPrivacyRoomAccess { return when (this) { JoinRule.Public -> SecurityAndPrivacyRoomAccess.Anyone - JoinRule.Knock, is JoinRule.KnockRestricted -> SecurityAndPrivacyRoomAccess.AskToJoin - is JoinRule.Restricted -> SecurityAndPrivacyRoomAccess.SpaceMember + 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() + .map { it.roomId } + .toImmutableList() + ) JoinRule.Invite -> SecurityAndPrivacyRoomAccess.InviteOnly // All other cases are not supported so we default to InviteOnly is JoinRule.Custom, @@ -294,8 +482,12 @@ 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() + ) + 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 e64cf633a8b..6ec47ba183d 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,9 +8,17 @@ 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.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( @@ -20,12 +28,42 @@ data class SecurityAndPrivacyState( val editedSettings: SecurityAndPrivacySettings, val homeserverName: String, val showEnableEncryptionConfirmation: Boolean, - val isKnockEnabled: 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 spaceSelectionMode: SpaceSelectionMode, val eventSink: (SecurityAndPrivacyEvent) -> Unit ) { + val isSpaceMemberSelectable = isSpaceSettingsEnabled && spaceSelectionMode != SpaceSelectionMode.None + + // Show SpaceMember option in two cases: + // - 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 showManageSpaceFooter = spaceSelectionMode is SpaceSelectionMode.Multiple && + (editedSettings.roomAccess is SecurityAndPrivacyRoomAccess.SpaceMember || + editedSettings.roomAccess is SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember) + + val isAskToJoinSelectable = isKnockEnabled + + 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 // Logic is in https://github.com/element-hq/element-meta/issues/3029 @@ -48,6 +86,40 @@ 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) + } + } + + @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) + } + } } data class SecurityAndPrivacySettings( @@ -76,16 +148,31 @@ enum class SecurityAndPrivacyHistoryVisibility { } } -enum class SecurityAndPrivacyRoomAccess { - InviteOnly, - AskToJoin, - Anyone, - SpaceMember; +sealed interface SpaceSelectionMode { + data object None : SpaceSelectionMode + data class Single(val spaceId: RoomId, val spaceRoom: SpaceRoom?) : SpaceSelectionMode + data object Multiple : SpaceSelectionMode +} + +sealed interface SecurityAndPrivacyRoomAccess { + data object InviteOnly : 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, SpaceMember -> false - AskToJoin, Anyone -> true + InviteOnly, is SpaceMember -> false + 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 223d524ca34..95cb45d641e 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,9 @@ 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.toImmutableSet open class SecurityAndPrivacyStateProvider : PreviewParameterProvider { override val values: Sequence @@ -61,11 +64,27 @@ private fun commonSecurityAndPrivacyStates(isSpace: Boolean): Sequence = emptySet(), + spaceSelectionMode: SpaceSelectionMode = SpaceSelectionMode.None, + isSpaceSettingsEnabled: Boolean = true, eventSink: (SecurityAndPrivacyEvent) -> Unit = {} ) = SecurityAndPrivacyState( editedSettings = editedSettings, @@ -127,5 +149,8 @@ fun aSecurityAndPrivacyState( isKnockEnabled = isKnockEnabled, permissions = permissions, isSpace = isSpace, + selectableJoinedSpaces = selectableJoinedSpaces.toImmutableSet(), + 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 bf6a5ffdf29..f4207ae3af9 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,11 +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, - onSelectOption = { state.eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(it)) }, ) } if (state.showRoomVisibilitySections) { @@ -208,12 +205,27 @@ private fun SecurityAndPrivacySection( @Composable private fun RoomAccessSection( - edited: SecurityAndPrivacyRoomAccess, - saved: SecurityAndPrivacyRoomAccess, - isKnockEnabled: Boolean, - onSelectOption: (SecurityAndPrivacyRoomAccess) -> Unit, + state: SecurityAndPrivacyState, modifier: Modifier = Modifier, ) { + val edited = state.editedSettings.roomAccess + + fun onSelectOption(option: SecurityAndPrivacyRoomAccess) { + state.eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(option)) + } + + fun onSpaceMemberAccessClick() { + state.eventSink(SecurityAndPrivacyEvent.SelectSpaceMemberAccess) + } + + fun onAskToJoinWithSpaceMembersClick() { + state.eventSink(SecurityAndPrivacyEvent.SelectAskToJoinWithSpaceMembersAccess) + } + + fun onManageSpacesClick() { + state.eventSink(SecurityAndPrivacyEvent.ManageAuthorizedSpaces) + } + SecurityAndPrivacySection( title = stringResource(R.string.screen_security_and_privacy_room_access_section_header), modifier = modifier, @@ -225,29 +237,36 @@ 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. - if (saved == SecurityAndPrivacyRoomAccess.SpaceMember) { + 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 == SecurityAndPrivacyRoomAccess.SpaceMember, enabled = false), + trailingContent = ListItemContent.RadioButton(selected = state.editedSettings.roomAccess is SecurityAndPrivacyRoomAccess.SpaceMember), leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Space())), - enabled = false, + 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, + ) + } + if (state.showAskToJoinWithSpaceMemberOption) { + ListItem( + headlineContent = { Text(text = stringResource(R.string.screen_security_and_privacy_ask_to_join_option_title)) }, + supportingContent = { Text(text = state.askToJoinWithSpaceMembersDescription()) }, + trailingContent = ListItemContent.RadioButton(selected = edited is SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember), + onClick = ::onAskToJoinWithSpaceMembersClick, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserAdd())), + enabled = state.isAskToJoinWithSpaceMembersSelectable, ) } ListItem( @@ -257,6 +276,20 @@ private fun RoomAccessSection( leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Lock())), onClick = { onSelectOption(SecurityAndPrivacyRoomAccess.InviteOnly) }, ) + if (state.showManageSpaceFooter) { + val footerText = stringWithLink( + textRes = R.string.screen_security_and_privacy_room_access_footer, + url = "", + linkTextRes = 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) + ) + } } } 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 8d74c35733b..06d095cf4f0 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 eaf52430c71..60aa211ef15 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" "Toute personne se trouvant dans un espace autorisé peut participer, mais toutes les autres doivent demander l’accès." "Tout le monde doit demander un accès." 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 401fe806f81..b8905bef30f 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 658067be2d0..dfe1aee5588 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 701cb72370b..f3c62f24979 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 ee346dd5d0a..3853fc5f8aa 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 acf4b04e71e..902a89d7bd7 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/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 f90040d3fbd..c0a7ca8e7f3 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,6 +14,8 @@ class FakeSecurityAndPrivacyNavigator( private val onDoneLambda: () -> Unit = { lambdaError() }, private val openEditRoomAddressLambda: () -> Unit = { lambdaError() }, private val closeEditRoomAddressLambda: () -> Unit = { lambdaError() }, + private val openManageAuthorizedSpacesLambda: () -> 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() { + openManageAuthorizedSpacesLambda() + } + + override fun closeManageAuthorizedSpaces() { + closeManageAuthorizedSpacesLambda() + } } 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 00000000000..a6f21c01620 --- /dev/null +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNodeTest.kt @@ -0,0 +1,107 @@ +/* + * 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.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 to ManageAuthorizedSpaces`() = runTest { + val flowNode = createFlowNode() + flowNode.navigator.openManageAuthorizedSpaces() + assertThat(flowNode.currentNavTarget()).isEqualTo(SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces) + } + + @Test + fun `closeManageAuthorizedSpaces pops backstack`() = runTest { + val flowNode = createFlowNode() + flowNode.navigator.openManageAuthorizedSpaces() + 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 deleted file mode 100644 index c035b5510da..00000000000 --- a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyPresenterTest.kt +++ /dev/null @@ -1,421 +0,0 @@ -/* - * 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 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.libraries.architecture.AsyncAction -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.room.StateEventType -import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility -import io.element.android.libraries.matrix.api.room.join.JoinRule -import io.element.android.libraries.matrix.api.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.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.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.test -import kotlinx.coroutines.test.runTest -import org.junit.Test - -class SecurityAndPrivacyPresenterTest { - @Test - fun `present - initial states`() = runTest { - val presenter = createSecurityAndPrivacyPresenter() - presenter.test { - with(awaitItem()) { - assertThat(editedSettings).isEqualTo(savedSettings) - assertThat(canBeSaved).isFalse() - assertThat(showEnableEncryptionConfirmation).isFalse() - assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized) - assertThat(showRoomAccessSection).isFalse() - assertThat(showRoomVisibilitySections).isFalse() - assertThat(showHistoryVisibilitySection).isFalse() - assertThat(showEncryptionSection).isFalse() - assertThat(isKnockEnabled).isFalse() - } - with(awaitItem()) { - assertThat(editedSettings).isEqualTo(savedSettings) - assertThat(canBeSaved).isFalse() - assertThat(showEnableEncryptionConfirmation).isFalse() - assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized) - assertThat(showRoomAccessSection).isTrue() - assertThat(showRoomVisibilitySections).isFalse() - assertThat(showHistoryVisibilitySection).isTrue() - assertThat(showEncryptionSection).isTrue() - assertThat(isKnockEnabled).isFalse() - } - } - } - - @Test - fun `present - room info change updates saved and edited settings`() = runTest { - val room = FakeJoinedRoom( - baseRoom = FakeBaseRoom( - roomPermissions = roomPermissions(), - initialRoomInfo = aRoomInfo( - joinRule = JoinRule.Public, - historyVisibility = RoomHistoryVisibility.WorldReadable, - canonicalAlias = A_ROOM_ALIAS, - ) - ) - ) - val presenter = createSecurityAndPrivacyPresenter(room = room) - presenter.test { - skipItems(1) - with(awaitItem()) { - assertThat(editedSettings).isEqualTo(savedSettings) - assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.Anyone) - assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.WorldReadable) - assertThat(editedSettings.address).isEqualTo(A_ROOM_ALIAS.value) - assertThat(canBeSaved).isFalse() - } - cancelAndIgnoreRemainingEvents() - } - } - - @Test - fun `present - change room access`() = runTest { - val presenter = createSecurityAndPrivacyPresenter() - presenter.test { - skipItems(1) - with(awaitItem()) { - assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly) - assertThat(showRoomVisibilitySections).isFalse() - eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone)) - } - with(awaitItem()) { - assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.Anyone) - assertThat(showRoomVisibilitySections).isTrue() - assertThat(canBeSaved).isTrue() - eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.InviteOnly)) - } - with(awaitItem()) { - assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly) - assertThat(showRoomVisibilitySections).isFalse() - assertThat(canBeSaved).isFalse() - } - } - } - - @Test - fun `present - change history visibility`() = runTest { - val presenter = createSecurityAndPrivacyPresenter() - presenter.test { - skipItems(1) - with(awaitItem()) { - assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.Shared) - eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.Invited)) - } - with(awaitItem()) { - assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.Invited) - assertThat(canBeSaved).isTrue() - eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.Shared)) - } - with(awaitItem()) { - assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.Shared) - assertThat(canBeSaved).isFalse() - } - } - } - - @Test - fun `present - enable encryption`() = runTest { - val presenter = createSecurityAndPrivacyPresenter() - presenter.test { - skipItems(1) - with(awaitItem()) { - assertThat(editedSettings.isEncrypted).isFalse() - eventSink(SecurityAndPrivacyEvent.ToggleEncryptionState) - } - with(awaitItem()) { - assertThat(showEnableEncryptionConfirmation).isTrue() - eventSink(SecurityAndPrivacyEvent.CancelEnableEncryption) - } - with(awaitItem()) { - assertThat(showEnableEncryptionConfirmation).isFalse() - eventSink(SecurityAndPrivacyEvent.ToggleEncryptionState) - } - with(awaitItem()) { - assertThat(showEnableEncryptionConfirmation).isTrue() - eventSink(SecurityAndPrivacyEvent.ConfirmEnableEncryption) - } - skipItems(1) - with(awaitItem()) { - assertThat(editedSettings.isEncrypted).isTrue() - assertThat(showEnableEncryptionConfirmation).isFalse() - assertThat(canBeSaved).isTrue() - eventSink(SecurityAndPrivacyEvent.ToggleEncryptionState) - } - skipItems(1) - with(awaitItem()) { - assertThat(editedSettings.isEncrypted).isFalse() - assertThat(canBeSaved).isFalse() - } - } - } - - @Test - fun `present - room visibility loading and change`() = runTest { - val room = FakeJoinedRoom( - baseRoom = FakeBaseRoom( - roomPermissions = roomPermissions(), - getRoomVisibilityResult = { Result.success(RoomVisibility.Private) }, - initialRoomInfo = aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared) - ) - ) - val presenter = createSecurityAndPrivacyPresenter(room = room) - presenter.test { - skipItems(1) - with(awaitItem()) { - assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Loading()) - } - with(awaitItem()) { - assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(false)) - eventSink(SecurityAndPrivacyEvent.ToggleRoomVisibility) - } - with(awaitItem()) { - assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(true)) - assertThat(canBeSaved).isTrue() - eventSink(SecurityAndPrivacyEvent.ToggleRoomVisibility) - } - with(awaitItem()) { - assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(false)) - assertThat(canBeSaved).isFalse() - } - } - } - - @Test - fun `present - edit room address`() = runTest { - val openEditRoomAddressLambda = lambdaRecorder { } - val navigator = FakeSecurityAndPrivacyNavigator(openEditRoomAddressLambda = openEditRoomAddressLambda) - val presenter = createSecurityAndPrivacyPresenter(navigator = navigator) - presenter.test { - skipItems(1) - with(awaitItem()) { - eventSink(SecurityAndPrivacyEvent.EditRoomAddress) - } - assert(openEditRoomAddressLambda).isCalledOnce() - } - } - - @Test - fun `present - save success`() = runTest { - val enableEncryptionLambda = lambdaRecorder> { Result.success(Unit) } - val updateJoinRuleLambda = lambdaRecorder> { Result.success(Unit) } - val updateRoomVisibilityLambda = lambdaRecorder> { Result.success(Unit) } - val updateRoomHistoryVisibilityLambda = lambdaRecorder> { Result.success(Unit) } - val room = FakeJoinedRoom( - baseRoom = FakeBaseRoom( - roomPermissions = roomPermissions(), - getRoomVisibilityResult = { Result.success(RoomVisibility.Private) }, - initialRoomInfo = aRoomInfo(joinRule = JoinRule.Invite, historyVisibility = RoomHistoryVisibility.Shared) - ), - enableEncryptionResult = enableEncryptionLambda, - updateJoinRuleResult = updateJoinRuleLambda, - updateRoomVisibilityResult = updateRoomVisibilityLambda, - updateRoomHistoryVisibilityResult = updateRoomHistoryVisibilityLambda, - ) - 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) - eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone)) - } - with(awaitItem()) { - eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.WorldReadable)) - } - with(awaitItem()) { - assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.WorldReadable) - eventSink(SecurityAndPrivacyEvent.ConfirmEnableEncryption) - } - skipItems(1) - with(awaitItem()) { - assertThat(editedSettings.isEncrypted).isTrue() - eventSink(SecurityAndPrivacyEvent.ToggleRoomVisibility) - } - with(awaitItem()) { - assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(true)) - eventSink(SecurityAndPrivacyEvent.Save) - } - with(awaitItem()) { - assertThat(saveAction).isEqualTo(AsyncAction.Loading) - } - - room.givenRoomInfo( - aRoomInfo( - joinRule = JoinRule.Public, - historyVisibility = RoomHistoryVisibility.WorldReadable, - isEncrypted = true, - ) - ) - // Saved settings are updated 2 times to match the edited settings - skipItems(2) - with(awaitItem()) { - assertThat(saveAction).isEqualTo(AsyncAction.Success(Unit)) - assertThat(savedSettings).isEqualTo(editedSettings) - assertThat(canBeSaved).isFalse() - } - assert(enableEncryptionLambda).isCalledOnce() - assert(updateJoinRuleLambda).isCalledOnce() - assert(updateRoomVisibilityLambda).isCalledOnce() - assert(updateRoomHistoryVisibilityLambda).isCalledOnce() - onDoneLambda.assertions().isCalledOnce() - } - } - - @Test - fun `present - save failure`() = runTest { - val enableEncryptionLambda = lambdaRecorder> { Result.success(Unit) } - val updateJoinRuleLambda = lambdaRecorder> { Result.success(Unit) } - val updateRoomVisibilityLambda = lambdaRecorder> { - Result.failure(Exception("Failed to update room visibility")) - } - val updateRoomHistoryVisibilityLambda = lambdaRecorder> { Result.success(Unit) } - val room = FakeJoinedRoom( - baseRoom = FakeBaseRoom( - roomPermissions = roomPermissions(), - getRoomVisibilityResult = { Result.success(RoomVisibility.Private) }, - initialRoomInfo = aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared, joinRule = JoinRule.Private) - ), - enableEncryptionResult = enableEncryptionLambda, - updateJoinRuleResult = updateJoinRuleLambda, - updateRoomVisibilityResult = updateRoomVisibilityLambda, - updateRoomHistoryVisibilityResult = updateRoomHistoryVisibilityLambda, - ) - val presenter = createSecurityAndPrivacyPresenter(room = room) - presenter.test { - skipItems(1) - with(awaitItem()) { - assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly) - eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone)) - } - with(awaitItem()) { - eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.WorldReadable)) - } - with(awaitItem()) { - assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.WorldReadable) - eventSink(SecurityAndPrivacyEvent.ConfirmEnableEncryption) - } - skipItems(1) - with(awaitItem()) { - assertThat(editedSettings.isEncrypted).isTrue() - eventSink(SecurityAndPrivacyEvent.ToggleRoomVisibility) - } - with(awaitItem()) { - assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(true)) - eventSink(SecurityAndPrivacyEvent.Save) - } - with(awaitItem()) { - assertThat(saveAction).isEqualTo(AsyncAction.Loading) - } - - room.givenRoomInfo( - aRoomInfo( - joinRule = JoinRule.Public, - historyVisibility = RoomHistoryVisibility.WorldReadable, - ) - ) - // Saved settings are updated 2 times to match the edited settings - skipItems(2) - val state = awaitItem() - with(state) { - assertThat(saveAction).isInstanceOf(AsyncAction.Failure::class.java) - assertThat(savedSettings.isVisibleInRoomDirectory).isNotEqualTo(editedSettings.isVisibleInRoomDirectory) - assertThat(canBeSaved).isTrue() - } - assert(enableEncryptionLambda).isCalledOnce() - assert(updateJoinRuleLambda).isCalledOnce() - assert(updateRoomVisibilityLambda).isCalledOnce() - assert(updateRoomHistoryVisibilityLambda).isCalledOnce() - // Clear error - state.eventSink(SecurityAndPrivacyEvent.DismissSaveError) - with(awaitItem()) { - assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized) - } - } - } - - @Test - fun `present - isKnockEnabled is true if the Knock feature flag is enabled`() = runTest { - val presenter = createSecurityAndPrivacyPresenter( - featureFlagService = FakeFeatureFlagService( - initialState = mapOf( - FeatureFlags.Knock.key to true, - ) - ) - ) - presenter.test { - assertThat(awaitItem().isKnockEnabled).isFalse() - assertThat(awaitItem().isKnockEnabled).isTrue() - } - } - - private fun roomPermissions( - canChangeRoomAccess: Boolean = true, - canChangeHistoryVisibility: Boolean = true, - canChangeEncryption: Boolean = true, - canChangeRoomVisibility: Boolean = true, - ): RoomPermissions { - return FakeRoomPermissions( - canSendState = { eventType -> - when (eventType) { - StateEventType.RoomJoinRules -> canChangeRoomAccess - StateEventType.RoomHistoryVisibility -> canChangeHistoryVisibility - StateEventType.RoomEncryption -> canChangeEncryption - StateEventType.RoomCanonicalAlias -> canChangeRoomVisibility - else -> lambdaError() - } - } - ) - } - - private fun createSecurityAndPrivacyPresenter( - serverName: String = "matrix.org", - room: FakeJoinedRoom = FakeJoinedRoom( - baseRoom = FakeBaseRoom( - roomPermissions = roomPermissions(), - getRoomVisibilityResult = { Result.success(RoomVisibility.Private) }, - initialRoomInfo = aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared, joinRule = JoinRule.Private) - ), - ), - navigator: SecurityAndPrivacyNavigator = FakeSecurityAndPrivacyNavigator(), - featureFlagService: FeatureFlagService = FakeFeatureFlagService(), - ): SecurityAndPrivacyPresenter { - return SecurityAndPrivacyPresenter( - room = room, - matrixClient = FakeMatrixClient( - userIdServerNameLambda = { serverName }, - ), - navigator = navigator, - featureFlagService = featureFlagService, - ) - } -} 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 00000000000..8314aca4cd1 --- /dev/null +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenterTest.kt @@ -0,0 +1,126 @@ +/* + * 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.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 reflects shared state`() = runTest { + val sharedStateHolder = SpaceSelectionStateHolder() + val presenter = ManageAuthorizedSpacesPresenter(sharedStateHolder) + presenter.test { + with(awaitItem()) { + assertThat(selectedIds).isEmpty() + assertThat(isDoneButtonEnabled).isFalse() + } + } + } + + @Test + 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 { + with(awaitItem()) { + assertThat(selectedIds).containsExactly(roomId) + assertThat(isDoneButtonEnabled).isTrue() + } + } + } + + @Test + 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 + initialState.eventSink(ManageAuthorizedSpacesEvent.ToggleSpace(roomId)) + with(awaitItem()) { + 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 sharedStateHolder = SpaceSelectionStateHolder() + sharedStateHolder.updateSelectedSpaceIds(persistentListOf(A_ROOM_ID)) + val presenter = ManageAuthorizedSpacesPresenter(sharedStateHolder) + presenter.test { + val initialState = awaitItem() + 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 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(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 new file mode 100644 index 00000000000..c732df6df0b --- /dev/null +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesViewTest.kt @@ -0,0 +1,104 @@ +/* + * 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.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.EventsRecorder +import io.element.android.tests.testutils.clickOn +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 +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ManageAuthorizedSpacesViewTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `clicking back emits Cancel event`() { + val recorder = EventsRecorder() + val state = aManageAuthorizedSpacesState(eventSink = recorder) + rule.setManageAuthorizedSpacesView(state) + rule.pressBack() + recorder.assertSingle(ManageAuthorizedSpacesEvent.Cancel) + } + + @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( + selectableSpaces = 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) + ), +) { + setContent { + ManageAuthorizedSpacesView(state = state) + } +} + +private fun aManageAuthorizedSpacesState( + selectableSpaces: List = emptyList(), + unknownSpaceIds: List = emptyList(), + selectedIds: List = emptyList(), + eventSink: (ManageAuthorizedSpacesEvent) -> Unit = {}, +) = ManageAuthorizedSpacesState( + selectableSpaces = selectableSpaces.toImmutableSet(), + unknownSpaceIds = unknownSpaceIds.toImmutableList(), + selectedIds = selectedIds.toImmutableList(), + 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 new file mode 100644 index 00000000000..e9cd49cc94d --- /dev/null +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenterTest.kt @@ -0,0 +1,1116 @@ +/* + * 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.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 +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 + +@Suppress("LargeClass") +class SecurityAndPrivacyPresenterTest { + @Test + fun `present - initial states`() = runTest { + val presenter = createSecurityAndPrivacyPresenter() + presenter.test { + with(awaitItem()) { + assertThat(editedSettings).isEqualTo(savedSettings) + assertThat(canBeSaved).isFalse() + assertThat(showEnableEncryptionConfirmation).isFalse() + assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized) + assertThat(showRoomAccessSection).isFalse() + assertThat(showRoomVisibilitySections).isFalse() + assertThat(showHistoryVisibilitySection).isFalse() + assertThat(showEncryptionSection).isFalse() + } + with(awaitItem()) { + assertThat(editedSettings).isEqualTo(savedSettings) + assertThat(canBeSaved).isFalse() + assertThat(showEnableEncryptionConfirmation).isFalse() + assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized) + assertThat(showRoomAccessSection).isTrue() + assertThat(showRoomVisibilitySections).isFalse() + assertThat(showHistoryVisibilitySection).isTrue() + assertThat(showEncryptionSection).isTrue() + } + } + } + + @Test + fun `present - room info change updates saved and edited settings`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + initialRoomInfo = aRoomInfo( + joinRule = JoinRule.Public, + historyVisibility = RoomHistoryVisibility.WorldReadable, + canonicalAlias = A_ROOM_ALIAS, + ) + ) + ) + val presenter = createSecurityAndPrivacyPresenter(room = room) + presenter.test { + skipItems(1) + with(awaitItem()) { + assertThat(editedSettings).isEqualTo(savedSettings) + assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.Anyone) + assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.WorldReadable) + assertThat(editedSettings.address).isEqualTo(A_ROOM_ALIAS.value) + assertThat(canBeSaved).isFalse() + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - change room access`() = runTest { + val presenter = createSecurityAndPrivacyPresenter() + presenter.test { + skipItems(1) + with(awaitItem()) { + assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly) + assertThat(showRoomVisibilitySections).isFalse() + eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone)) + } + with(awaitItem()) { + assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.Anyone) + assertThat(showRoomVisibilitySections).isTrue() + assertThat(canBeSaved).isTrue() + eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.InviteOnly)) + } + with(awaitItem()) { + assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly) + assertThat(showRoomVisibilitySections).isFalse() + assertThat(canBeSaved).isFalse() + } + } + } + + @Test + fun `present - change history visibility`() = runTest { + val presenter = createSecurityAndPrivacyPresenter() + presenter.test { + skipItems(1) + with(awaitItem()) { + assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.Shared) + eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.Invited)) + } + with(awaitItem()) { + assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.Invited) + assertThat(canBeSaved).isTrue() + eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.Shared)) + } + with(awaitItem()) { + assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.Shared) + assertThat(canBeSaved).isFalse() + } + } + } + + @Test + fun `present - enable encryption`() = runTest { + val presenter = createSecurityAndPrivacyPresenter() + presenter.test { + skipItems(1) + with(awaitItem()) { + assertThat(editedSettings.isEncrypted).isFalse() + eventSink(SecurityAndPrivacyEvent.ToggleEncryptionState) + } + with(awaitItem()) { + assertThat(showEnableEncryptionConfirmation).isTrue() + eventSink(SecurityAndPrivacyEvent.CancelEnableEncryption) + } + with(awaitItem()) { + assertThat(showEnableEncryptionConfirmation).isFalse() + eventSink(SecurityAndPrivacyEvent.ToggleEncryptionState) + } + with(awaitItem()) { + assertThat(showEnableEncryptionConfirmation).isTrue() + eventSink(SecurityAndPrivacyEvent.ConfirmEnableEncryption) + } + skipItems(1) + with(awaitItem()) { + assertThat(editedSettings.isEncrypted).isTrue() + assertThat(showEnableEncryptionConfirmation).isFalse() + assertThat(canBeSaved).isTrue() + eventSink(SecurityAndPrivacyEvent.ToggleEncryptionState) + } + skipItems(1) + with(awaitItem()) { + assertThat(editedSettings.isEncrypted).isFalse() + assertThat(canBeSaved).isFalse() + } + } + } + + @Test + fun `present - room visibility loading and change`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + getRoomVisibilityResult = { Result.success(RoomVisibility.Private) }, + initialRoomInfo = aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared) + ) + ) + val presenter = createSecurityAndPrivacyPresenter(room = room) + presenter.test { + skipItems(1) + with(awaitItem()) { + assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Loading()) + } + with(awaitItem()) { + assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(false)) + eventSink(SecurityAndPrivacyEvent.ToggleRoomVisibility) + } + with(awaitItem()) { + assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(true)) + assertThat(canBeSaved).isTrue() + eventSink(SecurityAndPrivacyEvent.ToggleRoomVisibility) + } + with(awaitItem()) { + assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(false)) + assertThat(canBeSaved).isFalse() + } + } + } + + @Test + fun `present - edit room address`() = runTest { + val openEditRoomAddressLambda = lambdaRecorder { } + val navigator = + FakeSecurityAndPrivacyNavigator(openEditRoomAddressLambda = openEditRoomAddressLambda) + val presenter = createSecurityAndPrivacyPresenter(navigator = navigator) + presenter.test { + skipItems(1) + with(awaitItem()) { + eventSink(SecurityAndPrivacyEvent.EditRoomAddress) + } + assert(openEditRoomAddressLambda).isCalledOnce() + } + } + + @Test + fun `present - save success`() = runTest { + val enableEncryptionLambda = lambdaRecorder> { Result.success(Unit) } + val updateJoinRuleLambda = lambdaRecorder> { Result.success(Unit) } + val updateRoomVisibilityLambda = lambdaRecorder> { Result.success(Unit) } + val updateRoomHistoryVisibilityLambda = lambdaRecorder> { Result.success(Unit) } + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + getRoomVisibilityResult = { Result.success(RoomVisibility.Private) }, + initialRoomInfo = aRoomInfo(joinRule = JoinRule.Invite, historyVisibility = RoomHistoryVisibility.Shared) + ), + enableEncryptionResult = enableEncryptionLambda, + updateJoinRuleResult = updateJoinRuleLambda, + updateRoomVisibilityResult = updateRoomVisibilityLambda, + updateRoomHistoryVisibilityResult = updateRoomHistoryVisibilityLambda, + ) + 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) + eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone)) + } + with(awaitItem()) { + eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.WorldReadable)) + } + with(awaitItem()) { + assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.WorldReadable) + eventSink(SecurityAndPrivacyEvent.ConfirmEnableEncryption) + } + skipItems(1) + with(awaitItem()) { + assertThat(editedSettings.isEncrypted).isTrue() + eventSink(SecurityAndPrivacyEvent.ToggleRoomVisibility) + } + with(awaitItem()) { + assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(true)) + eventSink(SecurityAndPrivacyEvent.Save) + } + with(awaitItem()) { + assertThat(saveAction).isEqualTo(AsyncAction.Loading) + } + + room.givenRoomInfo( + aRoomInfo( + joinRule = JoinRule.Public, + historyVisibility = RoomHistoryVisibility.WorldReadable, + isEncrypted = true, + ) + ) + // Saved settings are updated 2 times to match the edited settings + skipItems(2) + with(awaitItem()) { + assertThat(saveAction).isEqualTo(AsyncAction.Success(Unit)) + assertThat(savedSettings).isEqualTo(editedSettings) + assertThat(canBeSaved).isFalse() + } + assert(enableEncryptionLambda).isCalledOnce() + assert(updateJoinRuleLambda).isCalledOnce() + assert(updateRoomVisibilityLambda).isCalledOnce() + assert(updateRoomHistoryVisibilityLambda).isCalledOnce() + onDoneLambda.assertions().isCalledOnce() + } + } + + @Test + fun `present - save failure`() = runTest { + val enableEncryptionLambda = lambdaRecorder> { Result.success(Unit) } + val updateJoinRuleLambda = lambdaRecorder> { Result.success(Unit) } + val updateRoomVisibilityLambda = lambdaRecorder> { + Result.failure(Exception("Failed to update room visibility")) + } + val updateRoomHistoryVisibilityLambda = lambdaRecorder> { Result.success(Unit) } + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + getRoomVisibilityResult = { Result.success(RoomVisibility.Private) }, + initialRoomInfo = aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared, joinRule = JoinRule.Private) + ), + enableEncryptionResult = enableEncryptionLambda, + updateJoinRuleResult = updateJoinRuleLambda, + updateRoomVisibilityResult = updateRoomVisibilityLambda, + updateRoomHistoryVisibilityResult = updateRoomHistoryVisibilityLambda, + ) + val presenter = createSecurityAndPrivacyPresenter(room = room) + presenter.test { + skipItems(1) + with(awaitItem()) { + assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly) + eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone)) + } + with(awaitItem()) { + eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.WorldReadable)) + } + with(awaitItem()) { + assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.WorldReadable) + eventSink(SecurityAndPrivacyEvent.ConfirmEnableEncryption) + } + skipItems(1) + with(awaitItem()) { + assertThat(editedSettings.isEncrypted).isTrue() + eventSink(SecurityAndPrivacyEvent.ToggleRoomVisibility) + } + with(awaitItem()) { + assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(true)) + eventSink(SecurityAndPrivacyEvent.Save) + } + with(awaitItem()) { + assertThat(saveAction).isEqualTo(AsyncAction.Loading) + } + + room.givenRoomInfo( + aRoomInfo( + joinRule = JoinRule.Public, + historyVisibility = RoomHistoryVisibility.WorldReadable, + ) + ) + // Saved settings are updated 2 times to match the edited settings + skipItems(2) + val state = awaitItem() + with(state) { + assertThat(saveAction).isInstanceOf(AsyncAction.Failure::class.java) + assertThat(savedSettings.isVisibleInRoomDirectory).isNotEqualTo(editedSettings.isVisibleInRoomDirectory) + assertThat(canBeSaved).isTrue() + } + assert(enableEncryptionLambda).isCalledOnce() + assert(updateJoinRuleLambda).isCalledOnce() + assert(updateRoomVisibilityLambda).isCalledOnce() + assert(updateRoomHistoryVisibilityLambda).isCalledOnce() + // Clear error + state.eventSink(SecurityAndPrivacyEvent.DismissSaveError) + with(awaitItem()) { + assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized) + } + } + } + + @Test + 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.SpaceSettings.key to true, + ) + ) + ) + presenter.test { + 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() + } + } + + @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() + } + } + + @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() + } + } + } + + @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 - 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, + canChangeEncryption: Boolean = true, + canChangeRoomVisibility: Boolean = true, + ): RoomPermissions { + return FakeRoomPermissions( + canSendState = { eventType -> + when (eventType) { + StateEventType.RoomJoinRules -> canChangeRoomAccess + StateEventType.RoomHistoryVisibility -> canChangeHistoryVisibility + StateEventType.RoomEncryption -> canChangeEncryption + StateEventType.RoomCanonicalAlias -> canChangeRoomVisibility + else -> lambdaError() + } + } + ) + } + + private fun createSecurityAndPrivacyPresenter( + serverName: String = "matrix.org", + room: FakeJoinedRoom = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + getRoomVisibilityResult = { Result.success(RoomVisibility.Private) }, + initialRoomInfo = aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared, joinRule = JoinRule.Private) + ), + ), + navigator: SecurityAndPrivacyNavigator = FakeSecurityAndPrivacyNavigator(), + featureFlagService: FeatureFlagService = FakeFeatureFlagService(), + matrixClient: MatrixClient = FakeMatrixClient( + userIdServerNameLambda = { serverName }, + spaceService = FakeSpaceService( + joinedParentsResult = { Result.success(emptyList()) }, + getSpaceRoomResult = { null } + ), + ), + spaceSelectionStateHolder: SpaceSelectionStateHolder = SpaceSelectionStateHolder(), + ): SecurityAndPrivacyPresenter { + return SecurityAndPrivacyPresenter( + room = room, + matrixClient = matrixClient, + navigator = navigator, + featureFlagService = featureFlagService, + spaceSelectionStateHolder = spaceSelectionStateHolder, + ) + } +} 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 75% 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 b15bc2fe37c..a1f46b29381 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,20 +13,16 @@ 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.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 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 @@ -179,6 +174,50 @@ 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. + 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() + } } private fun AndroidComposeTestRule.setSecurityAndPrivacyView( 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 6f5ba674ece..8f005b0225a 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 ba816c11c78..fad698c7315 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 eaa36ee750d..d796c0b538b 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) } 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 67d323e77b6..b2f08ecb654 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 f5083fd7073..1a6ffc8271a 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -435,11 +435,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 c8b3c10cbc1..0ea527dca8e 100644 --- a/libraries/ui-strings/src/main/res/values-hr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-hr/translations.xml @@ -443,11 +443,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 3863a24fd80..f588e6bdcaa 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 9e56d09b169..69f2aba019a 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 09280109182..3c8f4f16efa 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 df47b71577e..3a108277baa 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -435,11 +435,6 @@ 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" "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/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 00000000000..1fb6e305bca --- /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 00000000000..1fb6e305bca --- /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 00000000000..327f3175765 --- /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 00000000000..0e84fd507e9 --- /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 00000000000..0e84fd507e9 --- /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 00000000000..85f1bc55ea2 --- /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 5015b5c9957..46ad8a30d52 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 fbef6afae3a..6b5c72706b8 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 8abe7a325a2..5015b5c9957 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 3989ce8d6e2..fbef6afae3a 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 fbef6afae3a..04d7a133883 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: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 8bb18a41533..8eb93a77e07 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: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 81ed09d87ee..8eb93a77e07 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:da888123f37a0dcc7d80849216044b82a442052c900d636d006020ad42469a71 +size 51849 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 14683ef0b0a..3989ce8d6e2 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 810bb5531ac..fbef6afae3a 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 0bd3c0413dc..8bb18a41533 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 00000000000..81ed09d87ee --- /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 00000000000..14683ef0b0a --- /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 00000000000..810bb5531ac --- /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 00000000000..0bd3c0413dc --- /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 beeea224860..d320c0dc11b 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: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 b39a2f040c6..9de784287c9 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: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 f29bc630f12..9de784287c9 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:56d1614ce5e0da0bc29f8184f045339e263087676e9d7908a94007f410924b5d +size 59154 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 e5fda5dfd7a..b39a2f040c6 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 46ad8a30d52..f29bc630f12 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 6b5c72706b8..e5fda5dfd7a 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 da192161cb1..83f51a3be75 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 4ddc1b913dd..eea66871f9d 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 32138581f71..da192161cb1 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 d274eddbe1d..4ddc1b913dd 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 4ddc1b913dd..c200e6a21cf 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: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 e939df2248a..e0f67fd111a 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: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 0ba776c5fd9..e0f67fd111a 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:9939e7a38e0e280d42a996576a3cfbeb773625cf43ddf8afd04aad6ab35fd955 +size 53555 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 5932151ec46..d274eddbe1d 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 8919639fe08..4ddc1b913dd 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 bce9a377e8d..e939df2248a 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 00000000000..0ba776c5fd9 --- /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 00000000000..5932151ec46 --- /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 00000000000..8919639fe08 --- /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 00000000000..bce9a377e8d --- /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 a3ddf8b3540..264166a0694 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: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 085ac47033b..920c64d3808 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: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 0914a151e42..920c64d3808 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:ee80f7a765a453c3704af6d1f38f90637528bcf1e5966dc1bd016f4d34f21fa4 +size 61069 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 c9a3183eea8..085ac47033b 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 83f51a3be75..0914a151e42 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 eea66871f9d..c9a3183eea8 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 diff --git a/tools/localazy/config.json b/tools/localazy/config.json index a8572fb660d..3894befcb08 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\\..*" ] } ]