From 002325c57498e34ebc424c156c987a2fbe07e55d Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 8 Jan 2025 17:39:09 +0100 Subject: [PATCH 01/30] feat(security&privacy) : first implementation of ui --- .../SecurityAndPrivacyEvents.kt | 16 + .../SecurityAndPrivacyNode.kt | 36 ++ .../SecurityAndPrivacyPresenter.kt | 54 +++ .../SecurityAndPrivacyState.kt | 51 +++ .../SecurityAndPrivacyStateProvider.kt | 74 ++++ .../SecurityAndPrivacyView.kt | 351 ++++++++++++++++++ 6 files changed, 582 insertions(+) create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNode.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt new file mode 100644 index 00000000000..72af1449a9c --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.roomdetails.impl.securityandprivacy + +sealed interface SecurityAndPrivacyEvents { + data object Save : SecurityAndPrivacyEvents + data class ChangeRoomAccess(val roomAccess: SecurityAndPrivacyRoomAccess) : SecurityAndPrivacyEvents + data object EnableEncryption: SecurityAndPrivacyEvents + data class ChangeHistoryVisibility(val historyVisibility: SecurityAndPrivacyHistoryVisibility) : SecurityAndPrivacyEvents + data class ChangeVisibleInRoomDirectory(val isVisibleInRoomDirectory: Boolean) : SecurityAndPrivacyEvents +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNode.kt new file mode 100644 index 00000000000..3a200cb24c1 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNode.kt @@ -0,0 +1,36 @@ +/* + * 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.roomdetails.impl.securityandprivacy + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.di.RoomScope + +@ContributesNode(RoomScope::class) +class SecurityAndPrivacyNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: SecurityAndPrivacyPresenter, +) : Node(buildContext, plugins = plugins) { + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + SecurityAndPrivacyView( + state = state, + onBackClick = ::navigateUp, + modifier = modifier + ) + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt new file mode 100644 index 00000000000..6568704d9c3 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt @@ -0,0 +1,54 @@ +/* + * 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.roomdetails.impl.securityandprivacy + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.architecture.Presenter +import java.util.Optional +import javax.inject.Inject + +class SecurityAndPrivacyPresenter @Inject constructor() : Presenter { + + @Composable + override fun present(): SecurityAndPrivacyState { + + val savedSettings by remember { + mutableStateOf( + SecurityAndPrivacySettings( + roomAccess = SecurityAndPrivacyRoomAccess.InviteOnly, + isEncrypted = true, + isVisibleInRoomDirectory = Optional.empty(), + historyVisibility = Optional.empty(), + formattedAddress = Optional.empty(), + ) + ) + } + + fun handleEvents(event: SecurityAndPrivacyEvents) { + when (event) { + SecurityAndPrivacyEvents.Save -> {} + is SecurityAndPrivacyEvents.ChangeRoomAccess -> {} + is SecurityAndPrivacyEvents.EnableEncryption -> {} + is SecurityAndPrivacyEvents.ChangeHistoryVisibility -> {} + is SecurityAndPrivacyEvents.ChangeVisibleInRoomDirectory -> {} + } + } + + return SecurityAndPrivacyState( + savedSettings = savedSettings, + currentSettings = savedSettings, + homeserverName = "", + canBeSaved = true, + eventSink = ::handleEvents + ) + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt new file mode 100644 index 00000000000..5d598f4c53d --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt @@ -0,0 +1,51 @@ +/* + * 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.roomdetails.impl.securityandprivacy + +import io.element.android.libraries.architecture.AsyncData +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +data class SecurityAndPrivacyState( + val savedSettings: SecurityAndPrivacySettings, + val currentSettings: SecurityAndPrivacySettings, + val homeserverName: String, + val canBeSaved: Boolean, + val eventSink: (SecurityAndPrivacyEvents) -> Unit +) { + val showRoomVisibilitySections = currentSettings.roomAccess != SecurityAndPrivacyRoomAccess.InviteOnly && currentSettings.historyVisibility.isPresent + + val availableHistoryVisibilities = buildSet { + add(SecurityAndPrivacyHistoryVisibility.SinceSelection) + if (currentSettings.roomAccess == SecurityAndPrivacyRoomAccess.Anyone && !currentSettings.isEncrypted) { + add(SecurityAndPrivacyHistoryVisibility.Anyone) + } else { + add(SecurityAndPrivacyHistoryVisibility.SinceInvite) + } + if (savedSettings.historyVisibility.getOrNull() == SecurityAndPrivacyHistoryVisibility.SinceInvite) { + add(SecurityAndPrivacyHistoryVisibility.SinceInvite) + } + } + val showRoomHistoryVisibilitySection = availableHistoryVisibilities.isNotEmpty() && currentSettings.historyVisibility.isPresent +} + +data class SecurityAndPrivacySettings( + val roomAccess: SecurityAndPrivacyRoomAccess, + val isEncrypted: Boolean, + val historyVisibility: Optional, + val formattedAddress: Optional, + val isVisibleInRoomDirectory: Optional> +) + +enum class SecurityAndPrivacyHistoryVisibility { + SinceSelection, SinceInvite, Anyone +} + +enum class SecurityAndPrivacyRoomAccess { + InviteOnly, AskToJoin, Anyone, SpaceMember +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt new file mode 100644 index 00000000000..2807763c6ee --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.roomdetails.impl.securityandprivacy + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncData +import java.util.Optional + +open class SecurityAndPrivacyStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aSecurityAndPrivacyState(), + aSecurityAndPrivacyState( + currentSettings = aSecurityAndPrivacySettings( + roomAccess = SecurityAndPrivacyRoomAccess.AskToJoin + ) + ), + aSecurityAndPrivacyState( + currentSettings = aSecurityAndPrivacySettings( + roomAccess = SecurityAndPrivacyRoomAccess.Anyone, + isEncrypted = false, + ) + ), + aSecurityAndPrivacyState( + currentSettings = aSecurityAndPrivacySettings( + roomAccess = SecurityAndPrivacyRoomAccess.SpaceMember + ) + ), + aSecurityAndPrivacyState( + currentSettings = aSecurityAndPrivacySettings( + isVisibleInRoomDirectory = Optional.of(AsyncData.Loading()) + ) + ), + aSecurityAndPrivacyState( + currentSettings = aSecurityAndPrivacySettings( + isVisibleInRoomDirectory = Optional.of(AsyncData.Success(true)) + ) + ), + aSecurityAndPrivacyState(canBeSaved = false) + ) +} + +fun aSecurityAndPrivacySettings( + roomAccess: SecurityAndPrivacyRoomAccess = SecurityAndPrivacyRoomAccess.InviteOnly, + isEncrypted: Boolean = true, + formattedAddress: Optional = Optional.empty(), + historyVisibility: Optional = Optional.of(SecurityAndPrivacyHistoryVisibility.SinceSelection), + isVisibleInRoomDirectory: Optional> = Optional.empty() +) = SecurityAndPrivacySettings( + roomAccess = roomAccess, + isEncrypted = isEncrypted, + formattedAddress = formattedAddress, + historyVisibility = historyVisibility, + isVisibleInRoomDirectory = isVisibleInRoomDirectory +) + +fun aSecurityAndPrivacyState( + currentSettings: SecurityAndPrivacySettings = aSecurityAndPrivacySettings(), + savedSettings: SecurityAndPrivacySettings = currentSettings, + canBeSaved: Boolean = true, + homeserverName: String = "myserver.xyz", + eventSink: (SecurityAndPrivacyEvents) -> Unit = {} +) = SecurityAndPrivacyState( + currentSettings = currentSettings, + savedSettings = savedSettings, + homeserverName = homeserverName, + canBeSaved = canBeSaved, + eventSink = eventSink +) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt new file mode 100644 index 00000000000..2ad61f2e18e --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt @@ -0,0 +1,351 @@ +/* + * 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.roomdetails.impl.securityandprivacy + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.progressSemantics +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ListItemDefaults +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.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.roomdetails.impl.R +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage +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.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight +import io.element.android.libraries.designsystem.theme.aliasScreenTitle +import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator +import io.element.android.libraries.designsystem.theme.components.IconSource +import io.element.android.libraries.designsystem.theme.components.ListItem +import io.element.android.libraries.designsystem.theme.components.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.ui.strings.CommonStrings +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +@Composable +fun SecurityAndPrivacyView( + state: SecurityAndPrivacyState, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + topBar = { + SecurityAndPrivacyToolbar( + isSaveActionEnabled = state.canBeSaved, + onBackClick = onBackClick, + onSaveClick = { + state.eventSink(SecurityAndPrivacyEvents.Save) + }, + ) + } + ) { padding -> + Column( + modifier = Modifier + .padding(padding) + .imePadding() + .verticalScroll(rememberScrollState()) + .consumeWindowInsets(padding), + verticalArrangement = Arrangement.spacedBy(32.dp), + ) { + RoomAccessSection( + modifier = Modifier.padding(top = 24.dp), + selected = state.currentSettings.roomAccess, + onSelected = { state.eventSink(SecurityAndPrivacyEvents.ChangeRoomAccess(it)) }, + ) + if (state.showRoomVisibilitySections) { + RoomVisibilitySection(state.homeserverName) + RoomAddressSection( + roomAddress = state.currentSettings.formattedAddress, + homeserverName = state.homeserverName, + onRoomAddressClick = { }, + isVisibleInPublicDirectory = state.currentSettings.isVisibleInRoomDirectory, + onVisibilityChange = { }, + ) + } + EncryptionSection( + isEncryptionEnabled = state.currentSettings.isEncrypted, + onEnableEncryption = { state.eventSink(SecurityAndPrivacyEvents.EnableEncryption) }, + ) + if (state.showRoomHistoryVisibilitySection) { + RoomHistorySection( + selectedOption = state.currentSettings.historyVisibility.get(), + availableOptions = state.availableHistoryVisibilities, + onSelected = { state.eventSink(SecurityAndPrivacyEvents.ChangeHistoryVisibility(it)) }, + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SecurityAndPrivacyToolbar( + isSaveActionEnabled: Boolean, + onBackClick: () -> Unit, + onSaveClick: () -> Unit, + modifier: Modifier = Modifier, +) { + TopAppBar( + modifier = modifier, + title = { + Text( + text = stringResource(R.string.screen_room_details_security_and_privacy_title), + style = ElementTheme.typography.aliasScreenTitle, + ) + }, + navigationIcon = { BackButton(onClick = onBackClick) }, + actions = { + TextButton( + text = stringResource(CommonStrings.action_save), + enabled = isSaveActionEnabled, + onClick = onSaveClick, + ) + } + ) +} + +@Composable +private fun SecurityAndPrivacySection( + title: String, + modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit, +) { + Column( + modifier = modifier.selectableGroup() + ) { + Text( + text = title, + style = ElementTheme.typography.fontBodyLgMedium, + color = ElementTheme.colors.textPrimary, + modifier = Modifier.padding(horizontal = 16.dp), + ) + content() + } +} + +@Composable +private fun RoomAccessSection( + selected: SecurityAndPrivacyRoomAccess, + onSelected: (SecurityAndPrivacyRoomAccess) -> Unit, + modifier: Modifier = Modifier, +) { + SecurityAndPrivacySection( + title = stringResource(CommonStrings.screen_security_and_privacy_room_access_section_header), + modifier = modifier, + ) { + ListItem( + headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_access_invite_only_option_title)) }, + supportingContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_access_invite_only_option_description)) }, + trailingContent = ListItemContent.RadioButton(selected = selected == SecurityAndPrivacyRoomAccess.InviteOnly), + onClick = { onSelected(SecurityAndPrivacyRoomAccess.InviteOnly) }, + ) + ListItem( + headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_ask_to_join_option_title)) }, + supportingContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_ask_to_join_option_description)) }, + trailingContent = ListItemContent.RadioButton(selected = selected == SecurityAndPrivacyRoomAccess.AskToJoin), + onClick = { onSelected(SecurityAndPrivacyRoomAccess.AskToJoin) }, + ) + ListItem( + headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_access_anyone_option_title)) }, + supportingContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_access_anyone_option_description)) }, + trailingContent = ListItemContent.RadioButton(selected = selected == SecurityAndPrivacyRoomAccess.Anyone), + onClick = { onSelected(SecurityAndPrivacyRoomAccess.Anyone) }, + ) + if (selected == SecurityAndPrivacyRoomAccess.SpaceMember) { + ListItem( + headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_access_space_members_option_title)) }, + supportingContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_access_space_members_option_description)) }, + trailingContent = ListItemContent.RadioButton(selected = true, enabled = false), + ) + } + } +} + +@Composable +private fun RoomVisibilitySection( + homeserverName: String, + modifier: Modifier = Modifier, +) { + SecurityAndPrivacySection( + title = stringResource(CommonStrings.screen_security_and_privacy_room_visibility_section_header), + modifier = modifier, + ) { + Spacer(Modifier.height(12.dp)) + Text( + text = stringResource(CommonStrings.screen_security_and_privacy_room_visibility_section_footer, homeserverName), + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + modifier = Modifier.padding(horizontal = 16.dp), + ) + } +} + +@Composable +private fun RoomAddressSection( + roomAddress: Optional, + homeserverName: String, + isVisibleInPublicDirectory: Optional>, + onRoomAddressClick: () -> Unit, + onVisibilityChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + SecurityAndPrivacySection( + title = stringResource(CommonStrings.screen_security_and_privacy_room_address_section_header), + modifier = modifier, + ) { + ListItem( + headlineContent = { + Text(text = roomAddress.getOrNull() ?: stringResource(CommonStrings.screen_security_and_privacy_add_room_address_action)) + }, + trailingContent = if (roomAddress.isEmpty) ListItemContent.Icon(IconSource.Vector(CompoundIcons.Plus())) else null, + supportingContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_address_section_footer)) }, + onClick = onRoomAddressClick, + colors = ListItemDefaults.colors(trailingIconColor = ElementTheme.colors.iconAccentPrimary), + alwaysClickable = true + ) + if (isVisibleInPublicDirectory.isPresent) { + ListItem( + headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_directory_visibility_toggle_title)) }, + supportingContent = { + Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_directory_visibility_section_footer, homeserverName)) + }, + trailingContent = + when (val isVisible = isVisibleInPublicDirectory.get()) { + is AsyncData.Uninitialized, is AsyncData.Loading -> { + ListItemContent.Custom { + CircularProgressIndicator( + modifier = Modifier + .progressSemantics() + .size(20.dp), + strokeWidth = 2.dp + ) + } + } + is AsyncData.Failure -> { + ListItemContent.Switch( + checked = false, + enabled = false, + ) + } + is AsyncData.Success -> { + ListItemContent.Switch( + checked = isVisible.data, + onChange = onVisibilityChange + ) + } + } + ) + } + } +} + +@Composable +private fun EncryptionSection( + isEncryptionEnabled: Boolean, + onEnableEncryption: () -> Unit, + modifier: Modifier = Modifier, +) { + SecurityAndPrivacySection( + title = stringResource(CommonStrings.screen_security_and_privacy_encryption_section_header), + modifier = modifier, + ) { + ListItem( + headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_encryption_toggle_title)) }, + supportingContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_encryption_section_footer)) }, + trailingContent = ListItemContent.Switch( + checked = isEncryptionEnabled, + enabled = !isEncryptionEnabled, + onChange = { onEnableEncryption() } + ), + ) + } +} + +@Composable +private fun RoomHistorySection( + selectedOption: SecurityAndPrivacyHistoryVisibility, + availableOptions: Set, + onSelected: (SecurityAndPrivacyHistoryVisibility) -> Unit, + modifier: Modifier = Modifier, +) { + SecurityAndPrivacySection( + title = stringResource(CommonStrings.screen_security_and_privacy_room_history_section_header), + modifier = modifier, + ) { + Spacer(Modifier.height(16.dp)) + for (availableOption in availableOptions) { + val isSelected = availableOption == selectedOption + when (availableOption) { + SecurityAndPrivacyHistoryVisibility.SinceSelection -> { + ListItem( + headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_history_since_selecting_option_title)) }, + trailingContent = ListItemContent.RadioButton(selected = isSelected), + onClick = { onSelected(availableOption) }, + ) + } + SecurityAndPrivacyHistoryVisibility.SinceInvite -> { + ListItem( + headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_history_since_invite_option_title)) }, + trailingContent = ListItemContent.RadioButton(selected = isSelected), + onClick = { onSelected(availableOption) }, + ) + } + SecurityAndPrivacyHistoryVisibility.Anyone -> { + ListItem( + headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_history_anyone_option_title)) }, + trailingContent = ListItemContent.RadioButton(selected = isSelected), + onClick = { onSelected(availableOption) }, + ) + } + } + } + } +} + +@PreviewWithLargeHeight +@Composable +internal fun SecurityAndPrivacyViewLightPreview(@PreviewParameter(SecurityAndPrivacyStateProvider::class) state: SecurityAndPrivacyState) = + ElementPreviewLight { ContentToPreview(state) } + +@PreviewWithLargeHeight +@Composable +internal fun SecurityAndPrivacyViewDarkPreview(@PreviewParameter(SecurityAndPrivacyStateProvider::class) state: SecurityAndPrivacyState) = + ElementPreviewDark { ContentToPreview(state) } + +@ExcludeFromCoverage +@Composable +private fun ContentToPreview(state: SecurityAndPrivacyState) { + SecurityAndPrivacyView( + state = state, + onBackClick = {}, + ) +} + From d35414407c3327bd14f5a12d3a8dcac8b274bac9 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 17 Jan 2025 13:05:25 +0100 Subject: [PATCH 02/30] change(room details) : update room details with new sections organisation --- .../roomdetails/impl/RoomDetailsFlowNode.kt | 11 ++ .../roomdetails/impl/RoomDetailsNode.kt | 6 + .../roomdetails/impl/RoomDetailsView.kt | 132 +++++++++--------- .../matrix/ui/room/MatrixRoomMembers.kt | 3 +- 4 files changed, 85 insertions(+), 67 deletions(-) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt index 0393cae1e8e..8d2f4b4f82f 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt @@ -33,6 +33,7 @@ import io.element.android.features.roomdetails.impl.members.RoomMemberListNode import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsNode import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsNode import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsFlowNode +import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyNode import io.element.android.features.userprofile.shared.UserProfileNodeHelper import io.element.android.libraries.architecture.BackstackWithOverlayBox import io.element.android.libraries.architecture.BaseFlowNode @@ -114,6 +115,9 @@ class RoomDetailsFlowNode @AssistedInject constructor( @Parcelize data object KnockRequestsList : NavTarget + + @Parcelize + data object SecurityAndPrivacy : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -160,6 +164,10 @@ class RoomDetailsFlowNode @AssistedInject constructor( backstack.push(NavTarget.KnockRequestsList) } + override fun openSecurityAndPrivacy() { + backstack.push(NavTarget.SecurityAndPrivacy) + } + override fun onJoinCall() { val inputs = CallType.RoomCall( sessionId = room.sessionId, @@ -290,6 +298,9 @@ class RoomDetailsFlowNode @AssistedInject constructor( NavTarget.KnockRequestsList -> { knockRequestsListEntryPoint.createNode(this, buildContext) } + NavTarget.SecurityAndPrivacy -> { + createNode(buildContext) + } } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt index 138e7cb28ec..6ae3c555032 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt @@ -49,6 +49,7 @@ class RoomDetailsNode @AssistedInject constructor( fun openAdminSettings() fun openPinnedMessagesList() fun openKnockRequestsList() + fun openSecurityAndPrivacy() fun onJoinCall() } @@ -121,6 +122,10 @@ class RoomDetailsNode @AssistedInject constructor( callbacks.forEach { it.openKnockRequestsList() } } + private fun openSecurityAndPrivacy() { + callbacks.forEach { it.openSecurityAndPrivacy() } + } + @Composable override fun View(modifier: Modifier) { val context = LocalContext.current @@ -153,6 +158,7 @@ class RoomDetailsNode @AssistedInject constructor( onJoinCallClick = ::onJoinCall, onPinnedMessagesClick = ::openPinnedMessages, onKnockRequestsClick = ::openKnockRequestsLists, + onSecurityAndPrivacyClick = ::openSecurityAndPrivacy ) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt index a5e5d1872c0..7d2bafb3986 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt @@ -12,7 +12,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets @@ -73,7 +72,6 @@ import io.element.android.libraries.designsystem.theme.components.ListItemStyle import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar -import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.RoomMember @@ -106,6 +104,7 @@ fun RoomDetailsView( onJoinCallClick: () -> Unit, onPinnedMessagesClick: () -> Unit, onKnockRequestsClick: () -> Unit, + onSecurityAndPrivacyClick: () -> Unit, modifier: Modifier = Modifier, ) { Scaffold( @@ -185,24 +184,12 @@ fun RoomDetailsView( } ) - if (state.canShowPinnedMessages) { - PinnedMessagesItem( - pinnedMessagesCount = state.pinnedMessagesCount, - onPinnedMessagesClick = onPinnedMessagesClick - ) - } - - if (state.displayRolesAndPermissionsSettings) { - ListItem( - headlineContent = { Text(stringResource(R.string.screen_room_details_roles_and_permissions)) }, - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Admin())), - onClick = openAdminSettings, - ) - } + SecurityAndPrivacyItem( + onClick = onSecurityAndPrivacyClick + ) } - val displayMemberListItem = state.roomType is RoomDetailsType.Room - if (displayMemberListItem) { + if (state.roomType is RoomDetailsType.Room) { PreferenceCategory { MembersItem( memberCount = state.memberCount, @@ -214,19 +201,31 @@ fun RoomDetailsView( onKnockRequestsClick = onKnockRequestsClick ) } + if (state.displayRolesAndPermissionsSettings) { + ListItem( + headlineContent = { Text(stringResource(R.string.screen_room_details_roles_and_permissions)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Admin())), + onClick = openAdminSettings, + ) + } } } - PollsSection( - openPollHistory = openPollHistory - ) - if (state.canShowMediaGallery) { - MediaGallerySection( - onClick = openMediaGallery + PreferenceCategory { + if (state.canShowPinnedMessages) { + PinnedMessagesItem( + pinnedMessagesCount = state.pinnedMessagesCount, + onPinnedMessagesClick = onPinnedMessagesClick + ) + } + PollsItem( + openPollHistory = openPollHistory ) - } - if (state.isEncrypted) { - SecuritySection() + if (state.canShowMediaGallery) { + MediaGalleryItem( + onClick = openMediaGallery + ) + } } if (state.roomType is RoomDetailsType.Dm && state.roomMemberDetailsState != null) { @@ -408,24 +407,26 @@ private fun DmHeaderSection( } @Composable -private fun ColumnScope.TitleAndSubtitle( +private fun TitleAndSubtitle( title: String, subtitle: String?, ) { - Spacer(modifier = Modifier.height(24.dp)) - Text( - text = title, - style = ElementTheme.typography.fontHeadingLgBold, - textAlign = TextAlign.Center, - ) - if (subtitle != null) { - Spacer(modifier = Modifier.height(6.dp)) + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Spacer(modifier = Modifier.height(24.dp)) Text( - text = subtitle, - style = ElementTheme.typography.fontBodyLgRegular, - color = MaterialTheme.colorScheme.secondary, + text = title, + style = ElementTheme.typography.fontHeadingLgBold, textAlign = TextAlign.Center, ) + if (subtitle != null) { + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = subtitle, + style = ElementTheme.typography.fontBodyLgRegular, + color = MaterialTheme.colorScheme.secondary, + textAlign = TextAlign.Center, + ) + } } } @@ -518,6 +519,19 @@ private fun NotificationItem( ) } +@Composable +private fun SecurityAndPrivacyItem( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + ListItem( + headlineContent = { Text(stringResource(R.string.screen_room_details_security_and_privacy_title)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Lock())), + onClick = onClick, + modifier = modifier, + ) +} + @Composable private fun FavoriteItem( isFavorite: Boolean, @@ -569,40 +583,25 @@ private fun PinnedMessagesItem( } @Composable -private fun PollsSection( +private fun PollsItem( openPollHistory: () -> Unit, ) { - PreferenceCategory { - ListItem( - headlineContent = { Text(stringResource(R.string.screen_polls_history_title)) }, - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Polls())), - onClick = openPollHistory, - ) - } + ListItem( + headlineContent = { Text(stringResource(R.string.screen_polls_history_title)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Polls())), + onClick = openPollHistory, + ) } @Composable -private fun MediaGallerySection( +private fun MediaGalleryItem( onClick: () -> Unit, ) { - PreferenceCategory { - ListItem( - headlineContent = { Text(stringResource(R.string.screen_room_details_media_gallery_title)) }, - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Image())), - onClick = onClick, - ) - } -} - -@Composable -private fun SecuritySection() { - PreferenceCategory(title = stringResource(R.string.screen_room_details_security_title)) { - ListItem( - headlineContent = { Text(stringResource(R.string.screen_room_details_encryption_enabled_title)) }, - supportingContent = { Text(stringResource(R.string.screen_room_details_encryption_enabled_subtitle)) }, - leadingContent = ListItemContent.Icon(IconSource.Resource(CommonDrawables.ic_encryption_enabled)), - ) - } + ListItem( + headlineContent = { Text(stringResource(R.string.screen_room_details_media_gallery_title)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Image())), + onClick = onClick, + ) } @Composable @@ -654,5 +653,6 @@ private fun ContentToPreview(state: RoomDetailsState) { onJoinCallClick = {}, onPinnedMessagesClick = {}, onKnockRequestsClick = {}, + onSecurityAndPrivacyClick = {}, ) } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt index 8ab1a4b9f50..c6a8b75816c 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt @@ -17,6 +17,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.room.roomMembers @Composable @@ -44,7 +45,7 @@ fun MatrixRoom.getDirectRoomMember(roomMembersState: MatrixRoomMembersState): St derivedStateOf { roomMembers ?.filter { it.membership.isActive() } - ?.takeIf { it.size == 2 && isDirect } + ?.takeIf { isDm } ?.find { it.userId != sessionId } } } From a781cc057bbdf9f334deeb4b4e2862396edec653 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 17 Jan 2025 17:32:14 +0100 Subject: [PATCH 03/30] feat(security & privacy) : introduce EditRoomAddress screen --- .../roomdetails/impl/RoomDetailsFlowNode.kt | 4 +- .../SecurityAndPrivacyFlowNode.kt | 63 +++++++++++++++++++ .../editroomaddress/EditRoomAddressEvents.kt | 12 ++++ .../editroomaddress/EditRoomAddressNode.kt | 35 +++++++++++ .../EditRoomAddressPresenter.kt | 29 +++++++++ .../editroomaddress/EditRoomAddressState.kt | 12 ++++ .../EditRoomAddressStateProvider.kt | 22 +++++++ .../editroomaddress/EditRoomAddressView.kt | 45 +++++++++++++ 8 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyFlowNode.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressEvents.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressNode.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressState.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressStateProvider.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressView.kt diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt index 8d2f4b4f82f..774abfe8cdd 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt @@ -33,7 +33,7 @@ import io.element.android.features.roomdetails.impl.members.RoomMemberListNode import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsNode import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsNode import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsFlowNode -import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyNode +import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyFlowNode import io.element.android.features.userprofile.shared.UserProfileNodeHelper import io.element.android.libraries.architecture.BackstackWithOverlayBox import io.element.android.libraries.architecture.BaseFlowNode @@ -299,7 +299,7 @@ class RoomDetailsFlowNode @AssistedInject constructor( knockRequestsListEntryPoint.createNode(this, buildContext) } NavTarget.SecurityAndPrivacy -> { - createNode(buildContext) + createNode(buildContext) } } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyFlowNode.kt new file mode 100644 index 00000000000..d257e096a4e --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyFlowNode.kt @@ -0,0 +1,63 @@ +/* + * 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.roomdetails.impl.securityandprivacy + +import android.os.Parcelable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.navmodel.backstack.BackStack +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress.EditRoomAddressNode +import io.element.android.libraries.architecture.BackstackView +import io.element.android.libraries.architecture.BaseFlowNode +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.RoomScope +import kotlinx.parcelize.Parcelize + +@ContributesNode(RoomScope::class) +class SecurityAndPrivacyFlowNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, +) : BaseFlowNode( + backstack = BackStack( + initialElement = NavTarget.SecurityAndPrivacy, + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins, +) { + + sealed interface NavTarget : Parcelable { + @Parcelize + data object SecurityAndPrivacy : NavTarget + + @Parcelize + data object EditRoomAddress : NavTarget + } + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return when (navTarget) { + NavTarget.SecurityAndPrivacy -> { + createNode(buildContext) + } + NavTarget.EditRoomAddress -> { + createNode(buildContext) + } + } + } + + @Composable + override fun View(modifier: Modifier) { + BackstackView(modifier) + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressEvents.kt new file mode 100644 index 00000000000..a37774f1c59 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressEvents.kt @@ -0,0 +1,12 @@ +/* + * 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.roomdetails.impl.securityandprivacy.editroomaddress + +sealed interface EditRoomAddressEvents { + data object Save : EditRoomAddressEvents +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressNode.kt new file mode 100644 index 00000000000..88299775cdd --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressNode.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.di.RoomScope + +@ContributesNode(RoomScope::class) +class EditRoomAddressNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: EditRoomAddressPresenter, +) : Node(buildContext, plugins = plugins) { + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + EditRoomAddressView( + state = state, + modifier = modifier + ) + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt new file mode 100644 index 00000000000..384a1244f8f --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt @@ -0,0 +1,29 @@ +/* + * 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.roomdetails.impl.securityandprivacy.editroomaddress + +import androidx.compose.runtime.Composable +import io.element.android.libraries.architecture.Presenter +import javax.inject.Inject + +class EditRoomAddressPresenter @Inject constructor() : Presenter { + + @Composable + override fun present(): EditRoomAddressState { + + fun handleEvents(event: EditRoomAddressEvents) { + when (event) { + EditRoomAddressEvents.Save -> Unit + } + } + + return EditRoomAddressState( + eventSink = ::handleEvents + ) + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressState.kt new file mode 100644 index 00000000000..6709d41024e --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressState.kt @@ -0,0 +1,12 @@ +/* + * 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.roomdetails.impl.securityandprivacy.editroomaddress + +data class EditRoomAddressState( + val eventSink: (EditRoomAddressEvents) -> Unit +) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressStateProvider.kt new file mode 100644 index 00000000000..0f610f17149 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressStateProvider.kt @@ -0,0 +1,22 @@ +/* + * 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.roomdetails.impl.securityandprivacy.editroomaddress + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +open class EditRoomAddressStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aEditRoomAddressState(), + // Add other states here + ) +} + +fun aEditRoomAddressState() = EditRoomAddressState( + eventSink = {} +) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressView.kt new file mode 100644 index 00000000000..3a4ca3eaaae --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressView.kt @@ -0,0 +1,45 @@ +/* + * 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.roomdetails.impl.securityandprivacy.editroomaddress + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Text + +@Composable +fun EditRoomAddressView( + state: EditRoomAddressState, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + "EditRoomAddress feature view", + color = MaterialTheme.colorScheme.primary, + ) + } +} + +@PreviewsDayNight +@Composable +internal fun EditRoomAddressViewPreview( + @PreviewParameter(EditRoomAddressStateProvider::class) state: EditRoomAddressState +) = ElementPreview { + EditRoomAddressView( + state = state, + ) +} From b5494000114ed7d8b7c43966bc0ec712f37afa9f Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 21 Jan 2025 11:49:56 +0100 Subject: [PATCH 04/30] feat(security&privacy) : expose new sdk methods --- .../configureroom/ConfigureRoomPresenter.kt | 6 +-- .../impl/configureroom/RoomAccess.kt | 8 ++-- .../api/createroom/CreateRoomParameters.kt | 4 +- .../matrix/api/createroom/JoinRuleOverride.kt | 16 ------- .../matrix/api/createroom/RoomVisibility.kt | 12 ----- .../libraries/matrix/api/room/MatrixRoom.kt | 30 ++++++++++++ .../matrix/api/room/MatrixRoomInfo.kt | 2 + .../api/room/history/RoomHistoryVisibility.kt | 47 +++++++++++++++++++ .../api/roomdirectory/RoomVisibility.kt | 28 +++++++++++ .../libraries/matrix/impl/RustMatrixClient.kt | 37 +++++---------- .../matrix/impl/room/MatrixRoomInfoMapper.kt | 2 + .../matrix/impl/room/RustMatrixRoom.kt | 28 +++++++++++ .../history/RoomHistoryVisibilityMapper.kt | 31 ++++++++++++ .../matrix/impl/room/join/AllowRule.kt | 7 +++ .../matrix/impl/room/join/JoinRule.kt | 12 +++++ .../roomdirectory/RoomVisibilityMapper.kt | 27 +++++++++++ .../impl/room/MatrixRoomInfoMapperTest.kt | 5 ++ .../matrix/test/room/FakeMatrixRoom.kt | 22 +++++++++ .../matrix/test/room/RoomInfoFixture.kt | 3 ++ .../matrix/test/room/RoomSummaryFixture.kt | 3 ++ 20 files changed, 269 insertions(+), 61 deletions(-) delete mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/JoinRuleOverride.kt delete mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/RoomVisibility.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/history/RoomHistoryVisibility.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomVisibility.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/history/RoomHistoryVisibilityMapper.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomVisibilityMapper.kt diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index 1b63f50134a..b8d5916d572 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -31,9 +31,9 @@ import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters import io.element.android.libraries.matrix.api.createroom.RoomPreset -import io.element.android.libraries.matrix.api.createroom.RoomVisibility import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper import io.element.android.libraries.matrix.api.roomAliasFromName +import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.mediapickers.api.PickerProvider import io.element.android.libraries.mediaupload.api.MediaPreProcessor @@ -191,7 +191,7 @@ class ConfigureRoomPresenter @Inject constructor( topic = config.topic, isEncrypted = false, isDirect = false, - visibility = RoomVisibility.PUBLIC, + visibility = RoomVisibility.Public, joinRuleOverride = config.roomVisibility.roomAccess.toJoinRule(), preset = RoomPreset.PUBLIC_CHAT, invite = config.invites.map { it.userId }, @@ -204,7 +204,7 @@ class ConfigureRoomPresenter @Inject constructor( topic = config.topic, isEncrypted = config.roomVisibility is RoomVisibilityState.Private, isDirect = false, - visibility = RoomVisibility.PRIVATE, + visibility = RoomVisibility.Private, preset = RoomPreset.PRIVATE_CHAT, invite = config.invites.map { it.userId }, avatar = avatarUrl, diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomAccess.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomAccess.kt index e85fec13f1d..ef35b654cc9 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomAccess.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomAccess.kt @@ -7,16 +7,16 @@ package io.element.android.features.createroom.impl.configureroom -import io.element.android.libraries.matrix.api.createroom.JoinRuleOverride +import io.element.android.libraries.matrix.api.room.join.JoinRule enum class RoomAccess { Anyone, Knocking } -fun RoomAccess.toJoinRule(): JoinRuleOverride { +fun RoomAccess.toJoinRule(): JoinRule? { return when (this) { - RoomAccess.Anyone -> JoinRuleOverride.None - RoomAccess.Knocking -> JoinRuleOverride.Knock + RoomAccess.Anyone -> null + RoomAccess.Knocking -> JoinRule.Knock } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/CreateRoomParameters.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/CreateRoomParameters.kt index abf00887d04..431a49fbead 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/CreateRoomParameters.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/CreateRoomParameters.kt @@ -8,6 +8,8 @@ package io.element.android.libraries.matrix.api.createroom import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.join.JoinRule +import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility import java.util.Optional data class CreateRoomParameters( @@ -19,6 +21,6 @@ data class CreateRoomParameters( val preset: RoomPreset, val invite: List? = null, val avatar: String? = null, - val joinRuleOverride: JoinRuleOverride = JoinRuleOverride.None, + val joinRuleOverride: JoinRule? = null, val roomAliasName: Optional = Optional.empty(), ) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/JoinRuleOverride.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/JoinRuleOverride.kt deleted file mode 100644 index fee17adc268..00000000000 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/JoinRuleOverride.kt +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 2024 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.libraries.matrix.api.createroom - -/** - * Rules to override the default room join rules. - */ -sealed interface JoinRuleOverride { - data object Knock : JoinRuleOverride - data object None : JoinRuleOverride -} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/RoomVisibility.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/RoomVisibility.kt deleted file mode 100644 index e677d4292a2..00000000000 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/RoomVisibility.kt +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright 2023, 2024 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.libraries.matrix.api.createroom - -enum class RoomVisibility { - PUBLIC, - PRIVATE, -} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index bb86684be0a..9275907f66d 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -24,10 +24,12 @@ import io.element.android.libraries.matrix.api.media.MediaUploadHandler import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.room.draft.ComposerDraft +import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility import io.element.android.libraries.matrix.api.room.knock.KnockRequest import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange +import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId @@ -402,4 +404,32 @@ interface MatrixRoom : Closeable { suspend fun withdrawVerificationAndResend(userIds: List, sendHandle: SendHandle): Result override fun close() = destroy() + + /** + * Update the canonical alias of the room. + * + * Note that publishing the alias in the room directory is done separately. + */ + suspend fun updateCanonicalAlias( + canonicalAlias: RoomAlias?, + alternativeAliases: List + ): Result + + /** + * Update the room's visibility in the room directory. + */ + suspend fun updateRoomVisibility(roomVisibility: RoomVisibility): Result + + /** + * Update room history visibility for this room. + */ + suspend fun updateHistoryVisibility(historyVisibility: RoomHistoryVisibility): Result + + /** + * Returns the visibility for this room in the room directory. + * + * [Public](`RoomVisibility::Public`) rooms are listed in the room + * directory and can be found using it. + */ + suspend fun getRoomVisibility(): Result } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomInfo.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomInfo.kt index 41456bd4eeb..ff525c403d6 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomInfo.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomInfo.kt @@ -12,6 +12,7 @@ import io.element.android.libraries.matrix.api.core.EventId 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.core.UserId +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.user.MatrixUser import kotlinx.collections.immutable.ImmutableList @@ -71,6 +72,7 @@ data class MatrixRoomInfo( val heroes: ImmutableList, val pinnedEventIds: ImmutableList, val creator: UserId?, + val historyVisibility: RoomHistoryVisibility, ) { val aliases: List get() = listOfNotNull(canonicalAlias) + alternativeAliases diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/history/RoomHistoryVisibility.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/history/RoomHistoryVisibility.kt new file mode 100644 index 00000000000..09faa9fb000 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/history/RoomHistoryVisibility.kt @@ -0,0 +1,47 @@ +/* + * 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.libraries.matrix.api.room.history + +sealed interface RoomHistoryVisibility { + /** + * Previous events are accessible to newly joined members from the point + * they were invited onwards. + * + * Events stop being accessible when the member's state changes to + * something other than *invite* or *join*. + */ + data object Invited : RoomHistoryVisibility + + /** + * Previous events are accessible to newly joined members from the point + * they joined the room onwards. + * Events stop being accessible when the member's state changes to + * something other than *join*. + */ + data object Joined : RoomHistoryVisibility + + /** + * Previous events are always accessible to newly joined members. + * + * All events in the room are accessible, even those sent when the member + * was not a part of the room. + */ + data object Shared : RoomHistoryVisibility + + /** + * All events while this is the `HistoryVisibility` value may be shared by + * any participating homeserver with anyone, regardless of whether they + * have ever joined the room. + */ + data object WorldReadable : RoomHistoryVisibility + + /** + * A custom visibility value. + */ + data class Custom(val value: String) : RoomHistoryVisibility +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomVisibility.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomVisibility.kt new file mode 100644 index 00000000000..f29c2aac592 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomVisibility.kt @@ -0,0 +1,28 @@ +/* + * 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.libraries.matrix.api.roomdirectory + +/** + * Enum class representing the visibility of a room in the room directory. + */ +sealed interface RoomVisibility { + /** + * Indicates that the room will be shown in the published room list. + */ + data object Public : RoomVisibility + + /** + * Indicates that the room will not be shown in the published room list. + */ + data object Private : RoomVisibility + + /** + * A custom value that's not present in the spec. + */ + data class Custom(val value: String) : RoomVisibility +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 4f470d556e3..8bc69769240 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -23,9 +23,7 @@ import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters -import io.element.android.libraries.matrix.api.createroom.JoinRuleOverride import io.element.android.libraries.matrix.api.createroom.RoomPreset -import io.element.android.libraries.matrix.api.createroom.RoomVisibility import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.notification.NotificationService @@ -38,8 +36,10 @@ import io.element.android.libraries.matrix.api.room.PendingRoom import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias +import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService +import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion @@ -59,8 +59,10 @@ import io.element.android.libraries.matrix.impl.room.RoomContentForwarder import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber import io.element.android.libraries.matrix.impl.room.RustRoomFactory import io.element.android.libraries.matrix.impl.room.TimelineEventTypeFilterFactory +import io.element.android.libraries.matrix.impl.room.join.map import io.element.android.libraries.matrix.impl.room.preview.RoomPreviewInfoMapper import io.element.android.libraries.matrix.impl.roomdirectory.RustRoomDirectoryService +import io.element.android.libraries.matrix.impl.roomdirectory.map import io.element.android.libraries.matrix.impl.roomlist.RoomListFactory import io.element.android.libraries.matrix.impl.roomlist.RustRoomListService import io.element.android.libraries.matrix.impl.sync.RustSyncService @@ -112,9 +114,7 @@ import kotlin.jvm.optionals.getOrNull import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds import org.matrix.rustcomponents.sdk.CreateRoomParameters as RustCreateRoomParameters -import org.matrix.rustcomponents.sdk.JoinRule as RustJoinRule import org.matrix.rustcomponents.sdk.RoomPreset as RustRoomPreset -import org.matrix.rustcomponents.sdk.RoomVisibility as RustRoomVisibility import org.matrix.rustcomponents.sdk.SyncService as ClientSyncService class RustMatrixClient( @@ -310,36 +310,23 @@ class RustMatrixClient( topic = createRoomParams.topic, isEncrypted = createRoomParams.isEncrypted, isDirect = createRoomParams.isDirect, - visibility = when (createRoomParams.visibility) { - RoomVisibility.PUBLIC -> RustRoomVisibility.Public - RoomVisibility.PRIVATE -> RustRoomVisibility.Private - }, - preset = when (createRoomParams.visibility) { - RoomVisibility.PRIVATE -> { - if (createRoomParams.isDirect) { - RustRoomPreset.TRUSTED_PRIVATE_CHAT - } else { - RustRoomPreset.PRIVATE_CHAT - } - } - RoomVisibility.PUBLIC -> { - RustRoomPreset.PUBLIC_CHAT - } + visibility = createRoomParams.visibility.map(), + preset = when (createRoomParams.preset) { + RoomPreset.PRIVATE_CHAT -> RustRoomPreset.PRIVATE_CHAT + RoomPreset.TRUSTED_PRIVATE_CHAT -> RustRoomPreset.TRUSTED_PRIVATE_CHAT + RoomPreset.PUBLIC_CHAT -> RustRoomPreset.PUBLIC_CHAT }, invite = createRoomParams.invite?.map { it.value }, avatar = createRoomParams.avatar, powerLevelContentOverride = defaultRoomCreationPowerLevels.copy( - invite = if (createRoomParams.joinRuleOverride == JoinRuleOverride.Knock) { + invite = if (createRoomParams.joinRuleOverride == JoinRule.Knock) { // override the invite power level so it's the same as kick. RoomMember.Role.MODERATOR.powerLevel.toInt() } else { null } ), - joinRuleOverride = when (createRoomParams.joinRuleOverride) { - JoinRuleOverride.Knock -> RustJoinRule.Knock - JoinRuleOverride.None -> null - }, + joinRuleOverride = createRoomParams.joinRuleOverride?.map(), canonicalAlias = createRoomParams.roomAliasName.getOrNull(), ) val roomId = RoomId(innerClient.createRoom(rustParams)) @@ -358,7 +345,7 @@ class RustMatrixClient( name = null, isEncrypted = true, isDirect = true, - visibility = RoomVisibility.PRIVATE, + visibility = RoomVisibility.Private, preset = RoomPreset.TRUSTED_PRIVATE_CHAT, invite = listOf(userId), ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapper.kt index 4072981a776..0be4ccbc67a 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapper.kt @@ -15,6 +15,7 @@ import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.impl.room.history.map import io.element.android.libraries.matrix.impl.room.join.map import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper import kotlinx.collections.immutable.ImmutableMap @@ -60,6 +61,7 @@ class MatrixRoomInfoMapper { numUnreadMessages = it.numUnreadMessages.toLong(), numUnreadMentions = it.numUnreadMentions.toLong(), numUnreadNotifications = it.numUnreadNotifications.toLong(), + historyVisibility = it.historyVisibility.map(), ) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 7217b25dcda..0823e8396c2 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -38,11 +38,13 @@ import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.draft.ComposerDraft +import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility import io.element.android.libraries.matrix.api.room.knock.KnockRequest import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange import io.element.android.libraries.matrix.api.room.roomNotificationSettings +import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId @@ -51,10 +53,12 @@ import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings import io.element.android.libraries.matrix.impl.core.RustSendHandle import io.element.android.libraries.matrix.impl.mapper.map import io.element.android.libraries.matrix.impl.room.draft.into +import io.element.android.libraries.matrix.impl.room.history.map import io.element.android.libraries.matrix.impl.room.knock.RustKnockRequest import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper import io.element.android.libraries.matrix.impl.room.powerlevels.RoomPowerLevelsMapper +import io.element.android.libraries.matrix.impl.roomdirectory.map import io.element.android.libraries.matrix.impl.timeline.RustTimeline import io.element.android.libraries.matrix.impl.timeline.toRustReceiptType import io.element.android.libraries.matrix.impl.util.MessageEventContent @@ -775,6 +779,30 @@ class RustMatrixRoom( } } + override suspend fun updateCanonicalAlias(canonicalAlias: RoomAlias?, alternativeAliases: List): Result = withContext(roomDispatcher) { + runCatching { + innerRoom.updateCanonicalAlias(canonicalAlias?.value, alternativeAliases.map { it.value }) + } + } + + override suspend fun updateRoomVisibility(roomVisibility: RoomVisibility): Result = withContext(roomDispatcher) { + runCatching { + innerRoom.updateRoomVisibility(roomVisibility.map()) + } + } + + override suspend fun updateHistoryVisibility(historyVisibility: RoomHistoryVisibility): Result = withContext(roomDispatcher) { + runCatching { + innerRoom.updateHistoryVisibility(historyVisibility.map()) + } + } + + override suspend fun getRoomVisibility(): Result = withContext(roomDispatcher) { + runCatching { + innerRoom.getRoomVisibility().map() + } + } + private fun createTimeline( timeline: InnerTimeline, mode: Timeline.Mode, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/history/RoomHistoryVisibilityMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/history/RoomHistoryVisibilityMapper.kt new file mode 100644 index 00000000000..60ff7cc698f --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/history/RoomHistoryVisibilityMapper.kt @@ -0,0 +1,31 @@ +/* + * 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.libraries.matrix.impl.room.history + +import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility +import org.matrix.rustcomponents.sdk.RoomHistoryVisibility as RustRoomHistoryVisibility + +fun RoomHistoryVisibility.map(): RustRoomHistoryVisibility { + return when (this) { + RoomHistoryVisibility.WorldReadable -> RustRoomHistoryVisibility.WorldReadable + RoomHistoryVisibility.Invited -> RustRoomHistoryVisibility.Invited + RoomHistoryVisibility.Joined -> RustRoomHistoryVisibility.Joined + RoomHistoryVisibility.Shared -> RustRoomHistoryVisibility.Shared + is RoomHistoryVisibility.Custom -> RustRoomHistoryVisibility.Custom(value) + } +} + +fun RustRoomHistoryVisibility.map(): RoomHistoryVisibility { + return when (this) { + RustRoomHistoryVisibility.WorldReadable -> RoomHistoryVisibility.WorldReadable + RustRoomHistoryVisibility.Invited -> RoomHistoryVisibility.Invited + RustRoomHistoryVisibility.Joined -> RoomHistoryVisibility.Joined + RustRoomHistoryVisibility.Shared -> RoomHistoryVisibility.Shared + is RustRoomHistoryVisibility.Custom -> RoomHistoryVisibility.Custom(value) + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/AllowRule.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/AllowRule.kt index 8c2269e0d5c..ae74e1edc0a 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/AllowRule.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/AllowRule.kt @@ -17,3 +17,10 @@ fun RustAllowRule.map(): AllowRule { is RustAllowRule.Custom -> AllowRule.Custom(json) } } + +fun AllowRule.map(): RustAllowRule { + return when (this) { + is AllowRule.RoomMembership -> RustAllowRule.RoomMembership(roomId.toString()) + is AllowRule.Custom -> RustAllowRule.Custom(json) + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/JoinRule.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/JoinRule.kt index dc652346bc6..bc10a369a40 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/JoinRule.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/JoinRule.kt @@ -21,3 +21,15 @@ fun RustJoinRule.map(): JoinRule { is RustJoinRule.KnockRestricted -> JoinRule.KnockRestricted(rules.map { it.map() }) } } + +fun JoinRule.map(): RustJoinRule { + return when (this) { + JoinRule.Public -> RustJoinRule.Public + JoinRule.Private -> RustJoinRule.Private + JoinRule.Knock -> RustJoinRule.Knock + JoinRule.Invite -> RustJoinRule.Invite + is JoinRule.Restricted -> RustJoinRule.Restricted(rules.map { it.map() }) + is JoinRule.Custom -> RustJoinRule.Custom(value) + is JoinRule.KnockRestricted -> RustJoinRule.KnockRestricted(rules.map { it.map() }) + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomVisibilityMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomVisibilityMapper.kt new file mode 100644 index 00000000000..bfbc48d3b92 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomVisibilityMapper.kt @@ -0,0 +1,27 @@ +/* + * 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.libraries.matrix.impl.roomdirectory + +import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility +import org.matrix.rustcomponents.sdk.RoomVisibility as RustRoomVisibility + +fun RoomVisibility.map(): RustRoomVisibility { + return when (this) { + RoomVisibility.Public -> RustRoomVisibility.Public + RoomVisibility.Private -> RustRoomVisibility.Private + is RoomVisibility.Custom -> RustRoomVisibility.Custom(value) + } +} + +fun RustRoomVisibility.map(): RoomVisibility { + return when (this) { + RustRoomVisibility.Public -> RoomVisibility.Public + RustRoomVisibility.Private -> RoomVisibility.Private + is RustRoomVisibility.Custom -> RoomVisibility.Custom(value) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapperTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapperTest.kt index 16a9df1d569..0639f8e9e40 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapperTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapperTest.kt @@ -14,6 +14,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.RoomNotificationMode +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.user.MatrixUser import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomHero @@ -34,6 +35,7 @@ import org.junit.Test import org.matrix.rustcomponents.sdk.Membership import org.matrix.rustcomponents.sdk.JoinRule as RustJoinRule import org.matrix.rustcomponents.sdk.RoomNotificationMode as RustRoomNotificationMode +import org.matrix.rustcomponents.sdk.RoomHistoryVisibility as RustRoomHistoryVisibility class MatrixRoomInfoMapperTest { @Test @@ -72,6 +74,7 @@ class MatrixRoomInfoMapperTest { numUnreadMentions = 14uL, pinnedEventIds = listOf(AN_EVENT_ID.value), roomCreator = A_USER_ID, + historyVisibility = RustRoomHistoryVisibility.Joined, ) ) ).isEqualTo( @@ -113,6 +116,7 @@ class MatrixRoomInfoMapperTest { numUnreadMessages = 12L, numUnreadNotifications = 13L, numUnreadMentions = 14L, + historyVisibility = RoomHistoryVisibility.Joined, ) ) } @@ -188,6 +192,7 @@ class MatrixRoomInfoMapperTest { numUnreadMessages = 12L, numUnreadNotifications = 13L, numUnreadMentions = 14L, + historyVisibility = RoomHistoryVisibility.Joined, ) ) } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 62d19bd66cb..551bb937958 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -33,10 +33,12 @@ import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.draft.ComposerDraft +import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility import io.element.android.libraries.matrix.api.room.knock.KnockRequest import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange +import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId @@ -145,6 +147,10 @@ class FakeMatrixRoom( private val subscribeToSyncLambda: () -> Unit = { lambdaError() }, private val ignoreDeviceTrustAndResendResult: (Map>, SendHandle) -> Result = { _, _ -> lambdaError() }, private val withdrawVerificationAndResendResult: (List, SendHandle) -> Result = { _, _ -> lambdaError() }, + private val updateCanonicalAliasResult: (RoomAlias?, List) -> Result = { _, _ -> lambdaError() }, + private val updateRoomVisibilityResult: (RoomVisibility) -> Result = { lambdaError() }, + private val updateRoomHistoryVisibilityResult: (RoomHistoryVisibility) -> Result = { lambdaError() }, + private val roomVisibilityResult: () -> Result = { lambdaError() }, ) : MatrixRoom { private val _roomInfoFlow: MutableSharedFlow = MutableSharedFlow(replay = 1) override val roomInfoFlow: Flow = _roomInfoFlow @@ -582,6 +588,22 @@ class FakeMatrixRoom( return withdrawVerificationAndResendResult(userIds, sendHandle) } + override suspend fun updateCanonicalAlias(canonicalAlias: RoomAlias?, alternativeAliases: List): Result = simulateLongTask { + updateCanonicalAliasResult(canonicalAlias, alternativeAliases) + } + + override suspend fun updateRoomVisibility(roomVisibility: RoomVisibility): Result = simulateLongTask { + updateRoomVisibilityResult(roomVisibility) + } + + override suspend fun updateHistoryVisibility(historyVisibility: RoomHistoryVisibility): Result = simulateLongTask { + updateRoomHistoryVisibilityResult(historyVisibility) + } + + override suspend fun getRoomVisibility(): Result = simulateLongTask { + roomVisibilityResult() + } + fun givenRoomMembersState(state: MatrixRoomMembersState) { membersStateFlow.value = state } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomInfoFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomInfoFixture.kt index 6920f9f8a02..ed62d96d98b 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomInfoFixture.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomInfoFixture.kt @@ -15,6 +15,7 @@ import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomNotificationMode +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.user.MatrixUser import io.element.android.libraries.matrix.test.AN_AVATAR_URL @@ -58,6 +59,7 @@ fun aRoomInfo( numUnreadMessages: Long = 0, numUnreadNotifications: Long = 0, numUnreadMentions: Long = 0, + historyVisibility: RoomHistoryVisibility = RoomHistoryVisibility.Joined, ) = MatrixRoomInfo( id = id, name = name, @@ -90,4 +92,5 @@ fun aRoomInfo( numUnreadMessages = numUnreadMessages, numUnreadNotifications = numUnreadNotifications, numUnreadMentions = numUnreadMentions, + historyVisibility = historyVisibility, ) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt index c741f031b82..df34bcc734b 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt @@ -15,6 +15,7 @@ import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomNotificationMode +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.message.RoomMessage import io.element.android.libraries.matrix.api.roomlist.RoomSummary @@ -71,6 +72,7 @@ fun aRoomSummary( numUnreadMessages: Long = 0, numUnreadNotifications: Long = 0, numUnreadMentions: Long = 0, + historyVisibility: RoomHistoryVisibility = RoomHistoryVisibility.Joined, lastMessage: RoomMessage? = aRoomMessage(), ) = RoomSummary( info = MatrixRoomInfo( @@ -105,6 +107,7 @@ fun aRoomSummary( numUnreadMessages = numUnreadMessages, numUnreadNotifications = numUnreadNotifications, numUnreadMentions = numUnreadMentions, + historyVisibility = historyVisibility, ), lastMessage = lastMessage, ) From c391b5b892ef169b8b6d40d443b32b0f46accf72 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 21 Jan 2025 20:38:29 +0100 Subject: [PATCH 05/30] feat(security&privacy) : get data from sdk --- .../SecurityAndPrivacyEvents.kt | 2 +- .../SecurityAndPrivacyPresenter.kt | 114 +++++++++++++++--- .../SecurityAndPrivacyState.kt | 4 +- .../SecurityAndPrivacyStateProvider.kt | 3 - .../SecurityAndPrivacyView.kt | 20 +-- 5 files changed, 114 insertions(+), 29 deletions(-) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt index 72af1449a9c..f87aff2f313 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt @@ -12,5 +12,5 @@ sealed interface SecurityAndPrivacyEvents { data class ChangeRoomAccess(val roomAccess: SecurityAndPrivacyRoomAccess) : SecurityAndPrivacyEvents data object EnableEncryption: SecurityAndPrivacyEvents data class ChangeHistoryVisibility(val historyVisibility: SecurityAndPrivacyHistoryVisibility) : SecurityAndPrivacyEvents - data class ChangeVisibleInRoomDirectory(val isVisibleInRoomDirectory: Boolean) : SecurityAndPrivacyEvents + data class ChangeRoomVisibility(val isVisibleInRoomDirectory: Boolean) : SecurityAndPrivacyEvents } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt index 6568704d9c3..901a3b4a4bb 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt @@ -8,47 +8,129 @@ package io.element.android.features.roomdetails.impl.securityandprivacy import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility +import io.element.android.libraries.matrix.api.room.join.JoinRule import java.util.Optional import javax.inject.Inject class SecurityAndPrivacyPresenter @Inject constructor() : Presenter { + private val matrixClient: MatrixClient, + private val room: MatrixRoom, +) : Presenter { @Composable override fun present(): SecurityAndPrivacyState { + val homeserverName = remember { matrixClient.userIdServerName() } + val roomInfo by room.roomInfoFlow.collectAsState(initial = null) + + val isVisibleInRoomDirectory = remember { + mutableStateOf>(AsyncData.Uninitialized) + } val savedSettings by remember { - mutableStateOf( + derivedStateOf { SecurityAndPrivacySettings( - roomAccess = SecurityAndPrivacyRoomAccess.InviteOnly, - isEncrypted = true, - isVisibleInRoomDirectory = Optional.empty(), - historyVisibility = Optional.empty(), - formattedAddress = Optional.empty(), + roomAccess = roomInfo?.joinRule.map(), + isEncrypted = room.isEncrypted, + isVisibleInRoomDirectory = Optional.ofNullable(isVisibleInRoomDirectory.value), + historyVisibility = Optional.ofNullable(roomInfo?.historyVisibility?.map()), + formattedAddress = Optional.ofNullable(roomInfo?.canonicalAlias?.value), ) - ) + } } + var currentRoomAccess by remember(savedSettings.roomAccess) { + mutableStateOf(savedSettings.roomAccess) + } + var currentHistoryVisibility by remember(savedSettings.historyVisibility) { + mutableStateOf(savedSettings.historyVisibility) + } + var currentVisibleInRoomDirectory by remember(savedSettings.isVisibleInRoomDirectory) { + mutableStateOf(savedSettings.isVisibleInRoomDirectory) + } + var currentIsEncrypted by remember(savedSettings.isEncrypted) { + mutableStateOf(savedSettings.isEncrypted) + } + val currentSettings = SecurityAndPrivacySettings( + roomAccess = currentRoomAccess, + isEncrypted = currentIsEncrypted, + isVisibleInRoomDirectory = currentVisibleInRoomDirectory, + historyVisibility = currentHistoryVisibility, + formattedAddress = savedSettings.formattedAddress, + ) + fun handleEvents(event: SecurityAndPrivacyEvents) { when (event) { - SecurityAndPrivacyEvents.Save -> {} - is SecurityAndPrivacyEvents.ChangeRoomAccess -> {} - is SecurityAndPrivacyEvents.EnableEncryption -> {} - is SecurityAndPrivacyEvents.ChangeHistoryVisibility -> {} - is SecurityAndPrivacyEvents.ChangeVisibleInRoomDirectory -> {} + SecurityAndPrivacyEvents.Save -> { + } + is SecurityAndPrivacyEvents.ChangeRoomAccess -> { + currentRoomAccess = event.roomAccess + } + is SecurityAndPrivacyEvents.EnableEncryption -> { + currentIsEncrypted = true + } + is SecurityAndPrivacyEvents.ChangeHistoryVisibility -> { + currentHistoryVisibility = Optional.of(event.historyVisibility) + } + is SecurityAndPrivacyEvents.ChangeRoomVisibility -> { + currentVisibleInRoomDirectory = Optional.of(AsyncData.Success(event.isVisibleInRoomDirectory)) + } } } - return SecurityAndPrivacyState( savedSettings = savedSettings, - currentSettings = savedSettings, - homeserverName = "", - canBeSaved = true, + currentSettings = currentSettings, + homeserverName = homeserverName, eventSink = ::handleEvents ) } } + +private fun JoinRule?.map(): SecurityAndPrivacyRoomAccess { + return when (this) { + JoinRule.Public -> SecurityAndPrivacyRoomAccess.Anyone + JoinRule.Knock, is JoinRule.KnockRestricted -> SecurityAndPrivacyRoomAccess.AskToJoin + is JoinRule.Restricted -> SecurityAndPrivacyRoomAccess.SpaceMember + is JoinRule.Custom, + JoinRule.Invite, + JoinRule.Private, + null -> SecurityAndPrivacyRoomAccess.InviteOnly + } +} + +private fun SecurityAndPrivacyRoomAccess.map(): JoinRule { + return when (this) { + SecurityAndPrivacyRoomAccess.Anyone -> JoinRule.Public + SecurityAndPrivacyRoomAccess.AskToJoin -> JoinRule.Knock + SecurityAndPrivacyRoomAccess.InviteOnly -> JoinRule.Private + SecurityAndPrivacyRoomAccess.SpaceMember -> error("Unsupported") + } +} + +private fun RoomHistoryVisibility.map(): SecurityAndPrivacyHistoryVisibility { + return when (this) { + RoomHistoryVisibility.Joined, + RoomHistoryVisibility.Invited -> SecurityAndPrivacyHistoryVisibility.SinceInvite + RoomHistoryVisibility.Shared, + is RoomHistoryVisibility.Custom -> SecurityAndPrivacyHistoryVisibility.SinceSelection + RoomHistoryVisibility.WorldReadable -> SecurityAndPrivacyHistoryVisibility.Anyone + } +} + +private fun SecurityAndPrivacyHistoryVisibility.map(): RoomHistoryVisibility { + return when (this) { + SecurityAndPrivacyHistoryVisibility.SinceSelection -> RoomHistoryVisibility.Shared + SecurityAndPrivacyHistoryVisibility.SinceInvite -> RoomHistoryVisibility.Invited + SecurityAndPrivacyHistoryVisibility.Anyone -> RoomHistoryVisibility.WorldReadable + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt index 5d598f4c53d..92081359856 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt @@ -15,9 +15,11 @@ data class SecurityAndPrivacyState( val savedSettings: SecurityAndPrivacySettings, val currentSettings: SecurityAndPrivacySettings, val homeserverName: String, - val canBeSaved: Boolean, val eventSink: (SecurityAndPrivacyEvents) -> Unit ) { + + val canBeSaved = savedSettings != currentSettings + val showRoomVisibilitySections = currentSettings.roomAccess != SecurityAndPrivacyRoomAccess.InviteOnly && currentSettings.historyVisibility.isPresent val availableHistoryVisibilities = buildSet { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt index 2807763c6ee..6955008914b 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt @@ -41,7 +41,6 @@ open class SecurityAndPrivacyStateProvider : PreviewParameterProvider Unit = {} ) = SecurityAndPrivacyState( currentSettings = currentSettings, savedSettings = savedSettings, homeserverName = homeserverName, - canBeSaved = canBeSaved, eventSink = eventSink ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt index 2ad61f2e18e..540f81a80e7 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt @@ -69,10 +69,10 @@ fun SecurityAndPrivacyView( ) { padding -> Column( modifier = Modifier - .padding(padding) - .imePadding() - .verticalScroll(rememberScrollState()) - .consumeWindowInsets(padding), + .padding(padding) + .imePadding() + .verticalScroll(rememberScrollState()) + .consumeWindowInsets(padding), verticalArrangement = Arrangement.spacedBy(32.dp), ) { RoomAccessSection( @@ -87,11 +87,14 @@ fun SecurityAndPrivacyView( homeserverName = state.homeserverName, onRoomAddressClick = { }, isVisibleInPublicDirectory = state.currentSettings.isVisibleInRoomDirectory, - onVisibilityChange = { }, + onVisibilityChange = { isVisible -> + state.eventSink(SecurityAndPrivacyEvents.ChangeRoomVisibility(isVisible)) + }, ) } EncryptionSection( isEncryptionEnabled = state.currentSettings.isEncrypted, + isSectionEnabled = !state.savedSettings.isEncrypted, onEnableEncryption = { state.eventSink(SecurityAndPrivacyEvents.EnableEncryption) }, ) if (state.showRoomHistoryVisibilitySection) { @@ -243,8 +246,8 @@ private fun RoomAddressSection( ListItemContent.Custom { CircularProgressIndicator( modifier = Modifier - .progressSemantics() - .size(20.dp), + .progressSemantics() + .size(20.dp), strokeWidth = 2.dp ) } @@ -270,6 +273,7 @@ private fun RoomAddressSection( @Composable private fun EncryptionSection( isEncryptionEnabled: Boolean, + isSectionEnabled: Boolean, onEnableEncryption: () -> Unit, modifier: Modifier = Modifier, ) { @@ -282,7 +286,7 @@ private fun EncryptionSection( supportingContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_encryption_section_footer)) }, trailingContent = ListItemContent.Switch( checked = isEncryptionEnabled, - enabled = !isEncryptionEnabled, + enabled = isSectionEnabled, onChange = { onEnableEncryption() } ), ) From 129eb45c686237a6e677617087a25cadeaeea60b Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 21 Jan 2025 20:38:43 +0100 Subject: [PATCH 06/30] feat(security&privacy) : introduce navigator --- .../SecurityAndPrivacyEvents.kt | 1 + .../SecurityAndPrivacyFlowNode.kt | 6 +++-- .../SecurityAndPrivacyNavigator.kt | 24 +++++++++++++++++++ .../SecurityAndPrivacyNode.kt | 8 +++++-- .../SecurityAndPrivacyPresenter.kt | 12 ++++++++-- .../SecurityAndPrivacyView.kt | 2 +- 6 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNavigator.kt diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt index f87aff2f313..d817e30a36c 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt @@ -8,6 +8,7 @@ package io.element.android.features.roomdetails.impl.securityandprivacy sealed interface SecurityAndPrivacyEvents { + data object EditRoomAddress : SecurityAndPrivacyEvents data object Save : SecurityAndPrivacyEvents data class ChangeRoomAccess(val roomAccess: SecurityAndPrivacyRoomAccess) : SecurityAndPrivacyEvents data object EnableEncryption: SecurityAndPrivacyEvents diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyFlowNode.kt index d257e096a4e..76d5a5b0052 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyFlowNode.kt @@ -45,13 +45,15 @@ class SecurityAndPrivacyFlowNode @AssistedInject constructor( data object EditRoomAddress : NavTarget } + private val navigator = BackstackSecurityAndPrivacyNavigator(backstack) + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { NavTarget.SecurityAndPrivacy -> { - createNode(buildContext) + createNode(buildContext, plugins = listOf(navigator)) } NavTarget.EditRoomAddress -> { - createNode(buildContext) + createNode(buildContext, plugins = listOf(navigator)) } } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNavigator.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNavigator.kt new file mode 100644 index 00000000000..050283e7395 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNavigator.kt @@ -0,0 +1,24 @@ +/* + * 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.roomdetails.impl.securityandprivacy + +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.push + +interface SecurityAndPrivacyNavigator : Plugin { + fun openEditRoomAddress() +} + +class BackstackSecurityAndPrivacyNavigator( + private val backStack: BackStack +) : SecurityAndPrivacyNavigator { + override fun openEditRoomAddress() { + backStack.push(SecurityAndPrivacyFlowNode.NavTarget.EditRoomAddress) + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNode.kt index 3a200cb24c1..ed483e807e4 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNode.kt @@ -12,6 +12,7 @@ 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 dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode @@ -21,15 +22,18 @@ import io.element.android.libraries.di.RoomScope class SecurityAndPrivacyNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val presenter: SecurityAndPrivacyPresenter, + presenterFactory: SecurityAndPrivacyPresenter.Factory, ) : Node(buildContext, plugins = plugins) { + private val navigator = plugins().first() + private val presenter = presenterFactory.create(navigator) + @Composable override fun View(modifier: Modifier) { val state = presenter.present() SecurityAndPrivacyView( state = state, - onBackClick = ::navigateUp, + onBackClick = this::navigateUp, modifier = modifier ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt index 901a3b4a4bb..db4a99d37bc 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt @@ -14,6 +14,9 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.MatrixClient @@ -21,12 +24,16 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility import io.element.android.libraries.matrix.api.room.join.JoinRule import java.util.Optional -import javax.inject.Inject -class SecurityAndPrivacyPresenter @Inject constructor() : Presenter { +class SecurityAndPrivacyPresenter @AssistedInject constructor( + @Assisted private val navigator: SecurityAndPrivacyNavigator, private val matrixClient: MatrixClient, private val room: MatrixRoom, ) : Presenter { + @AssistedFactory + interface Factory { + fun create(navigator: SecurityAndPrivacyNavigator): SecurityAndPrivacyPresenter + } @Composable override fun present(): SecurityAndPrivacyState { @@ -85,6 +92,7 @@ class SecurityAndPrivacyPresenter @Inject constructor() : Presenter { currentVisibleInRoomDirectory = Optional.of(AsyncData.Success(event.isVisibleInRoomDirectory)) } + SecurityAndPrivacyEvents.EditRoomAddress -> navigator.openEditRoomAddress() } } return SecurityAndPrivacyState( diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt index 540f81a80e7..8f91f199421 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt @@ -85,7 +85,7 @@ fun SecurityAndPrivacyView( RoomAddressSection( roomAddress = state.currentSettings.formattedAddress, homeserverName = state.homeserverName, - onRoomAddressClick = { }, + onRoomAddressClick = { state.eventSink(SecurityAndPrivacyEvents.EditRoomAddress) }, isVisibleInPublicDirectory = state.currentSettings.isVisibleInRoomDirectory, onVisibilityChange = { isVisible -> state.eventSink(SecurityAndPrivacyEvents.ChangeRoomVisibility(isVisible)) From 9faa30526249337e29f19d272ca4a5431d9e71f4 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 21 Jan 2025 21:55:06 +0100 Subject: [PATCH 07/30] feat(room address) : extract some reusable code --- .../configureroom/ConfigureRoomPresenter.kt | 48 +++--------- .../impl/configureroom/ConfigureRoomState.kt | 1 + .../ConfigureRoomStateProvider.kt | 1 + .../impl/configureroom/ConfigureRoomView.kt | 47 +----------- .../ui/room/address/RoomAddressField.kt | 74 +++++++++++++++++++ .../ui/room/address}/RoomAddressValidity.kt | 2 +- .../room/address/RoomAddressValidityEffect.kt | 52 +++++++++++++ 7 files changed, 142 insertions(+), 83 deletions(-) create mode 100644 libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressField.kt rename {features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom => libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address}/RoomAddressValidity.kt (91%) create mode 100644 libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressValidityEffect.kt diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index b8d5916d572..68c1d0e9467 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -17,7 +17,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.rememberUpdatedState import im.vector.app.features.analytics.plan.CreatedRoom import io.element.android.features.createroom.impl.CreateRoomConfig import io.element.android.features.createroom.impl.CreateRoomDataStore @@ -32,9 +31,10 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters import io.element.android.libraries.matrix.api.createroom.RoomPreset import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper -import io.element.android.libraries.matrix.api.roomAliasFromName import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility import io.element.android.libraries.matrix.ui.media.AvatarAction +import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity +import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidityEffect import io.element.android.libraries.mediapickers.api.PickerProvider import io.element.android.libraries.mediaupload.api.MediaPreProcessor import io.element.android.libraries.permissions.api.PermissionsEvents @@ -42,12 +42,10 @@ import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.services.analytics.api.AnalyticsService import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay import kotlinx.coroutines.launch import timber.log.Timber -import java.util.Optional import javax.inject.Inject -import kotlin.jvm.optionals.getOrNull +import kotlin.jvm.optionals.getOrDefault class ConfigureRoomPresenter @Inject constructor( private val dataStore: CreateRoomDataStore, @@ -96,7 +94,12 @@ class ConfigureRoomPresenter @Inject constructor( } } - RoomAddressValidityEffect(createRoomConfig.roomVisibility.roomAddress()) { newRoomAddressValidity -> + RoomAddressValidityEffect( + client = matrixClient, + roomAliasHelper = roomAliasHelper, + newRoomAddress = createRoomConfig.roomVisibility.roomAddress().getOrDefault(""), + knownRoomAddress = null, + ) { newRoomAddressValidity -> roomAddressValidity.value = newRoomAddressValidity } @@ -146,39 +149,6 @@ class ConfigureRoomPresenter @Inject constructor( ) } - @Composable - private fun RoomAddressValidityEffect( - roomAddress: Optional, - onRoomAddressValidityChange: (RoomAddressValidity) -> Unit, - ) { - val onChange by rememberUpdatedState(onRoomAddressValidityChange) - LaunchedEffect(roomAddress) { - val roomAliasName = roomAddress.getOrNull().orEmpty() - if (roomAliasName.isEmpty()) { - onChange(RoomAddressValidity.Unknown) - return@LaunchedEffect - } - // debounce the room address validation - delay(300) - val roomAlias = matrixClient.roomAliasFromName(roomAliasName).getOrNull() - if (roomAlias == null || !roomAliasHelper.isRoomAliasValid(roomAlias)) { - onChange(RoomAddressValidity.InvalidSymbols) - } else { - matrixClient.resolveRoomAlias(roomAlias) - .onSuccess { resolved -> - if (resolved.isPresent) { - onChange(RoomAddressValidity.NotAvailable) - } else { - onChange(RoomAddressValidity.Valid) - } - } - .onFailure { - onChange(RoomAddressValidity.Valid) - } - } - } - } - private fun CoroutineScope.createRoom( config: CreateRoomConfig, createRoomAction: MutableState> diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt index 794c40cb4b8..6651d16604c 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt @@ -11,6 +11,7 @@ import io.element.android.features.createroom.impl.CreateRoomConfig import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.ui.media.AvatarAction +import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity import io.element.android.libraries.permissions.api.PermissionsState import kotlinx.collections.immutable.ImmutableList diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt index 6b9219f57db..71568dbbc76 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt @@ -13,6 +13,7 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.ui.components.aMatrixUserList import io.element.android.libraries.matrix.ui.media.AvatarAction +import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity import io.element.android.libraries.permissions.api.PermissionsState import io.element.android.libraries.permissions.api.aPermissionsState import kotlinx.collections.immutable.toImmutableList diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index d92051437a3..2684c8334dd 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState @@ -58,6 +57,7 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList import io.element.android.libraries.matrix.ui.components.UnsavedAvatar +import io.element.android.libraries.matrix.ui.room.address.RoomAddressField import io.element.android.libraries.permissions.api.PermissionsView import io.element.android.libraries.ui.strings.CommonStrings @@ -142,10 +142,12 @@ fun ConfigureRoomView( ) RoomAddressField( modifier = Modifier.padding(horizontal = 16.dp), - address = state.config.roomVisibility.roomAddress, + address = state.config.roomVisibility.roomAddress.value, homeserverName = state.homeserverName, addressValidity = state.roomAddressValidity, onAddressChange = { state.eventSink(ConfigureRoomEvents.RoomAddressChanged(it)) }, + label = stringResource(R.string.screen_create_room_room_address_section_title), + supportingText = stringResource(R.string.screen_create_room_room_address_section_footer), ) Spacer(Modifier) } @@ -318,47 +320,6 @@ private fun RoomAccessOptions( } } -@Composable -private fun RoomAddressField( - address: RoomAddress, - homeserverName: String, - addressValidity: RoomAddressValidity, - onAddressChange: (String) -> Unit, - modifier: Modifier = Modifier, -) { - TextField( - modifier = modifier.fillMaxWidth(), - value = address.value, - label = stringResource(R.string.screen_create_room_room_address_section_title), - leadingIcon = { - Text( - text = "#", - style = ElementTheme.typography.fontBodyLgMedium, - color = ElementTheme.colors.textSecondary, - ) - }, - trailingIcon = { - Text( - text = homeserverName, - style = ElementTheme.typography.fontBodyLgMedium, - color = ElementTheme.colors.textSecondary, - ) - }, - supportingText = when (addressValidity) { - RoomAddressValidity.InvalidSymbols -> { - stringResource(CommonStrings.error_room_address_invalid_symbols) - } - RoomAddressValidity.NotAvailable -> { - stringResource(CommonStrings.error_room_address_already_exists) - } - else -> stringResource(R.string.screen_create_room_room_address_section_footer) - }, - isError = addressValidity.isError(), - onValueChange = onAddressChange, - singleLine = true, - ) -} - @PreviewWithLargeHeight @Composable internal fun ConfigureRoomViewLightPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressField.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressField.kt new file mode 100644 index 00000000000..590c1704745 --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressField.kt @@ -0,0 +1,74 @@ +/* + * 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.libraries.matrix.ui.room.address + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextField +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun RoomAddressField( + address: String, + homeserverName: String, + addressValidity: RoomAddressValidity, + onAddressChange: (String) -> Unit, + label: String, + supportingText: String, + modifier: Modifier = Modifier, +) { + TextField( + modifier = modifier, + value = address, + label = label, + leadingIcon = { + Text( + text = "#", + style = ElementTheme.typography.fontBodyLgMedium, + color = ElementTheme.colors.textSecondary, + ) + }, + trailingIcon = { + Text( + text = homeserverName, + style = ElementTheme.typography.fontBodyLgMedium, + color = ElementTheme.colors.textSecondary, + ) + }, + supportingText = when (addressValidity) { + RoomAddressValidity.InvalidSymbols -> { + stringResource(CommonStrings.error_room_address_invalid_symbols) + } + RoomAddressValidity.NotAvailable -> { + stringResource(CommonStrings.error_room_address_already_exists) + } + else -> supportingText + }, + isError = addressValidity.isError(), + onValueChange = onAddressChange, + singleLine = true, + ) +} + +@PreviewsDayNight +@Composable +fun RoomAddressFieldPreview() = ElementPreview { + RoomAddressField( + address = "room", + homeserverName = "element.io", + addressValidity = RoomAddressValidity.Valid, + onAddressChange = {}, + label = "Room address", + supportingText = "This is the address that people will use to join your room", + ) +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomAddressValidity.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressValidity.kt similarity index 91% rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomAddressValidity.kt rename to libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressValidity.kt index 903173dc819..25af0ab98dd 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomAddressValidity.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressValidity.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.createroom.impl.configureroom +package io.element.android.libraries.matrix.ui.room.address import androidx.compose.runtime.Immutable diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressValidityEffect.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressValidityEffect.kt new file mode 100644 index 00000000000..73c14c50dfd --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressValidityEffect.kt @@ -0,0 +1,52 @@ +/* + * 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.libraries.matrix.ui.room.address + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper +import io.element.android.libraries.matrix.api.roomAliasFromName +import kotlinx.coroutines.delay + +@Composable +fun RoomAddressValidityEffect( + client: MatrixClient, + roomAliasHelper: RoomAliasHelper, + newRoomAddress: String, + knownRoomAddress: String?, + onRoomAddressValidityChange: (RoomAddressValidity) -> Unit, +) { + val onChange by rememberUpdatedState(onRoomAddressValidityChange) + LaunchedEffect(newRoomAddress) { + if (newRoomAddress.isEmpty() || newRoomAddress == knownRoomAddress) { + onChange(RoomAddressValidity.Unknown) + return@LaunchedEffect + } + // debounce the room address validation + delay(300) + val roomAlias = client.roomAliasFromName(newRoomAddress).getOrNull() + if (roomAlias == null || !roomAliasHelper.isRoomAliasValid(roomAlias)) { + onChange(RoomAddressValidity.InvalidSymbols) + } else { + client.resolveRoomAlias(roomAlias) + .onSuccess { resolved -> + if (resolved.isPresent) { + onChange(RoomAddressValidity.NotAvailable) + } else { + onChange(RoomAddressValidity.Valid) + } + } + .onFailure { + onChange(RoomAddressValidity.Valid) + } + } + } +} From f8cd8b3cc2c197a9ac48c4e65234e6bddacef088 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 21 Jan 2025 21:55:55 +0100 Subject: [PATCH 08/30] feat(security&privacy) : start handling edition of room address --- .../editroomaddress/EditRoomAddressEvents.kt | 1 + .../editroomaddress/EditRoomAddressNode.kt | 1 + .../EditRoomAddressPresenter.kt | 60 +++++++++++- .../editroomaddress/EditRoomAddressState.kt | 9 +- .../EditRoomAddressStateProvider.kt | 13 ++- .../editroomaddress/EditRoomAddressView.kt | 91 ++++++++++++++++--- 6 files changed, 160 insertions(+), 15 deletions(-) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressEvents.kt index a37774f1c59..69b51ce1c61 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressEvents.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressEvents.kt @@ -9,4 +9,5 @@ package io.element.android.features.roomdetails.impl.securityandprivacy.editroom sealed interface EditRoomAddressEvents { data object Save : EditRoomAddressEvents + data class RoomAddressChanged(val roomAddress: String) : EditRoomAddressEvents } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressNode.kt index 88299775cdd..fbbfee203f5 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressNode.kt @@ -29,6 +29,7 @@ class EditRoomAddressNode @AssistedInject constructor( val state = presenter.present() EditRoomAddressView( state = state, + onBackClick = ::navigateUp, modifier = modifier ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt index 384a1244f8f..a2417e308a9 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt @@ -8,22 +8,80 @@ package io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomAlias +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper +import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity +import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidityEffect import javax.inject.Inject -class EditRoomAddressPresenter @Inject constructor() : Presenter { +class EditRoomAddressPresenter @Inject constructor( + private val client: MatrixClient, + private val room: MatrixRoom, + private val roomAliasHelper: RoomAliasHelper, +) : Presenter { @Composable override fun present(): EditRoomAddressState { + val homeserverName = remember { client.userIdServerName() } + val roomAddressValidity = remember { + mutableStateOf(RoomAddressValidity.Unknown) + } + val savedRoomAddress = remember { + room.firstAliasMatching(homeserverName)?.roomAddress() + } + var currentRoomAddress by remember { + mutableStateOf( + savedRoomAddress ?: roomAliasHelper.roomAliasNameFromRoomDisplayName(room.displayName) + ) + } fun handleEvents(event: EditRoomAddressEvents) { when (event) { EditRoomAddressEvents.Save -> Unit + is EditRoomAddressEvents.RoomAddressChanged -> { + currentRoomAddress = event.roomAddress + } } } + RoomAddressValidityEffect( + client = client, + roomAliasHelper = roomAliasHelper, + newRoomAddress = currentRoomAddress, + knownRoomAddress = savedRoomAddress + ) { newRoomAddressValidity -> + roomAddressValidity.value = newRoomAddressValidity + } + return EditRoomAddressState( + homeserverName = homeserverName, + roomAddressValidity = roomAddressValidity.value, + roomAddress = currentRoomAddress, eventSink = ::handleEvents ) } + +} + +private fun MatrixRoom.firstAliasMatching(serverName: String): RoomAlias? { + // Check if the canonical alias matches the homeserver + if (this.alias?.matchesServer(serverName) == true) { + return this.alias + } + return this.alternativeAliases.firstOrNull { it.value.contains(serverName) } +} + +private fun RoomAlias.roomAddress(): String { + return value.drop(1).split(":").first() +} + +private fun RoomAlias.matchesServer(serverName: String): Boolean { + return value.contains(serverName) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressState.kt index 6709d41024e..a4d6dbe5a09 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressState.kt @@ -7,6 +7,13 @@ package io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress +import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity + data class EditRoomAddressState( + val homeserverName: String, + val roomAddress: String, + val roomAddressValidity: RoomAddressValidity, val eventSink: (EditRoomAddressEvents) -> Unit -) +) { + val canBeSaved = roomAddressValidity == RoomAddressValidity.Valid +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressStateProvider.kt index 0f610f17149..48d6615615d 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressStateProvider.kt @@ -8,6 +8,7 @@ package io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity open class EditRoomAddressStateProvider : PreviewParameterProvider { override val values: Sequence @@ -17,6 +18,14 @@ open class EditRoomAddressStateProvider : PreviewParameterProvider Unit = {} +) = EditRoomAddressState( + roomAddress = roomAddress, + roomAddressValidity = roomAddressValidity, + homeserverName = homeserverName, + eventSink = eventSink ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressView.kt index 3a4ca3eaaae..c6059dce0cf 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressView.kt @@ -8,32 +8,100 @@ package io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.MaterialTheme +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment 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.theme.ElementTheme +import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.aliasScreenTitle +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.room.address.RoomAddressField +import io.element.android.libraries.ui.strings.CommonStrings @Composable fun EditRoomAddressView( state: EditRoomAddressState, + onBackClick: () -> Unit, modifier: Modifier = Modifier, ) { - Box( - modifier = modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Text( - "EditRoomAddress feature view", - color = MaterialTheme.colorScheme.primary, - ) + Scaffold( + modifier = modifier, + topBar = { + EditRoomAddressTopBar( + isSaveActionEnabled = state.canBeSaved, + onBackClick = onBackClick, + onSaveClick = { + state.eventSink(EditRoomAddressEvents.Save) + }, + ) + } + ) { padding -> + Box( + modifier = Modifier + .padding(padding) + .imePadding() + .verticalScroll(rememberScrollState()) + .consumeWindowInsets(padding) + ) { + RoomAddressField( + address = state.roomAddress, + homeserverName = state.homeserverName, + addressValidity = state.roomAddressValidity, + onAddressChange = { + state.eventSink(EditRoomAddressEvents.RoomAddressChanged(it)) + }, + label = stringResource(CommonStrings.screen_edit_room_address_title), + supportingText = stringResource(CommonStrings.screen_edit_room_address_room_address_section_footer), + modifier = Modifier + .fillMaxWidth() + .padding(all = 16.dp) + ) + + } } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun EditRoomAddressTopBar( + isSaveActionEnabled: Boolean, + onBackClick: () -> Unit, + onSaveClick: () -> Unit, + modifier: Modifier = Modifier, +) { + TopAppBar( + modifier = modifier, + title = { + Text( + text = stringResource(CommonStrings.screen_edit_room_address_title), + style = ElementTheme.typography.aliasScreenTitle, + ) + }, + navigationIcon = { BackButton(onClick = onBackClick) }, + actions = { + TextButton( + text = stringResource(CommonStrings.action_save), + enabled = isSaveActionEnabled, + onClick = onSaveClick, + ) + } + ) +} + @PreviewsDayNight @Composable internal fun EditRoomAddressViewPreview( @@ -41,5 +109,6 @@ internal fun EditRoomAddressViewPreview( ) = ElementPreview { EditRoomAddressView( state = state, + onBackClick = {}, ) } From b7831f44cc8bda1aab18344723b9a247d4eeda3c Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 22 Jan 2025 15:12:38 +0100 Subject: [PATCH 09/30] feat(security&privacy) : expose methods from sdk to update alias and rename alias to canonicalAlias --- .../roomdetails/impl/RoomDetailsPresenter.kt | 2 +- .../EditRoomAddressPresenter.kt | 6 +++--- .../libraries/matrix/api/room/MatrixRoom.kt | 20 ++++++++++++++++++- .../matrix/api/room/alias/MatrixRoomAlias.kt | 2 +- .../matrix/impl/room/RustMatrixRoom.kt | 16 ++++++++++++++- .../matrix/test/room/FakeMatrixRoom.kt | 12 ++++++++++- 6 files changed, 50 insertions(+), 8 deletions(-) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index 4758f4d77a8..c8aa0c43c7d 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -150,7 +150,7 @@ class RoomDetailsPresenter @Inject constructor( return RoomDetailsState( roomId = room.roomId, roomName = roomName, - roomAlias = room.alias, + roomAlias = room.canonicalAlias, roomAvatarUrl = roomAvatar, roomTopic = topicState, memberCount = room.joinedMemberCount, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt index a2417e308a9..608c772cb3b 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt @@ -72,10 +72,10 @@ class EditRoomAddressPresenter @Inject constructor( private fun MatrixRoom.firstAliasMatching(serverName: String): RoomAlias? { // Check if the canonical alias matches the homeserver - if (this.alias?.matchesServer(serverName) == true) { - return this.alias + if (canonicalAlias?.matchesServer(serverName) == true) { + return canonicalAlias } - return this.alternativeAliases.firstOrNull { it.value.contains(serverName) } + return alternativeAliases.firstOrNull { it.value.contains(serverName) } } private fun RoomAlias.roomAddress(): String { diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 9275907f66d..d67ee5c676d 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -45,7 +45,7 @@ interface MatrixRoom : Closeable { val sessionId: SessionId val roomId: RoomId val displayName: String - val alias: RoomAlias? + val canonicalAlias: RoomAlias? val alternativeAliases: List val topic: String? val avatarUrl: String? @@ -432,4 +432,22 @@ interface MatrixRoom : Closeable { * directory and can be found using it. */ suspend fun getRoomVisibility(): Result + /** + * Publish a new room alias for this room in the room directory. + * + * Returns: + * - `true` if the room alias didn't exist and it's now published. + * - `false` if the room alias was already present so it couldn't be + * published. + */ + suspend fun publishRoomAliasInRoomDirectory(roomAlias: RoomAlias): Result + /** + * Remove an existing room alias for this room in the room directory. + * + * Returns: + * - `true` if the room alias was present and it's now removed from the + * room directory. + * - `false` if the room alias didn't exist so it couldn't be removed. + */ + suspend fun removeRoomAliasFromRoomDirectory(roomAlias: RoomAlias): Result } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/alias/MatrixRoomAlias.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/alias/MatrixRoomAlias.kt index 06d94287650..f9158ae3601 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/alias/MatrixRoomAlias.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/alias/MatrixRoomAlias.kt @@ -19,7 +19,7 @@ fun MatrixRoom.matches(roomIdOrAlias: RoomIdOrAlias): Boolean { roomIdOrAlias.roomId == roomId } is RoomIdOrAlias.Alias -> { - roomIdOrAlias.roomAlias == alias || roomIdOrAlias.roomAlias in alternativeAliases + roomIdOrAlias.roomAlias == canonicalAlias || roomIdOrAlias.roomAlias in alternativeAliases } } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 0823e8396c2..dd75b3f6175 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -280,7 +280,7 @@ class RustMatrixRoom( override val isEncrypted: Boolean get() = runCatching { innerRoom.isEncrypted() }.getOrDefault(false) - override val alias: RoomAlias? + override val canonicalAlias: RoomAlias? get() = runCatching { innerRoom.canonicalAlias()?.let(::RoomAlias) }.getOrDefault(null) override val alternativeAliases: List @@ -785,6 +785,20 @@ class RustMatrixRoom( } } + override suspend fun publishRoomAliasInRoomDirectory(roomAlias: RoomAlias): Result = withContext(roomDispatcher) { + runCatching { + innerRoom.publishRoomAliasInRoomDirectory(roomAlias.value) + } + } + + override suspend fun removeRoomAliasFromRoomDirectory(roomAlias: RoomAlias): Result = withContext(roomDispatcher) { + runCatching { + innerRoom.removeRoomAliasFromRoomDirectory(roomAlias.value) + } + } + + + override suspend fun updateRoomVisibility(roomVisibility: RoomVisibility): Result = withContext(roomDispatcher) { runCatching { innerRoom.updateRoomVisibility(roomVisibility.map()) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 551bb937958..daaea64e595 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -68,7 +68,7 @@ class FakeMatrixRoom( override val topic: String? = null, override val avatarUrl: String? = null, override var isEncrypted: Boolean = false, - override val alias: RoomAlias? = null, + override val canonicalAlias: RoomAlias? = null, override val alternativeAliases: List = emptyList(), override val isPublic: Boolean = true, override val isSpace: Boolean = false, @@ -151,6 +151,8 @@ class FakeMatrixRoom( private val updateRoomVisibilityResult: (RoomVisibility) -> Result = { lambdaError() }, private val updateRoomHistoryVisibilityResult: (RoomHistoryVisibility) -> Result = { lambdaError() }, private val roomVisibilityResult: () -> Result = { lambdaError() }, + private val publishRoomAliasInRoomDirectoryResult: (RoomAlias) -> Result = { lambdaError() }, + private val removeRoomAliasFromRoomDirectoryResult: (RoomAlias) -> Result = { lambdaError() }, ) : MatrixRoom { private val _roomInfoFlow: MutableSharedFlow = MutableSharedFlow(replay = 1) override val roomInfoFlow: Flow = _roomInfoFlow @@ -604,6 +606,14 @@ class FakeMatrixRoom( roomVisibilityResult() } + override suspend fun publishRoomAliasInRoomDirectory(roomAlias: RoomAlias): Result = simulateLongTask { + publishRoomAliasInRoomDirectoryResult(roomAlias) + } + + override suspend fun removeRoomAliasFromRoomDirectory(roomAlias: RoomAlias): Result = simulateLongTask { + removeRoomAliasFromRoomDirectoryResult(roomAlias) + } + fun givenRoomMembersState(state: MatrixRoomMembersState) { membersStateFlow.value = state } From 65e54474e33bbe6ea592468d4ddb9c2987c35099 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 22 Jan 2025 15:13:36 +0100 Subject: [PATCH 10/30] feat(security&privacy) : manage save action for edit room address --- .../SecurityAndPrivacyNavigator.kt | 6 ++ .../editroomaddress/EditRoomAddressEvents.kt | 1 + .../editroomaddress/EditRoomAddressNode.kt | 7 +- .../EditRoomAddressPresenter.kt | 71 +++++++++++++++++-- .../editroomaddress/EditRoomAddressState.kt | 2 + .../EditRoomAddressStateProvider.kt | 8 ++- .../editroomaddress/EditRoomAddressView.kt | 16 ++++- 7 files changed, 101 insertions(+), 10 deletions(-) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNavigator.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNavigator.kt index 050283e7395..8b6fb3312e8 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNavigator.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNavigator.kt @@ -9,10 +9,12 @@ package io.element.android.features.roomdetails.impl.securityandprivacy import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.pop import com.bumble.appyx.navmodel.backstack.operation.push interface SecurityAndPrivacyNavigator : Plugin { fun openEditRoomAddress() + fun closeEditorRoomAddress() } class BackstackSecurityAndPrivacyNavigator( @@ -21,4 +23,8 @@ class BackstackSecurityAndPrivacyNavigator( override fun openEditRoomAddress() { backStack.push(SecurityAndPrivacyFlowNode.NavTarget.EditRoomAddress) } + + override fun closeEditorRoomAddress() { + backStack.pop() + } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressEvents.kt index 69b51ce1c61..485a9d2fa03 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressEvents.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressEvents.kt @@ -9,5 +9,6 @@ package io.element.android.features.roomdetails.impl.securityandprivacy.editroom sealed interface EditRoomAddressEvents { data object Save : EditRoomAddressEvents + data object DismissError : EditRoomAddressEvents data class RoomAddressChanged(val roomAddress: String) : EditRoomAddressEvents } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressNode.kt index fbbfee203f5..9b1ece00cc4 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressNode.kt @@ -12,18 +12,23 @@ 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 dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyNavigator import io.element.android.libraries.di.RoomScope @ContributesNode(RoomScope::class) class EditRoomAddressNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val presenter: EditRoomAddressPresenter, + presenterFactory: EditRoomAddressPresenter.Factory, ) : Node(buildContext, plugins = plugins) { + private val navigator = plugins().first() + private val presenter = presenterFactory.create(navigator) + @Composable override fun View(modifier: Modifier) { val state = presenter.present() diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt index 608c772cb3b..31fbaf57eac 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt @@ -8,27 +8,44 @@ package io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyNavigator +import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper +import io.element.android.libraries.matrix.api.roomAliasFromName import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidityEffect -import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber -class EditRoomAddressPresenter @Inject constructor( +class EditRoomAddressPresenter @AssistedInject constructor( + @Assisted private val navigator: SecurityAndPrivacyNavigator, private val client: MatrixClient, private val room: MatrixRoom, private val roomAliasHelper: RoomAliasHelper, ) : Presenter { + @AssistedFactory + interface Factory { + fun create(navigator: SecurityAndPrivacyNavigator): EditRoomAddressPresenter + } @Composable override fun present(): EditRoomAddressState { + val coroutineScope = rememberCoroutineScope() val homeserverName = remember { client.userIdServerName() } val roomAddressValidity = remember { mutableStateOf(RoomAddressValidity.Unknown) @@ -36,7 +53,8 @@ class EditRoomAddressPresenter @Inject constructor( val savedRoomAddress = remember { room.firstAliasMatching(homeserverName)?.roomAddress() } - var currentRoomAddress by remember { + val saveAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } + var newRoomAddress by remember { mutableStateOf( savedRoomAddress ?: roomAliasHelper.roomAliasNameFromRoomDisplayName(room.displayName) ) @@ -44,9 +62,16 @@ class EditRoomAddressPresenter @Inject constructor( fun handleEvents(event: EditRoomAddressEvents) { when (event) { - EditRoomAddressEvents.Save -> Unit + EditRoomAddressEvents.Save -> coroutineScope.save( + saveAction = saveAction, + serverName = homeserverName, + newRoomAddress = newRoomAddress + ) is EditRoomAddressEvents.RoomAddressChanged -> { - currentRoomAddress = event.roomAddress + newRoomAddress = event.roomAddress + } + EditRoomAddressEvents.DismissError -> { + saveAction.value = AsyncAction.Uninitialized } } } @@ -54,7 +79,7 @@ class EditRoomAddressPresenter @Inject constructor( RoomAddressValidityEffect( client = client, roomAliasHelper = roomAliasHelper, - newRoomAddress = currentRoomAddress, + newRoomAddress = newRoomAddress, knownRoomAddress = savedRoomAddress ) { newRoomAddressValidity -> roomAddressValidity.value = newRoomAddressValidity @@ -63,11 +88,43 @@ class EditRoomAddressPresenter @Inject constructor( return EditRoomAddressState( homeserverName = homeserverName, roomAddressValidity = roomAddressValidity.value, - roomAddress = currentRoomAddress, + roomAddress = newRoomAddress, + saveAction = saveAction.value, eventSink = ::handleEvents ) } + private fun CoroutineScope.save( + saveAction: MutableState>, + serverName: String, + newRoomAddress: String, + ) = launch { + suspend { + val savedCanonicalAlias = room.canonicalAlias + val savedAliasFromHomeserver = room.firstAliasMatching(serverName) + val newRoomAlias = client.roomAliasFromName(newRoomAddress).getOrThrow() + + // First publish the new alias in the room directory + room.publishRoomAliasInRoomDirectory(newRoomAlias).getOrThrow() + // Then try remove the old alias from the room directory + if (savedAliasFromHomeserver != null) { + room.removeRoomAliasFromRoomDirectory(savedAliasFromHomeserver).getOrThrow() + } + // Finally update the canonical alias state.. + when { + // Allow to update the canonical alias only if the saved canonical alias matches the homeserver or if there is no canonical alias + savedCanonicalAlias == null || savedCanonicalAlias.matchesServer(serverName) -> { + room.updateCanonicalAlias(newRoomAlias, room.alternativeAliases).getOrThrow() + } + // Otherwise, update the alternative aliases and keep the current canonical alias + else -> { + val newAlternativeAliases = listOf(newRoomAlias) + room.alternativeAliases + room.updateCanonicalAlias(savedCanonicalAlias, newAlternativeAliases).getOrThrow() + } + } + navigator.closeEditorRoomAddress() + }.runCatchingUpdatingState(saveAction) + } } private fun MatrixRoom.firstAliasMatching(serverName: String): RoomAlias? { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressState.kt index a4d6dbe5a09..9bc256daba1 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressState.kt @@ -7,12 +7,14 @@ package io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress +import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity data class EditRoomAddressState( val homeserverName: String, val roomAddress: String, val roomAddressValidity: RoomAddressValidity, + val saveAction: AsyncAction, val eventSink: (EditRoomAddressEvents) -> Unit ) { val canBeSaved = roomAddressValidity == RoomAddressValidity.Valid diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressStateProvider.kt index 48d6615615d..733e9cb8198 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressStateProvider.kt @@ -8,13 +8,17 @@ package io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity open class EditRoomAddressStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aEditRoomAddressState(), - // Add other states here + aEditRoomAddressState(roomAddressValidity = RoomAddressValidity.NotAvailable), + aEditRoomAddressState(roomAddressValidity = RoomAddressValidity.InvalidSymbols), + aEditRoomAddressState(roomAddressValidity = RoomAddressValidity.Valid), + aEditRoomAddressState(roomAddressValidity = RoomAddressValidity.Valid, saveAction = AsyncAction.Loading), ) } @@ -22,10 +26,12 @@ fun aEditRoomAddressState( roomAddress: String = "therapy", roomAddressValidity: RoomAddressValidity = RoomAddressValidity.Unknown, homeserverName: String = ":myserver.org", + saveAction: AsyncAction = AsyncAction.Uninitialized, eventSink: (EditRoomAddressEvents) -> Unit = {} ) = EditRoomAddressState( roomAddress = roomAddress, roomAddressValidity = roomAddressValidity, homeserverName = homeserverName, + saveAction = saveAction, eventSink = eventSink ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressView.kt index c6059dce0cf..4c15feea3bf 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressView.kt @@ -21,6 +21,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.designsystem.components.async.AsyncActionView +import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -70,8 +72,20 @@ fun EditRoomAddressView( .fillMaxWidth() .padding(all = 16.dp) ) - } + AsyncActionView( + async = state.saveAction, + progressDialog = { + AsyncActionViewDefaults.ProgressDialog( + progressText = stringResource(CommonStrings.common_saving), + ) + }, + onSuccess = {}, + errorMessage = { stringResource(CommonStrings.error_unknown) }, + onRetry = { state.eventSink(EditRoomAddressEvents.Save) }, + onErrorDismiss = { state.eventSink(EditRoomAddressEvents.DismissError) }, + ) + } } From 0e6c86f3e5a9776bafdc5a5782bd305f92eeaf12 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 22 Jan 2025 16:34:03 +0100 Subject: [PATCH 11/30] feat(privacy&security) : extract some code for address management --- .../SecurityAndPrivacyPresenter.kt | 12 ++++++++-- .../SecurityAndPrivacyState.kt | 2 +- .../SecurityAndPrivacyStateProvider.kt | 2 +- .../SecurityAndPrivacyView.kt | 2 +- .../EditRoomAddressPresenter.kt | 16 ++++--------- .../editroomaddress/RoomAlias.kt | 24 +++++++++++++++++++ 6 files changed, 41 insertions(+), 17 deletions(-) create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/RoomAlias.kt diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt index db4a99d37bc..0c2f993e15c 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt @@ -17,10 +17,13 @@ import androidx.compose.runtime.setValue import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress.matchesServer import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility import io.element.android.libraries.matrix.api.room.join.JoinRule import java.util.Optional @@ -51,7 +54,7 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor( isEncrypted = room.isEncrypted, isVisibleInRoomDirectory = Optional.ofNullable(isVisibleInRoomDirectory.value), historyVisibility = Optional.ofNullable(roomInfo?.historyVisibility?.map()), - formattedAddress = Optional.ofNullable(roomInfo?.canonicalAlias?.value), + addressName = Optional.ofNullable(roomInfo?.firstDisplayableAlias(homeserverName)?.value), ) } } @@ -73,7 +76,7 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor( isEncrypted = currentIsEncrypted, isVisibleInRoomDirectory = currentVisibleInRoomDirectory, historyVisibility = currentHistoryVisibility, - formattedAddress = savedSettings.formattedAddress, + addressName = savedSettings.addressName, ) fun handleEvents(event: SecurityAndPrivacyEvents) { @@ -142,3 +145,8 @@ private fun SecurityAndPrivacyHistoryVisibility.map(): RoomHistoryVisibility { SecurityAndPrivacyHistoryVisibility.Anyone -> RoomHistoryVisibility.WorldReadable } } + +private fun MatrixRoomInfo.firstDisplayableAlias(serverName: String): RoomAlias? { + return aliases.firstOrNull { it.matchesServer(serverName)} ?: aliases.firstOrNull() +} + diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt index 92081359856..ba504fda9f9 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt @@ -40,7 +40,7 @@ data class SecurityAndPrivacySettings( val roomAccess: SecurityAndPrivacyRoomAccess, val isEncrypted: Boolean, val historyVisibility: Optional, - val formattedAddress: Optional, + val addressName: Optional, val isVisibleInRoomDirectory: Optional> ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt index 6955008914b..bc865a761a4 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt @@ -53,7 +53,7 @@ fun aSecurityAndPrivacySettings( ) = SecurityAndPrivacySettings( roomAccess = roomAccess, isEncrypted = isEncrypted, - formattedAddress = formattedAddress, + addressName = formattedAddress, historyVisibility = historyVisibility, isVisibleInRoomDirectory = isVisibleInRoomDirectory ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt index 8f91f199421..399fdc62f39 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt @@ -83,7 +83,7 @@ fun SecurityAndPrivacyView( if (state.showRoomVisibilitySections) { RoomVisibilitySection(state.homeserverName) RoomAddressSection( - roomAddress = state.currentSettings.formattedAddress, + roomAddress = state.currentSettings.addressName, homeserverName = state.homeserverName, onRoomAddressClick = { state.eventSink(SecurityAndPrivacyEvents.EditRoomAddress) }, isVisibleInPublicDirectory = state.currentSettings.isVisibleInRoomDirectory, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt index 31fbaf57eac..caaf4c373d9 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt @@ -30,7 +30,6 @@ import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidityEffect import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import timber.log.Timber class EditRoomAddressPresenter @AssistedInject constructor( @Assisted private val navigator: SecurityAndPrivacyNavigator, @@ -50,9 +49,7 @@ class EditRoomAddressPresenter @AssistedInject constructor( val roomAddressValidity = remember { mutableStateOf(RoomAddressValidity.Unknown) } - val savedRoomAddress = remember { - room.firstAliasMatching(homeserverName)?.roomAddress() - } + val savedRoomAddress = remember { room.firstAliasMatching(homeserverName)?.addressName() } val saveAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } var newRoomAddress by remember { mutableStateOf( @@ -127,6 +124,9 @@ class EditRoomAddressPresenter @AssistedInject constructor( } } +/** + * Returns the first alias that matches the given server name, or null if none match. + */ private fun MatrixRoom.firstAliasMatching(serverName: String): RoomAlias? { // Check if the canonical alias matches the homeserver if (canonicalAlias?.matchesServer(serverName) == true) { @@ -134,11 +134,3 @@ private fun MatrixRoom.firstAliasMatching(serverName: String): RoomAlias? { } return alternativeAliases.firstOrNull { it.value.contains(serverName) } } - -private fun RoomAlias.roomAddress(): String { - return value.drop(1).split(":").first() -} - -private fun RoomAlias.matchesServer(serverName: String): Boolean { - return value.contains(serverName) -} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/RoomAlias.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/RoomAlias.kt new file mode 100644 index 00000000000..39e955da669 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/RoomAlias.kt @@ -0,0 +1,24 @@ +/* + * 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.roomdetails.impl.securityandprivacy.editroomaddress + +import io.element.android.libraries.matrix.api.core.RoomAlias + +/** + * Returns the local part of the alias. + */ +fun RoomAlias.addressName(): String { + return value.drop(1).split(":").first() +} + +/** + * Checks if the room alias matches the given server name. + */ +fun RoomAlias.matchesServer(serverName: String): Boolean { + return value.split(":").last() == serverName +} From 392299d5cebbc910d67cfd792704832fc1ec0133 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 22 Jan 2025 17:07:11 +0100 Subject: [PATCH 12/30] feat(security&privacy) : update the save address algorithm --- .../editroomaddress/EditRoomAddressPresenter.kt | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt index caaf4c373d9..3402e2f174f 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt @@ -107,15 +107,24 @@ class EditRoomAddressPresenter @AssistedInject constructor( if (savedAliasFromHomeserver != null) { room.removeRoomAliasFromRoomDirectory(savedAliasFromHomeserver).getOrThrow() } - // Finally update the canonical alias state.. + + // Finally update the canonical alias state when { // Allow to update the canonical alias only if the saved canonical alias matches the homeserver or if there is no canonical alias savedCanonicalAlias == null || savedCanonicalAlias.matchesServer(serverName) -> { - room.updateCanonicalAlias(newRoomAlias, room.alternativeAliases).getOrThrow() + val newAlternativeAliases = room.alternativeAliases.filter { it != savedAliasFromHomeserver } + room.updateCanonicalAlias(newRoomAlias, newAlternativeAliases).getOrThrow() } - // Otherwise, update the alternative aliases and keep the current canonical alias + // Otherwise, only update the alternative aliases and keep the current canonical alias else -> { - val newAlternativeAliases = listOf(newRoomAlias) + room.alternativeAliases + val newAlternativeAliases = buildList { + add(newRoomAlias) + for (alias in room.alternativeAliases) { + if (alias != savedAliasFromHomeserver) { + add(alias) + } + } + } room.updateCanonicalAlias(savedCanonicalAlias, newAlternativeAliases).getOrThrow() } } From 7eda9453df226de738328e565ebbaeb88e493994 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 22 Jan 2025 17:55:37 +0100 Subject: [PATCH 13/30] feat(security&privacy) : manage encryption settings --- .../SecurityAndPrivacyEvents.kt | 4 +- .../SecurityAndPrivacyPresenter.kt | 19 ++++++++- .../SecurityAndPrivacyState.kt | 1 + .../SecurityAndPrivacyStateProvider.kt | 5 +++ .../SecurityAndPrivacyView.kt | 41 +++++++++++++------ 5 files changed, 55 insertions(+), 15 deletions(-) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt index d817e30a36c..204c1a72e1a 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt @@ -11,7 +11,9 @@ sealed interface SecurityAndPrivacyEvents { data object EditRoomAddress : SecurityAndPrivacyEvents data object Save : SecurityAndPrivacyEvents data class ChangeRoomAccess(val roomAccess: SecurityAndPrivacyRoomAccess) : SecurityAndPrivacyEvents - data object EnableEncryption: SecurityAndPrivacyEvents + data object ToggleEncryptionState: SecurityAndPrivacyEvents + data object CancelEnableEncryption : SecurityAndPrivacyEvents + data object ConfirmEnableEncryption: SecurityAndPrivacyEvents data class ChangeHistoryVisibility(val historyVisibility: SecurityAndPrivacyHistoryVisibility) : SecurityAndPrivacyEvents data class ChangeRoomVisibility(val isVisibleInRoomDirectory: Boolean) : SecurityAndPrivacyEvents } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt index 0c2f993e15c..257950e11a6 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt @@ -71,6 +71,9 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor( var currentIsEncrypted by remember(savedSettings.isEncrypted) { mutableStateOf(savedSettings.isEncrypted) } + + var showEncryptionConfirmation by remember { mutableStateOf(false) } + val currentSettings = SecurityAndPrivacySettings( roomAccess = currentRoomAccess, isEncrypted = currentIsEncrypted, @@ -86,8 +89,12 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor( is SecurityAndPrivacyEvents.ChangeRoomAccess -> { currentRoomAccess = event.roomAccess } - is SecurityAndPrivacyEvents.EnableEncryption -> { - currentIsEncrypted = true + is SecurityAndPrivacyEvents.ToggleEncryptionState -> { + if(currentSettings.isEncrypted) { + currentIsEncrypted = false + } else { + showEncryptionConfirmation = true + } } is SecurityAndPrivacyEvents.ChangeHistoryVisibility -> { currentHistoryVisibility = Optional.of(event.historyVisibility) @@ -96,12 +103,20 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor( currentVisibleInRoomDirectory = Optional.of(AsyncData.Success(event.isVisibleInRoomDirectory)) } SecurityAndPrivacyEvents.EditRoomAddress -> navigator.openEditRoomAddress() + SecurityAndPrivacyEvents.CancelEnableEncryption -> { + showEncryptionConfirmation = false + } + SecurityAndPrivacyEvents.ConfirmEnableEncryption -> { + showEncryptionConfirmation = false + currentIsEncrypted = true + } } } return SecurityAndPrivacyState( savedSettings = savedSettings, currentSettings = currentSettings, homeserverName = homeserverName, + showEncryptionConfirmation = showEncryptionConfirmation, eventSink = ::handleEvents ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt index ba504fda9f9..23c273dac9e 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt @@ -15,6 +15,7 @@ data class SecurityAndPrivacyState( val savedSettings: SecurityAndPrivacySettings, val currentSettings: SecurityAndPrivacySettings, val homeserverName: String, + val showEncryptionConfirmation: Boolean, val eventSink: (SecurityAndPrivacyEvents) -> Unit ) { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt index bc865a761a4..a3370147120 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt @@ -41,6 +41,9 @@ open class SecurityAndPrivacyStateProvider : PreviewParameterProvider Unit = {} ) = SecurityAndPrivacyState( currentSettings = currentSettings, savedSettings = savedSettings, homeserverName = homeserverName, + showEncryptionConfirmation = showEncryptionConfirmation, eventSink = eventSink ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt index 399fdc62f39..e6b14b6762b 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt @@ -33,6 +33,7 @@ import io.element.android.features.roomdetails.impl.R import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight @@ -69,10 +70,10 @@ fun SecurityAndPrivacyView( ) { padding -> Column( modifier = Modifier - .padding(padding) - .imePadding() - .verticalScroll(rememberScrollState()) - .consumeWindowInsets(padding), + .padding(padding) + .imePadding() + .verticalScroll(rememberScrollState()) + .consumeWindowInsets(padding), verticalArrangement = Arrangement.spacedBy(32.dp), ) { RoomAccessSection( @@ -93,9 +94,12 @@ fun SecurityAndPrivacyView( ) } EncryptionSection( - isEncryptionEnabled = state.currentSettings.isEncrypted, + isRoomEncrypted = state.currentSettings.isEncrypted, isSectionEnabled = !state.savedSettings.isEncrypted, - onEnableEncryption = { state.eventSink(SecurityAndPrivacyEvents.EnableEncryption) }, + onToggleEncryption = { state.eventSink(SecurityAndPrivacyEvents.ToggleEncryptionState) }, + showConfirmation = state.showEncryptionConfirmation, + onDismissConfirmation = { state.eventSink(SecurityAndPrivacyEvents.CancelEnableEncryption) }, + onConfirmEncryption = { state.eventSink(SecurityAndPrivacyEvents.ConfirmEnableEncryption) }, ) if (state.showRoomHistoryVisibilitySection) { RoomHistorySection( @@ -246,8 +250,8 @@ private fun RoomAddressSection( ListItemContent.Custom { CircularProgressIndicator( modifier = Modifier - .progressSemantics() - .size(20.dp), + .progressSemantics() + .size(20.dp), strokeWidth = 2.dp ) } @@ -272,9 +276,12 @@ private fun RoomAddressSection( @Composable private fun EncryptionSection( - isEncryptionEnabled: Boolean, + isRoomEncrypted: Boolean, isSectionEnabled: Boolean, - onEnableEncryption: () -> Unit, + showConfirmation: Boolean, + onToggleEncryption: () -> Unit, + onConfirmEncryption: () -> Unit, + onDismissConfirmation: () -> Unit, modifier: Modifier = Modifier, ) { SecurityAndPrivacySection( @@ -285,10 +292,20 @@ private fun EncryptionSection( headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_encryption_toggle_title)) }, supportingContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_encryption_section_footer)) }, trailingContent = ListItemContent.Switch( - checked = isEncryptionEnabled, + checked = isRoomEncrypted, enabled = isSectionEnabled, - onChange = { onEnableEncryption() } + onChange = { onToggleEncryption() }, ), + onClick = onToggleEncryption, + ) + } + if (showConfirmation) { + ConfirmationDialog( + title = stringResource(CommonStrings.screen_security_and_privacy_enable_encryption_alert_title), + content = stringResource(CommonStrings.screen_security_and_privacy_enable_encryption_alert_description), + submitText = stringResource(CommonStrings.screen_security_and_privacy_enable_encryption_alert_confirm_button_title), + onSubmitClick = onConfirmEncryption, + onDismiss = onDismissConfirmation, ) } } From 19d49a3c0b7ead64d4f9d7e4c29f488ef35bad59 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 23 Jan 2025 14:18:17 +0100 Subject: [PATCH 14/30] feat(security&privacy) : expose more methods from sdk --- .../libraries/matrix/api/room/MatrixRoom.kt | 11 +++++++++++ .../libraries/matrix/impl/room/RustMatrixRoom.kt | 16 ++++++++++++++-- .../libraries/matrix/test/room/FakeMatrixRoom.kt | 15 ++++++++++++--- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index d67ee5c676d..9e921140ab7 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.room.draft.ComposerDraft 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.knock.KnockRequest import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels @@ -450,4 +451,14 @@ interface MatrixRoom : Closeable { * - `false` if the room alias didn't exist so it couldn't be removed. */ suspend fun removeRoomAliasFromRoomDirectory(roomAlias: RoomAlias): Result + + /** + * Enable End-to-end encryption in this room. + */ + suspend fun enableEncryption(): Result + + /** + * Update the join rule for this room. + */ + suspend fun updateJoinRule(joinRule: JoinRule): Result } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index dd75b3f6175..58a76a91e8c 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -39,6 +39,7 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.draft.ComposerDraft 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.knock.KnockRequest import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels @@ -54,6 +55,7 @@ import io.element.android.libraries.matrix.impl.core.RustSendHandle import io.element.android.libraries.matrix.impl.mapper.map import io.element.android.libraries.matrix.impl.room.draft.into import io.element.android.libraries.matrix.impl.room.history.map +import io.element.android.libraries.matrix.impl.room.join.map import io.element.android.libraries.matrix.impl.room.knock.RustKnockRequest import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper @@ -797,8 +799,6 @@ class RustMatrixRoom( } } - - override suspend fun updateRoomVisibility(roomVisibility: RoomVisibility): Result = withContext(roomDispatcher) { runCatching { innerRoom.updateRoomVisibility(roomVisibility.map()) @@ -817,6 +817,18 @@ class RustMatrixRoom( } } + override suspend fun enableEncryption(): Result = withContext(roomDispatcher) { + runCatching { + innerRoom.enableEncryption() + } + } + + override suspend fun updateJoinRule(joinRule: JoinRule): Result = withContext(roomDispatcher) { + runCatching { + innerRoom.updateJoinRules(joinRule.map()) + } + } + private fun createTimeline( timeline: InnerTimeline, mode: Timeline.Mode, diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index daaea64e595..11dc695bc04 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -34,6 +34,7 @@ import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.draft.ComposerDraft 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.knock.KnockRequest import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels @@ -153,6 +154,8 @@ class FakeMatrixRoom( private val roomVisibilityResult: () -> Result = { lambdaError() }, private val publishRoomAliasInRoomDirectoryResult: (RoomAlias) -> Result = { lambdaError() }, private val removeRoomAliasFromRoomDirectoryResult: (RoomAlias) -> Result = { lambdaError() }, + private val enableEncryptionResult: () -> Result = { lambdaError() }, + private val updateJoinRuleResult: (JoinRule) -> Result = { lambdaError() }, ) : MatrixRoom { private val _roomInfoFlow: MutableSharedFlow = MutableSharedFlow(replay = 1) override val roomInfoFlow: Flow = _roomInfoFlow @@ -203,9 +206,11 @@ class FakeMatrixRoom( return Result.success(Unit) } - fun enableEncryption() { - isEncrypted = true - emitSyncUpdate() + override suspend fun enableEncryption(): Result = simulateLongTask { + enableEncryptionResult().onSuccess { + isEncrypted = true + emitSyncUpdate() + } } private val _syncUpdateFlow = MutableStateFlow(0L) @@ -614,6 +619,10 @@ class FakeMatrixRoom( removeRoomAliasFromRoomDirectoryResult(roomAlias) } + override suspend fun updateJoinRule(joinRule: JoinRule): Result = simulateLongTask { + updateJoinRuleResult(joinRule) + } + fun givenRoomMembersState(state: MatrixRoomMembersState) { membersStateFlow.value = state } From edee18a03322975306eb5b5eeddc41b8fad7ad34 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 23 Jan 2025 14:19:23 +0100 Subject: [PATCH 15/30] feat(security&privacy) : manage save action and some edge cases. --- .../SecurityAndPrivacyEvents.kt | 1 + .../SecurityAndPrivacyPresenter.kt | 208 +++++++++++++----- .../SecurityAndPrivacyState.kt | 40 ++-- .../SecurityAndPrivacyStateProvider.kt | 30 +-- .../SecurityAndPrivacyView.kt | 208 ++++++++++-------- 5 files changed, 311 insertions(+), 176 deletions(-) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt index 204c1a72e1a..be076b9c523 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt @@ -16,4 +16,5 @@ sealed interface SecurityAndPrivacyEvents { data object ConfirmEnableEncryption: SecurityAndPrivacyEvents data class ChangeHistoryVisibility(val historyVisibility: SecurityAndPrivacyHistoryVisibility) : SecurityAndPrivacyEvents data class ChangeRoomVisibility(val isVisibleInRoomDirectory: Boolean) : SecurityAndPrivacyEvents + data object DismissSaveError : SecurityAndPrivacyEvents } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt index 257950e11a6..8e5d678f098 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt @@ -8,25 +8,35 @@ package io.element.android.features.roomdetails.impl.securityandprivacy import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress.matchesServer +import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.runCatchingUpdatingState +import io.element.android.libraries.architecture.runUpdatingState import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility import io.element.android.libraries.matrix.api.room.join.JoinRule -import java.util.Optional +import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber class SecurityAndPrivacyPresenter @AssistedInject constructor( @Assisted private val navigator: SecurityAndPrivacyNavigator, @@ -40,67 +50,68 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor( @Composable override fun present(): SecurityAndPrivacyState { + val coroutineScope = rememberCoroutineScope() val homeserverName = remember { matrixClient.userIdServerName() } val roomInfo by room.roomInfoFlow.collectAsState(initial = null) - - val isVisibleInRoomDirectory = remember { - mutableStateOf>(AsyncData.Uninitialized) - } + val isVisibleInRoomDirectory by isRoomVisibleInRoomDirectory() val savedSettings by remember { derivedStateOf { SecurityAndPrivacySettings( roomAccess = roomInfo?.joinRule.map(), isEncrypted = room.isEncrypted, - isVisibleInRoomDirectory = Optional.ofNullable(isVisibleInRoomDirectory.value), - historyVisibility = Optional.ofNullable(roomInfo?.historyVisibility?.map()), - addressName = Optional.ofNullable(roomInfo?.firstDisplayableAlias(homeserverName)?.value), + isVisibleInRoomDirectory = isVisibleInRoomDirectory, + historyVisibility = roomInfo?.historyVisibility?.map(), + addressName = roomInfo?.firstDisplayableAlias(homeserverName)?.value ) } } - var currentRoomAccess by remember(savedSettings.roomAccess) { + var editedRoomAccess by remember(savedSettings.roomAccess) { mutableStateOf(savedSettings.roomAccess) } - var currentHistoryVisibility by remember(savedSettings.historyVisibility) { + var editedHistoryVisibility by remember(savedSettings.historyVisibility) { mutableStateOf(savedSettings.historyVisibility) } - var currentVisibleInRoomDirectory by remember(savedSettings.isVisibleInRoomDirectory) { + var editedVisibleInRoomDirectory by remember(savedSettings.isVisibleInRoomDirectory) { mutableStateOf(savedSettings.isVisibleInRoomDirectory) } - var currentIsEncrypted by remember(savedSettings.isEncrypted) { + var editedIsEncrypted by remember(savedSettings.isEncrypted) { mutableStateOf(savedSettings.isEncrypted) } - var showEncryptionConfirmation by remember { mutableStateOf(false) } + var showEncryptionConfirmation by remember(savedSettings.isEncrypted) { mutableStateOf(false) } - val currentSettings = SecurityAndPrivacySettings( - roomAccess = currentRoomAccess, - isEncrypted = currentIsEncrypted, - isVisibleInRoomDirectory = currentVisibleInRoomDirectory, - historyVisibility = currentHistoryVisibility, + val editedSettings = SecurityAndPrivacySettings( + roomAccess = editedRoomAccess, + isEncrypted = editedIsEncrypted, + isVisibleInRoomDirectory = editedVisibleInRoomDirectory, + historyVisibility = editedHistoryVisibility, addressName = savedSettings.addressName, ) + val saveAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } + fun handleEvents(event: SecurityAndPrivacyEvents) { when (event) { SecurityAndPrivacyEvents.Save -> { + coroutineScope.save(saveAction, savedSettings, editedSettings) } is SecurityAndPrivacyEvents.ChangeRoomAccess -> { - currentRoomAccess = event.roomAccess + editedRoomAccess = event.roomAccess } is SecurityAndPrivacyEvents.ToggleEncryptionState -> { - if(currentSettings.isEncrypted) { - currentIsEncrypted = false + if (editedSettings.isEncrypted) { + editedIsEncrypted = false } else { showEncryptionConfirmation = true } } is SecurityAndPrivacyEvents.ChangeHistoryVisibility -> { - currentHistoryVisibility = Optional.of(event.historyVisibility) + editedHistoryVisibility = event.historyVisibility } is SecurityAndPrivacyEvents.ChangeRoomVisibility -> { - currentVisibleInRoomDirectory = Optional.of(AsyncData.Success(event.isVisibleInRoomDirectory)) + editedVisibleInRoomDirectory = AsyncData.Success(event.isVisibleInRoomDirectory) } SecurityAndPrivacyEvents.EditRoomAddress -> navigator.openEditRoomAddress() SecurityAndPrivacyEvents.CancelEnableEncryption -> { @@ -108,60 +119,137 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor( } SecurityAndPrivacyEvents.ConfirmEnableEncryption -> { showEncryptionConfirmation = false - currentIsEncrypted = true + editedIsEncrypted = true + } + SecurityAndPrivacyEvents.DismissSaveError -> { + saveAction.value = AsyncAction.Uninitialized } } } - return SecurityAndPrivacyState( + + val state = SecurityAndPrivacyState( savedSettings = savedSettings, - currentSettings = currentSettings, + editedSettings = editedSettings, homeserverName = homeserverName, showEncryptionConfirmation = showEncryptionConfirmation, + saveAction = saveAction.value, eventSink = ::handleEvents ) + LaunchedEffect(state.availableHistoryVisibilities) { + editedSettings.historyVisibility?.also { + if (it !in state.availableHistoryVisibilities) { + editedHistoryVisibility = it.fallback() + } + } + } + return state } -} -private fun JoinRule?.map(): SecurityAndPrivacyRoomAccess { - return when (this) { - JoinRule.Public -> SecurityAndPrivacyRoomAccess.Anyone - JoinRule.Knock, is JoinRule.KnockRestricted -> SecurityAndPrivacyRoomAccess.AskToJoin - is JoinRule.Restricted -> SecurityAndPrivacyRoomAccess.SpaceMember - is JoinRule.Custom, - JoinRule.Invite, - JoinRule.Private, - null -> SecurityAndPrivacyRoomAccess.InviteOnly + @Composable + private fun isRoomVisibleInRoomDirectory(): State> { + val result = remember { mutableStateOf>(AsyncData.Uninitialized) } + LaunchedEffect(Unit) { + result.runUpdatingState { + room.getRoomVisibility().map { it == RoomVisibility.Public } + } + } + return result } -} -private fun SecurityAndPrivacyRoomAccess.map(): JoinRule { - return when (this) { - SecurityAndPrivacyRoomAccess.Anyone -> JoinRule.Public - SecurityAndPrivacyRoomAccess.AskToJoin -> JoinRule.Knock - SecurityAndPrivacyRoomAccess.InviteOnly -> JoinRule.Private - SecurityAndPrivacyRoomAccess.SpaceMember -> error("Unsupported") + private fun CoroutineScope.save( + saveAction: MutableState>, + savedSettings: SecurityAndPrivacySettings, + editedSettings: SecurityAndPrivacySettings, + ) = launch { + suspend { + var somethingWentWrong = false + if (editedSettings.isEncrypted && !savedSettings.isEncrypted) { + room + .enableEncryption() + .onFailure { + Timber.d("Failed to enable encryption") + somethingWentWrong = true + } + } + if (editedSettings.historyVisibility != null && editedSettings.historyVisibility != savedSettings.historyVisibility) { + room + .updateHistoryVisibility(editedSettings.historyVisibility.map()) + .onFailure { + Timber.d("Failed to update history visibility") + somethingWentWrong = true + } + } + if (editedSettings.roomAccess != savedSettings.roomAccess) { + room + .updateJoinRule(editedSettings.roomAccess.map()) + .onFailure { + Timber.d("Failed to update join rule") + somethingWentWrong = true + } + } + + val editedIsVisibleInRoomDirectory = when (editedSettings.roomAccess) { + SecurityAndPrivacyRoomAccess.AskToJoin, + SecurityAndPrivacyRoomAccess.Anyone -> editedSettings.isVisibleInRoomDirectory.dataOrNull() + else -> false + } + val savedIsVisibleInRoomDirectory = savedSettings.isVisibleInRoomDirectory.dataOrNull() + if (editedIsVisibleInRoomDirectory != null && editedIsVisibleInRoomDirectory != savedIsVisibleInRoomDirectory) { + val roomVisibility = if (editedIsVisibleInRoomDirectory) RoomVisibility.Public else RoomVisibility.Private + room + .updateRoomVisibility(roomVisibility) + .onFailure { + Timber.d("Failed to update room visibility") + somethingWentWrong = true + } + } + if (somethingWentWrong) { + error("") + } + }.runCatchingUpdatingState(saveAction) } -} -private fun RoomHistoryVisibility.map(): SecurityAndPrivacyHistoryVisibility { - return when (this) { - RoomHistoryVisibility.Joined, - RoomHistoryVisibility.Invited -> SecurityAndPrivacyHistoryVisibility.SinceInvite - RoomHistoryVisibility.Shared, - is RoomHistoryVisibility.Custom -> SecurityAndPrivacyHistoryVisibility.SinceSelection - RoomHistoryVisibility.WorldReadable -> SecurityAndPrivacyHistoryVisibility.Anyone + private fun JoinRule?.map(): SecurityAndPrivacyRoomAccess { + return when (this) { + JoinRule.Public -> SecurityAndPrivacyRoomAccess.Anyone + JoinRule.Knock, is JoinRule.KnockRestricted -> SecurityAndPrivacyRoomAccess.AskToJoin + is JoinRule.Restricted -> SecurityAndPrivacyRoomAccess.SpaceMember + is JoinRule.Custom, + JoinRule.Invite, + JoinRule.Private, + null -> SecurityAndPrivacyRoomAccess.InviteOnly + } } -} -private fun SecurityAndPrivacyHistoryVisibility.map(): RoomHistoryVisibility { - return when (this) { - SecurityAndPrivacyHistoryVisibility.SinceSelection -> RoomHistoryVisibility.Shared - SecurityAndPrivacyHistoryVisibility.SinceInvite -> RoomHistoryVisibility.Invited - SecurityAndPrivacyHistoryVisibility.Anyone -> RoomHistoryVisibility.WorldReadable + private fun SecurityAndPrivacyRoomAccess.map(): JoinRule { + return when (this) { + SecurityAndPrivacyRoomAccess.Anyone -> JoinRule.Public + SecurityAndPrivacyRoomAccess.AskToJoin -> JoinRule.Knock + SecurityAndPrivacyRoomAccess.InviteOnly -> JoinRule.Private + SecurityAndPrivacyRoomAccess.SpaceMember -> error("Unsupported") + } + } + + private fun RoomHistoryVisibility.map(): SecurityAndPrivacyHistoryVisibility { + return when (this) { + RoomHistoryVisibility.Joined, + RoomHistoryVisibility.Invited -> SecurityAndPrivacyHistoryVisibility.SinceInvite + RoomHistoryVisibility.Shared, + is RoomHistoryVisibility.Custom -> SecurityAndPrivacyHistoryVisibility.SinceSelection + RoomHistoryVisibility.WorldReadable -> SecurityAndPrivacyHistoryVisibility.Anyone + } } -} -private fun MatrixRoomInfo.firstDisplayableAlias(serverName: String): RoomAlias? { - return aliases.firstOrNull { it.matchesServer(serverName)} ?: aliases.firstOrNull() + private fun SecurityAndPrivacyHistoryVisibility.map(): RoomHistoryVisibility { + return when (this) { + SecurityAndPrivacyHistoryVisibility.SinceSelection -> RoomHistoryVisibility.Shared + SecurityAndPrivacyHistoryVisibility.SinceInvite -> RoomHistoryVisibility.Invited + SecurityAndPrivacyHistoryVisibility.Anyone -> RoomHistoryVisibility.WorldReadable + } + } + + private fun MatrixRoomInfo.firstDisplayableAlias(serverName: String): RoomAlias? { + return aliases.firstOrNull { it.matchesServer(serverName) } ?: aliases.firstOrNull() + } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt index 23c273dac9e..3209c71b521 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt @@ -7,46 +7,58 @@ package io.element.android.features.roomdetails.impl.securityandprivacy +import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData -import java.util.Optional -import kotlin.jvm.optionals.getOrNull data class SecurityAndPrivacyState( + // the settings that are currently applied on the room. val savedSettings: SecurityAndPrivacySettings, - val currentSettings: SecurityAndPrivacySettings, + // the settings the user wants to apply. + val editedSettings: SecurityAndPrivacySettings, val homeserverName: String, val showEncryptionConfirmation: Boolean, + val saveAction: AsyncAction, val eventSink: (SecurityAndPrivacyEvents) -> Unit ) { - val canBeSaved = savedSettings != currentSettings + val canBeSaved = savedSettings != editedSettings - val showRoomVisibilitySections = currentSettings.roomAccess != SecurityAndPrivacyRoomAccess.InviteOnly && currentSettings.historyVisibility.isPresent val availableHistoryVisibilities = buildSet { add(SecurityAndPrivacyHistoryVisibility.SinceSelection) - if (currentSettings.roomAccess == SecurityAndPrivacyRoomAccess.Anyone && !currentSettings.isEncrypted) { + if (editedSettings.roomAccess == SecurityAndPrivacyRoomAccess.Anyone && !editedSettings.isEncrypted) { add(SecurityAndPrivacyHistoryVisibility.Anyone) } else { add(SecurityAndPrivacyHistoryVisibility.SinceInvite) } - if (savedSettings.historyVisibility.getOrNull() == SecurityAndPrivacyHistoryVisibility.SinceInvite) { - add(SecurityAndPrivacyHistoryVisibility.SinceInvite) - } } - val showRoomHistoryVisibilitySection = availableHistoryVisibilities.isNotEmpty() && currentSettings.historyVisibility.isPresent + val showRoomAccessSection: Boolean = true + val showRoomVisibilitySections = editedSettings.roomAccess != SecurityAndPrivacyRoomAccess.InviteOnly + val showHistoryVisibilitySection = editedSettings.historyVisibility != null + val showEncryptionSection = true } data class SecurityAndPrivacySettings( val roomAccess: SecurityAndPrivacyRoomAccess, val isEncrypted: Boolean, - val historyVisibility: Optional, - val addressName: Optional, - val isVisibleInRoomDirectory: Optional> + val historyVisibility: SecurityAndPrivacyHistoryVisibility?, + val addressName: String?, + val isVisibleInRoomDirectory: AsyncData ) enum class SecurityAndPrivacyHistoryVisibility { - SinceSelection, SinceInvite, Anyone + SinceSelection, SinceInvite, Anyone; + + /** + * Returns the fallback visibility when the current visibility is not available. + */ + fun fallback(): SecurityAndPrivacyHistoryVisibility { + return when (this) { + SinceSelection -> SinceSelection + SinceInvite -> Anyone + Anyone -> SinceInvite + } + } } enum class SecurityAndPrivacyRoomAccess { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt index a3370147120..8bf9b9757e3 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt @@ -8,37 +8,37 @@ package io.element.android.features.roomdetails.impl.securityandprivacy import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData -import java.util.Optional open class SecurityAndPrivacyStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aSecurityAndPrivacyState(), aSecurityAndPrivacyState( - currentSettings = aSecurityAndPrivacySettings( + editedSettings = aSecurityAndPrivacySettings( roomAccess = SecurityAndPrivacyRoomAccess.AskToJoin ) ), aSecurityAndPrivacyState( - currentSettings = aSecurityAndPrivacySettings( + editedSettings = aSecurityAndPrivacySettings( roomAccess = SecurityAndPrivacyRoomAccess.Anyone, isEncrypted = false, ) ), aSecurityAndPrivacyState( - currentSettings = aSecurityAndPrivacySettings( + editedSettings = aSecurityAndPrivacySettings( roomAccess = SecurityAndPrivacyRoomAccess.SpaceMember ) ), aSecurityAndPrivacyState( - currentSettings = aSecurityAndPrivacySettings( - isVisibleInRoomDirectory = Optional.of(AsyncData.Loading()) + editedSettings = aSecurityAndPrivacySettings( + isVisibleInRoomDirectory = AsyncData.Loading() ) ), aSecurityAndPrivacyState( - currentSettings = aSecurityAndPrivacySettings( - isVisibleInRoomDirectory = Optional.of(AsyncData.Success(true)) + editedSettings = aSecurityAndPrivacySettings( + isVisibleInRoomDirectory = AsyncData.Success(true) ) ), aSecurityAndPrivacyState( @@ -50,9 +50,9 @@ open class SecurityAndPrivacyStateProvider : PreviewParameterProvider = Optional.empty(), - historyVisibility: Optional = Optional.of(SecurityAndPrivacyHistoryVisibility.SinceSelection), - isVisibleInRoomDirectory: Optional> = Optional.empty() + formattedAddress: String? = null, + historyVisibility: SecurityAndPrivacyHistoryVisibility? = null, + isVisibleInRoomDirectory: AsyncData = AsyncData.Uninitialized, ) = SecurityAndPrivacySettings( roomAccess = roomAccess, isEncrypted = isEncrypted, @@ -62,15 +62,17 @@ fun aSecurityAndPrivacySettings( ) fun aSecurityAndPrivacyState( - currentSettings: SecurityAndPrivacySettings = aSecurityAndPrivacySettings(), - savedSettings: SecurityAndPrivacySettings = currentSettings, + savedSettings: SecurityAndPrivacySettings = aSecurityAndPrivacySettings(), + editedSettings: SecurityAndPrivacySettings = savedSettings, homeserverName: String = "myserver.xyz", showEncryptionConfirmation: Boolean = false, + saveAction: AsyncAction = AsyncAction.Uninitialized, eventSink: (SecurityAndPrivacyEvents) -> Unit = {} ) = SecurityAndPrivacyState( - currentSettings = currentSettings, + editedSettings = editedSettings, savedSettings = savedSettings, homeserverName = homeserverName, showEncryptionConfirmation = showEncryptionConfirmation, + saveAction = saveAction, eventSink = eventSink ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt index e6b14b6762b..552d80186aa 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt @@ -32,6 +32,8 @@ import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.roomdetails.impl.R import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage +import io.element.android.libraries.designsystem.components.async.AsyncActionView +import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.list.ListItemContent @@ -47,8 +49,6 @@ 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.ui.strings.CommonStrings -import java.util.Optional -import kotlin.jvm.optionals.getOrNull @Composable fun SecurityAndPrivacyView( @@ -76,40 +76,58 @@ fun SecurityAndPrivacyView( .consumeWindowInsets(padding), verticalArrangement = Arrangement.spacedBy(32.dp), ) { - RoomAccessSection( - modifier = Modifier.padding(top = 24.dp), - selected = state.currentSettings.roomAccess, - onSelected = { state.eventSink(SecurityAndPrivacyEvents.ChangeRoomAccess(it)) }, - ) + if (state.showRoomAccessSection) { + RoomAccessSection( + modifier = Modifier.padding(top = 24.dp), + edited = state.editedSettings.roomAccess, + saved = state.savedSettings.roomAccess, + onSelected = { state.eventSink(SecurityAndPrivacyEvents.ChangeRoomAccess(it)) }, + ) + } if (state.showRoomVisibilitySections) { RoomVisibilitySection(state.homeserverName) RoomAddressSection( - roomAddress = state.currentSettings.addressName, + roomAddress = state.editedSettings.addressName, homeserverName = state.homeserverName, onRoomAddressClick = { state.eventSink(SecurityAndPrivacyEvents.EditRoomAddress) }, - isVisibleInPublicDirectory = state.currentSettings.isVisibleInRoomDirectory, + isVisibleInRoomDirectory = state.editedSettings.isVisibleInRoomDirectory, onVisibilityChange = { isVisible -> state.eventSink(SecurityAndPrivacyEvents.ChangeRoomVisibility(isVisible)) }, ) } - EncryptionSection( - isRoomEncrypted = state.currentSettings.isEncrypted, - isSectionEnabled = !state.savedSettings.isEncrypted, - onToggleEncryption = { state.eventSink(SecurityAndPrivacyEvents.ToggleEncryptionState) }, - showConfirmation = state.showEncryptionConfirmation, - onDismissConfirmation = { state.eventSink(SecurityAndPrivacyEvents.CancelEnableEncryption) }, - onConfirmEncryption = { state.eventSink(SecurityAndPrivacyEvents.ConfirmEnableEncryption) }, - ) - if (state.showRoomHistoryVisibilitySection) { - RoomHistorySection( - selectedOption = state.currentSettings.historyVisibility.get(), + if (state.showEncryptionSection) { + EncryptionSection( + isRoomEncrypted = state.editedSettings.isEncrypted, + canToggleEncryption = !state.savedSettings.isEncrypted, + onToggleEncryption = { state.eventSink(SecurityAndPrivacyEvents.ToggleEncryptionState) }, + showConfirmation = state.showEncryptionConfirmation, + onDismissConfirmation = { state.eventSink(SecurityAndPrivacyEvents.CancelEnableEncryption) }, + onConfirmEncryption = { state.eventSink(SecurityAndPrivacyEvents.ConfirmEnableEncryption) }, + ) + } + if (state.showHistoryVisibilitySection) { + HistoryVisibilitySection( + editedOption = state.editedSettings.historyVisibility, + savedOptions = state.savedSettings.historyVisibility, availableOptions = state.availableHistoryVisibilities, onSelected = { state.eventSink(SecurityAndPrivacyEvents.ChangeHistoryVisibility(it)) }, ) } } } + AsyncActionView( + async = state.saveAction, + onSuccess = { }, + onErrorDismiss = { state.eventSink(SecurityAndPrivacyEvents.DismissSaveError) }, + errorMessage = { stringResource(CommonStrings.error_unknown) }, + progressDialog = { + AsyncActionViewDefaults.ProgressDialog( + progressText = stringResource(CommonStrings.common_saving), + ) + }, + onRetry = { state.eventSink(SecurityAndPrivacyEvents.Save) }, + ) } @OptIn(ExperimentalMaterial3Api::class) @@ -160,7 +178,8 @@ private fun SecurityAndPrivacySection( @Composable private fun RoomAccessSection( - selected: SecurityAndPrivacyRoomAccess, + edited: SecurityAndPrivacyRoomAccess, + saved: SecurityAndPrivacyRoomAccess, onSelected: (SecurityAndPrivacyRoomAccess) -> Unit, modifier: Modifier = Modifier, ) { @@ -171,26 +190,27 @@ private fun RoomAccessSection( ListItem( headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_access_invite_only_option_title)) }, supportingContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_access_invite_only_option_description)) }, - trailingContent = ListItemContent.RadioButton(selected = selected == SecurityAndPrivacyRoomAccess.InviteOnly), + trailingContent = ListItemContent.RadioButton(selected = edited == SecurityAndPrivacyRoomAccess.InviteOnly), onClick = { onSelected(SecurityAndPrivacyRoomAccess.InviteOnly) }, ) ListItem( headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_ask_to_join_option_title)) }, supportingContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_ask_to_join_option_description)) }, - trailingContent = ListItemContent.RadioButton(selected = selected == SecurityAndPrivacyRoomAccess.AskToJoin), + trailingContent = ListItemContent.RadioButton(selected = edited == SecurityAndPrivacyRoomAccess.AskToJoin), onClick = { onSelected(SecurityAndPrivacyRoomAccess.AskToJoin) }, ) ListItem( headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_access_anyone_option_title)) }, supportingContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_access_anyone_option_description)) }, - trailingContent = ListItemContent.RadioButton(selected = selected == SecurityAndPrivacyRoomAccess.Anyone), + trailingContent = ListItemContent.RadioButton(selected = edited == SecurityAndPrivacyRoomAccess.Anyone), onClick = { onSelected(SecurityAndPrivacyRoomAccess.Anyone) }, ) - if (selected == SecurityAndPrivacyRoomAccess.SpaceMember) { + if (saved == SecurityAndPrivacyRoomAccess.SpaceMember) { ListItem( headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_access_space_members_option_title)) }, supportingContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_access_space_members_option_description)) }, trailingContent = ListItemContent.RadioButton(selected = true, enabled = false), + enabled = false, ) } } @@ -217,9 +237,9 @@ private fun RoomVisibilitySection( @Composable private fun RoomAddressSection( - roomAddress: Optional, + roomAddress: String?, homeserverName: String, - isVisibleInPublicDirectory: Optional>, + isVisibleInRoomDirectory: AsyncData, onRoomAddressClick: () -> Unit, onVisibilityChange: (Boolean) -> Unit, modifier: Modifier = Modifier, @@ -230,54 +250,53 @@ private fun RoomAddressSection( ) { ListItem( headlineContent = { - Text(text = roomAddress.getOrNull() ?: stringResource(CommonStrings.screen_security_and_privacy_add_room_address_action)) + Text(text = roomAddress ?: stringResource(CommonStrings.screen_security_and_privacy_add_room_address_action)) }, - trailingContent = if (roomAddress.isEmpty) ListItemContent.Icon(IconSource.Vector(CompoundIcons.Plus())) else null, + trailingContent = if (roomAddress.isNullOrEmpty()) ListItemContent.Icon(IconSource.Vector(CompoundIcons.Plus())) else null, supportingContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_address_section_footer)) }, onClick = onRoomAddressClick, colors = ListItemDefaults.colors(trailingIconColor = ElementTheme.colors.iconAccentPrimary), alwaysClickable = true ) - if (isVisibleInPublicDirectory.isPresent) { - ListItem( - headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_directory_visibility_toggle_title)) }, - supportingContent = { - Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_directory_visibility_section_footer, homeserverName)) - }, - trailingContent = - when (val isVisible = isVisibleInPublicDirectory.get()) { - is AsyncData.Uninitialized, is AsyncData.Loading -> { - ListItemContent.Custom { - CircularProgressIndicator( - modifier = Modifier - .progressSemantics() - .size(20.dp), - strokeWidth = 2.dp - ) - } - } - is AsyncData.Failure -> { - ListItemContent.Switch( - checked = false, - enabled = false, - ) - } - is AsyncData.Success -> { - ListItemContent.Switch( - checked = isVisible.data, - onChange = onVisibilityChange + + ListItem( + headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_directory_visibility_toggle_title)) }, + supportingContent = { + Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_directory_visibility_section_footer, homeserverName)) + }, + trailingContent = + when (isVisibleInRoomDirectory) { + is AsyncData.Uninitialized, is AsyncData.Loading -> { + ListItemContent.Custom { + CircularProgressIndicator( + modifier = Modifier + .progressSemantics() + .size(20.dp), + strokeWidth = 2.dp ) } } - ) - } + is AsyncData.Failure -> { + ListItemContent.Switch( + checked = false, + enabled = false, + ) + } + is AsyncData.Success -> { + ListItemContent.Switch( + checked = isVisibleInRoomDirectory.data, + onChange = onVisibilityChange, + ) + } + } + ) } } @Composable private fun EncryptionSection( isRoomEncrypted: Boolean, - isSectionEnabled: Boolean, + canToggleEncryption: Boolean, showConfirmation: Boolean, onToggleEncryption: () -> Unit, onConfirmEncryption: () -> Unit, @@ -293,10 +312,10 @@ private fun EncryptionSection( supportingContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_encryption_section_footer)) }, trailingContent = ListItemContent.Switch( checked = isRoomEncrypted, - enabled = isSectionEnabled, + enabled = canToggleEncryption, onChange = { onToggleEncryption() }, ), - onClick = onToggleEncryption, + onClick = if (canToggleEncryption) onToggleEncryption else null ) } if (showConfirmation) { @@ -311,8 +330,9 @@ private fun EncryptionSection( } @Composable -private fun RoomHistorySection( - selectedOption: SecurityAndPrivacyHistoryVisibility, +private fun HistoryVisibilitySection( + editedOption: SecurityAndPrivacyHistoryVisibility?, + savedOptions: SecurityAndPrivacyHistoryVisibility?, availableOptions: Set, onSelected: (SecurityAndPrivacyHistoryVisibility) -> Unit, modifier: Modifier = Modifier, @@ -323,34 +343,46 @@ private fun RoomHistorySection( ) { Spacer(Modifier.height(16.dp)) for (availableOption in availableOptions) { - val isSelected = availableOption == selectedOption - when (availableOption) { - SecurityAndPrivacyHistoryVisibility.SinceSelection -> { - ListItem( - headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_history_since_selecting_option_title)) }, - trailingContent = ListItemContent.RadioButton(selected = isSelected), - onClick = { onSelected(availableOption) }, - ) - } - SecurityAndPrivacyHistoryVisibility.SinceInvite -> { - ListItem( - headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_history_since_invite_option_title)) }, - trailingContent = ListItemContent.RadioButton(selected = isSelected), - onClick = { onSelected(availableOption) }, - ) - } - SecurityAndPrivacyHistoryVisibility.Anyone -> { - ListItem( - headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_history_anyone_option_title)) }, - trailingContent = ListItemContent.RadioButton(selected = isSelected), - onClick = { onSelected(availableOption) }, - ) - } - } + val isSelected = availableOption == editedOption + HistoryVisibilityItem( + option = availableOption, + isSelected = isSelected, + onSelected = onSelected, + ) + } + if (savedOptions != null && !availableOptions.contains(savedOptions)) { + HistoryVisibilityItem( + option = savedOptions, + isSelected = true, + isEnabled = false, + onSelected = {}, + ) } } } +@Composable +private fun HistoryVisibilityItem( + option: SecurityAndPrivacyHistoryVisibility, + isSelected: Boolean, + onSelected: (SecurityAndPrivacyHistoryVisibility) -> Unit, + modifier: Modifier = Modifier, + isEnabled: Boolean = true, +) { + val headlineText = when (option) { + SecurityAndPrivacyHistoryVisibility.SinceSelection -> stringResource(CommonStrings.screen_security_and_privacy_room_history_since_selecting_option_title) + SecurityAndPrivacyHistoryVisibility.SinceInvite -> stringResource(CommonStrings.screen_security_and_privacy_room_history_since_invite_option_title) + SecurityAndPrivacyHistoryVisibility.Anyone -> stringResource(CommonStrings.screen_security_and_privacy_room_history_anyone_option_title) + } + ListItem( + headlineContent = { Text(text = headlineText) }, + trailingContent = ListItemContent.RadioButton(selected = isSelected, enabled = isEnabled), + onClick = { onSelected(option) }, + enabled = isEnabled, + modifier = modifier, + ) +} + @PreviewWithLargeHeight @Composable internal fun SecurityAndPrivacyViewLightPreview(@PreviewParameter(SecurityAndPrivacyStateProvider::class) state: SecurityAndPrivacyState) = From 75fef6b325afe849ad6a36f9c8fd10c7b5b8ec99 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 23 Jan 2025 21:51:25 +0100 Subject: [PATCH 16/30] feat(security&privacy) : introduce permissions and use in RoomDetails --- .../roomdetails/impl/RoomDetailsPresenter.kt | 4 ++ .../roomdetails/impl/RoomDetailsState.kt | 1 + .../impl/RoomDetailsStateProvider.kt | 2 + .../roomdetails/impl/RoomDetailsView.kt | 9 ++-- .../SecurityAndPrivacyPermissions.kt | 50 +++++++++++++++++++ 5 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/permissions/SecurityAndPrivacyPermissions.kt diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index c8aa0c43c7d..009882b537b 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -24,6 +24,7 @@ import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.messages.api.pinned.IsPinnedMessagesFeatureEnabled import io.element.android.features.roomcall.api.RoomCallState import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter +import io.element.android.features.roomdetails.impl.securityandprivacy.permissions.securityAndPrivacyPermissionsAsState import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.coroutine.CoroutineDispatchers @@ -147,6 +148,8 @@ class RoomDetailsPresenter @Inject constructor( val roomMemberDetailsState = roomMemberDetailsPresenter?.present() + val securityAndPrivacyPermissions by room.securityAndPrivacyPermissionsAsState(syncUpdateFlow.value) + return RoomDetailsState( roomId = room.roomId, roomName = roomName, @@ -172,6 +175,7 @@ class RoomDetailsPresenter @Inject constructor( pinnedMessagesCount = pinnedMessagesCount, canShowKnockRequests = canShowKnockRequests, knockRequestsCount = knockRequestsCount, + canShowSecurityAndPrivacy = securityAndPrivacyPermissions.hasAny, eventSink = ::handleEvents, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt index 34f7c38966e..7ec039e320b 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt @@ -44,6 +44,7 @@ data class RoomDetailsState( val pinnedMessagesCount: Int?, val canShowKnockRequests: Boolean, val knockRequestsCount: Int?, + val canShowSecurityAndPrivacy: Boolean, val eventSink: (RoomDetailsEvent) -> Unit ) { val roomBadges = buildList { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt index d1ae7cdaa2f..59601f065da 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt @@ -107,6 +107,7 @@ fun aRoomDetailsState( pinnedMessagesCount: Int? = null, canShowKnockRequests: Boolean = false, knockRequestsCount: Int? = null, + canShowSecurityAndPrivacy: Boolean = true, eventSink: (RoomDetailsEvent) -> Unit = {}, ) = RoomDetailsState( roomId = roomId, @@ -133,6 +134,7 @@ fun aRoomDetailsState( pinnedMessagesCount = pinnedMessagesCount, canShowKnockRequests = canShowKnockRequests, knockRequestsCount = knockRequestsCount, + canShowSecurityAndPrivacy = canShowSecurityAndPrivacy, eventSink = eventSink ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt index 7d2bafb3986..e35de9f5ec9 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt @@ -183,10 +183,11 @@ fun RoomDetailsView( state.eventSink(RoomDetailsEvent.SetFavorite(it)) } ) - - SecurityAndPrivacyItem( - onClick = onSecurityAndPrivacyClick - ) + if (state.canShowSecurityAndPrivacy) { + SecurityAndPrivacyItem( + onClick = onSecurityAndPrivacyClick + ) + } } if (state.roomType is RoomDetailsType.Room) { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/permissions/SecurityAndPrivacyPermissions.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/permissions/SecurityAndPrivacyPermissions.kt new file mode 100644 index 00000000000..84436057f49 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/permissions/SecurityAndPrivacyPermissions.kt @@ -0,0 +1,50 @@ +/* + * 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.roomdetails.impl.securityandprivacy.permissions + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.produceState +import io.element.android.features.roomdetails.impl.securityandprivacy.permissions.SecurityAndPrivacyPermissions.Companion.DEFAULT +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.StateEventType +import io.element.android.libraries.matrix.api.room.powerlevels.canSendState + +data class SecurityAndPrivacyPermissions( + val canChangeRoomAccess: Boolean, + val canChangeHistoryVisibility: Boolean, + val canChangeEncryption: Boolean, + val canChangeRoomVisibility: Boolean, +) { + val hasAny = canChangeRoomAccess || + canChangeHistoryVisibility || + canChangeEncryption || + canChangeRoomVisibility + + companion object { + val DEFAULT = SecurityAndPrivacyPermissions( + canChangeRoomAccess = false, + canChangeHistoryVisibility = false, + canChangeEncryption = false, + canChangeRoomVisibility = false, + ) + } +} + +@Composable +fun MatrixRoom.securityAndPrivacyPermissionsAsState(updateKey: Long): State { + return produceState(DEFAULT, key1 = updateKey) { + value = SecurityAndPrivacyPermissions( + canChangeRoomAccess = canSendState(type = StateEventType.ROOM_JOIN_RULES).getOrElse { false }, + canChangeHistoryVisibility = canSendState(type = StateEventType.ROOM_HISTORY_VISIBILITY).getOrElse { false }, + canChangeEncryption = canSendState(type = StateEventType.ROOM_ENCRYPTION).getOrElse { false }, + canChangeRoomVisibility = canSendState(type = StateEventType.ROOM_CANONICAL_ALIAS).getOrElse { false }, + ) + } +} + From 88fce64d2f9ca3ec2d00fc86bc277f77ab05b2af Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 23 Jan 2025 23:29:35 +0100 Subject: [PATCH 17/30] feat(security&privacy) : use permissions and improve save --- .../SecurityAndPrivacyPresenter.kt | 214 ++++++++++-------- .../SecurityAndPrivacyState.kt | 22 +- .../SecurityAndPrivacyStateProvider.kt | 12 +- .../SecurityAndPrivacyView.kt | 1 - 4 files changed, 143 insertions(+), 106 deletions(-) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt index 8e5d678f098..fcd2946f85e 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt @@ -10,7 +10,6 @@ package io.element.android.features.roomdetails.impl.securityandprivacy import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState -import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -22,6 +21,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress.matchesServer +import io.element.android.features.roomdetails.impl.securityandprivacy.permissions.securityAndPrivacyPermissionsAsState import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter @@ -35,8 +35,10 @@ import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibilit import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import timber.log.Timber class SecurityAndPrivacyPresenter @AssistedInject constructor( @Assisted private val navigator: SecurityAndPrivacyNavigator, @@ -51,17 +53,22 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor( @Composable override fun present(): SecurityAndPrivacyState { val coroutineScope = rememberCoroutineScope() + + val saveAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } val homeserverName = remember { matrixClient.userIdServerName() } - val roomInfo by room.roomInfoFlow.collectAsState(initial = null) - val isVisibleInRoomDirectory by isRoomVisibleInRoomDirectory() + val syncUpdateFlow = room.syncUpdateFlow.collectAsState() + val roomInfo by room.roomInfoFlow.collectAsState(null) + + val savedIsVisibleInRoomDirectory = remember { mutableStateOf>(AsyncData.Uninitialized) } + IsRoomVisibleInRoomDirectoryEffect(savedIsVisibleInRoomDirectory) val savedSettings by remember { derivedStateOf { SecurityAndPrivacySettings( roomAccess = roomInfo?.joinRule.map(), isEncrypted = room.isEncrypted, - isVisibleInRoomDirectory = isVisibleInRoomDirectory, - historyVisibility = roomInfo?.historyVisibility?.map(), + isVisibleInRoomDirectory = savedIsVisibleInRoomDirectory.value, + historyVisibility = roomInfo?.historyVisibility.map(), addressName = roomInfo?.firstDisplayableAlias(homeserverName)?.value ) } @@ -73,15 +80,12 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor( var editedHistoryVisibility by remember(savedSettings.historyVisibility) { mutableStateOf(savedSettings.historyVisibility) } - var editedVisibleInRoomDirectory by remember(savedSettings.isVisibleInRoomDirectory) { - mutableStateOf(savedSettings.isVisibleInRoomDirectory) - } var editedIsEncrypted by remember(savedSettings.isEncrypted) { mutableStateOf(savedSettings.isEncrypted) } - - var showEncryptionConfirmation by remember(savedSettings.isEncrypted) { mutableStateOf(false) } - + var editedVisibleInRoomDirectory by remember(savedIsVisibleInRoomDirectory.value) { + mutableStateOf(savedIsVisibleInRoomDirectory.value) + } val editedSettings = SecurityAndPrivacySettings( roomAccess = editedRoomAccess, isEncrypted = editedIsEncrypted, @@ -90,18 +94,24 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor( addressName = savedSettings.addressName, ) - val saveAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } + var showEncryptionConfirmation by remember(savedSettings.isEncrypted) { mutableStateOf(false) } + val permissions by room.securityAndPrivacyPermissionsAsState(syncUpdateFlow.value) fun handleEvents(event: SecurityAndPrivacyEvents) { when (event) { SecurityAndPrivacyEvents.Save -> { - coroutineScope.save(saveAction, savedSettings, editedSettings) + coroutineScope.save( + saveAction = saveAction, + isVisibleInRoomDirectory = savedIsVisibleInRoomDirectory, + savedSettings = savedSettings, + editedSettings = editedSettings + ) } is SecurityAndPrivacyEvents.ChangeRoomAccess -> { editedRoomAccess = event.roomAccess } is SecurityAndPrivacyEvents.ToggleEncryptionState -> { - if (editedSettings.isEncrypted) { + if (editedIsEncrypted) { editedIsEncrypted = false } else { showEncryptionConfirmation = true @@ -133,123 +143,137 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor( homeserverName = homeserverName, showEncryptionConfirmation = showEncryptionConfirmation, saveAction = saveAction.value, + permissions = permissions, eventSink = ::handleEvents ) + + // If the history visibility is not available for the current access, use the fallback. LaunchedEffect(state.availableHistoryVisibilities) { - editedSettings.historyVisibility?.also { - if (it !in state.availableHistoryVisibilities) { - editedHistoryVisibility = it.fallback() - } + if (editedSettings.historyVisibility !in state.availableHistoryVisibilities) { + editedHistoryVisibility = editedSettings.historyVisibility.fallback() } } return state } @Composable - private fun isRoomVisibleInRoomDirectory(): State> { - val result = remember { mutableStateOf>(AsyncData.Uninitialized) } + private fun IsRoomVisibleInRoomDirectoryEffect(isRoomVisible: MutableState>) { LaunchedEffect(Unit) { - result.runUpdatingState { + isRoomVisible.runUpdatingState { room.getRoomVisibility().map { it == RoomVisibility.Public } } } - return result } private fun CoroutineScope.save( saveAction: MutableState>, + isVisibleInRoomDirectory: MutableState>, savedSettings: SecurityAndPrivacySettings, editedSettings: SecurityAndPrivacySettings, ) = launch { suspend { - var somethingWentWrong = false - if (editedSettings.isEncrypted && !savedSettings.isEncrypted) { - room - .enableEncryption() - .onFailure { - Timber.d("Failed to enable encryption") - somethingWentWrong = true - } + val enableEncryption = async { + if (editedSettings.isEncrypted && !savedSettings.isEncrypted) { + room.enableEncryption() + } else { + Result.success(Unit) + } } - if (editedSettings.historyVisibility != null && editedSettings.historyVisibility != savedSettings.historyVisibility) { - room - .updateHistoryVisibility(editedSettings.historyVisibility.map()) - .onFailure { - Timber.d("Failed to update history visibility") - somethingWentWrong = true - } + val updateHistoryVisibility = async { + if (editedSettings.historyVisibility != savedSettings.historyVisibility) { + room.updateHistoryVisibility(editedSettings.historyVisibility.map()) + } else { + Result.success(Unit) + } } - if (editedSettings.roomAccess != savedSettings.roomAccess) { - room - .updateJoinRule(editedSettings.roomAccess.map()) - .onFailure { - Timber.d("Failed to update join rule") - somethingWentWrong = true - } + val updateJoinRule = async { + if (editedSettings.roomAccess != savedSettings.roomAccess) { + room.updateJoinRule(editedSettings.roomAccess.map()) + } else { + Result.success(Unit) + } } - - val editedIsVisibleInRoomDirectory = when (editedSettings.roomAccess) { - SecurityAndPrivacyRoomAccess.AskToJoin, - SecurityAndPrivacyRoomAccess.Anyone -> editedSettings.isVisibleInRoomDirectory.dataOrNull() - else -> false + val updateRoomVisibility = async { + // When a user changes join rules to something other than knock or public, + // the room should be automatically made invisible (private) in the room directory. + val editedIsVisibleInRoomDirectory = when (editedSettings.roomAccess) { + SecurityAndPrivacyRoomAccess.AskToJoin, + SecurityAndPrivacyRoomAccess.Anyone -> editedSettings.isVisibleInRoomDirectory.dataOrNull() + else -> false + } + val savedIsVisibleInRoomDirectory = savedSettings.isVisibleInRoomDirectory.dataOrNull() + if (editedIsVisibleInRoomDirectory != null && editedIsVisibleInRoomDirectory != savedIsVisibleInRoomDirectory) { + val roomVisibility = if (editedIsVisibleInRoomDirectory) RoomVisibility.Public else RoomVisibility.Private + room + .updateRoomVisibility(roomVisibility) + .onSuccess { + isVisibleInRoomDirectory.value = AsyncData.Success(editedIsVisibleInRoomDirectory) + } + } else { + Result.success(Unit) + } } - val savedIsVisibleInRoomDirectory = savedSettings.isVisibleInRoomDirectory.dataOrNull() - if (editedIsVisibleInRoomDirectory != null && editedIsVisibleInRoomDirectory != savedIsVisibleInRoomDirectory) { - val roomVisibility = if (editedIsVisibleInRoomDirectory) RoomVisibility.Public else RoomVisibility.Private - room - .updateRoomVisibility(roomVisibility) - .onFailure { - Timber.d("Failed to update room visibility") - somethingWentWrong = true - } + val artificialDelay = async { + // Artificial delay to make sure the user sees the loading state + delay(500) + Result.success(Unit) } - if (somethingWentWrong) { - error("") + val results = awaitAll( + enableEncryption, + updateHistoryVisibility, + updateJoinRule, + updateRoomVisibility, + artificialDelay + ) + if (results.any { it.isFailure }) { + throw SecurityAndPrivacyFailures.SaveFailed } }.runCatchingUpdatingState(saveAction) } +} - private fun JoinRule?.map(): SecurityAndPrivacyRoomAccess { - return when (this) { - JoinRule.Public -> SecurityAndPrivacyRoomAccess.Anyone - JoinRule.Knock, is JoinRule.KnockRestricted -> SecurityAndPrivacyRoomAccess.AskToJoin - is JoinRule.Restricted -> SecurityAndPrivacyRoomAccess.SpaceMember - is JoinRule.Custom, - JoinRule.Invite, - JoinRule.Private, - null -> SecurityAndPrivacyRoomAccess.InviteOnly - } +private fun JoinRule?.map(): SecurityAndPrivacyRoomAccess { + return when (this) { + JoinRule.Public -> SecurityAndPrivacyRoomAccess.Anyone + JoinRule.Knock, is JoinRule.KnockRestricted -> SecurityAndPrivacyRoomAccess.AskToJoin + is JoinRule.Restricted -> SecurityAndPrivacyRoomAccess.SpaceMember + is JoinRule.Custom, + JoinRule.Invite, + JoinRule.Private, + null -> SecurityAndPrivacyRoomAccess.InviteOnly } +} - private fun SecurityAndPrivacyRoomAccess.map(): JoinRule { - return when (this) { - SecurityAndPrivacyRoomAccess.Anyone -> JoinRule.Public - SecurityAndPrivacyRoomAccess.AskToJoin -> JoinRule.Knock - SecurityAndPrivacyRoomAccess.InviteOnly -> JoinRule.Private - SecurityAndPrivacyRoomAccess.SpaceMember -> error("Unsupported") - } +private fun SecurityAndPrivacyRoomAccess.map(): JoinRule { + return when (this) { + SecurityAndPrivacyRoomAccess.Anyone -> JoinRule.Public + SecurityAndPrivacyRoomAccess.AskToJoin -> JoinRule.Knock + SecurityAndPrivacyRoomAccess.InviteOnly -> JoinRule.Private + SecurityAndPrivacyRoomAccess.SpaceMember -> error("Unsupported") } +} - private fun RoomHistoryVisibility.map(): SecurityAndPrivacyHistoryVisibility { - return when (this) { - RoomHistoryVisibility.Joined, - RoomHistoryVisibility.Invited -> SecurityAndPrivacyHistoryVisibility.SinceInvite - RoomHistoryVisibility.Shared, - is RoomHistoryVisibility.Custom -> SecurityAndPrivacyHistoryVisibility.SinceSelection - RoomHistoryVisibility.WorldReadable -> SecurityAndPrivacyHistoryVisibility.Anyone - } - } +private fun RoomHistoryVisibility?.map(): SecurityAndPrivacyHistoryVisibility { + return when (this) { + RoomHistoryVisibility.WorldReadable -> SecurityAndPrivacyHistoryVisibility.Anyone + RoomHistoryVisibility.Joined, + RoomHistoryVisibility.Invited -> SecurityAndPrivacyHistoryVisibility.SinceInvite + RoomHistoryVisibility.Shared, + is RoomHistoryVisibility.Custom, + null -> SecurityAndPrivacyHistoryVisibility.SinceSelection - private fun SecurityAndPrivacyHistoryVisibility.map(): RoomHistoryVisibility { - return when (this) { - SecurityAndPrivacyHistoryVisibility.SinceSelection -> RoomHistoryVisibility.Shared - SecurityAndPrivacyHistoryVisibility.SinceInvite -> RoomHistoryVisibility.Invited - SecurityAndPrivacyHistoryVisibility.Anyone -> RoomHistoryVisibility.WorldReadable - } } +} - private fun MatrixRoomInfo.firstDisplayableAlias(serverName: String): RoomAlias? { - return aliases.firstOrNull { it.matchesServer(serverName) } ?: aliases.firstOrNull() +private fun SecurityAndPrivacyHistoryVisibility.map(): RoomHistoryVisibility { + return when (this) { + SecurityAndPrivacyHistoryVisibility.SinceSelection -> RoomHistoryVisibility.Shared + SecurityAndPrivacyHistoryVisibility.SinceInvite -> RoomHistoryVisibility.Invited + SecurityAndPrivacyHistoryVisibility.Anyone -> RoomHistoryVisibility.WorldReadable } } +private fun MatrixRoomInfo.firstDisplayableAlias(serverName: String): RoomAlias? { + return aliases.firstOrNull { it.matchesServer(serverName) } ?: aliases.firstOrNull() +} + diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt index 3209c71b521..6d3c835d9a4 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt @@ -7,6 +7,7 @@ package io.element.android.features.roomdetails.impl.securityandprivacy +import io.element.android.features.roomdetails.impl.securityandprivacy.permissions.SecurityAndPrivacyPermissions import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData @@ -18,12 +19,12 @@ data class SecurityAndPrivacyState( val homeserverName: String, val showEncryptionConfirmation: Boolean, val saveAction: AsyncAction, + private val permissions: SecurityAndPrivacyPermissions, val eventSink: (SecurityAndPrivacyEvents) -> Unit ) { val canBeSaved = savedSettings != editedSettings - val availableHistoryVisibilities = buildSet { add(SecurityAndPrivacyHistoryVisibility.SinceSelection) if (editedSettings.roomAccess == SecurityAndPrivacyRoomAccess.Anyone && !editedSettings.isEncrypted) { @@ -32,16 +33,17 @@ data class SecurityAndPrivacyState( add(SecurityAndPrivacyHistoryVisibility.SinceInvite) } } - val showRoomAccessSection: Boolean = true - val showRoomVisibilitySections = editedSettings.roomAccess != SecurityAndPrivacyRoomAccess.InviteOnly - val showHistoryVisibilitySection = editedSettings.historyVisibility != null - val showEncryptionSection = true + + val showRoomAccessSection = permissions.canChangeRoomAccess + val showRoomVisibilitySections = permissions.canChangeRoomVisibility && editedSettings.roomAccess != SecurityAndPrivacyRoomAccess.InviteOnly + val showHistoryVisibilitySection = permissions.canChangeHistoryVisibility + val showEncryptionSection = permissions.canChangeEncryption } data class SecurityAndPrivacySettings( val roomAccess: SecurityAndPrivacyRoomAccess, val isEncrypted: Boolean, - val historyVisibility: SecurityAndPrivacyHistoryVisibility?, + val historyVisibility: SecurityAndPrivacyHistoryVisibility, val addressName: String?, val isVisibleInRoomDirectory: AsyncData ) @@ -54,8 +56,8 @@ enum class SecurityAndPrivacyHistoryVisibility { */ fun fallback(): SecurityAndPrivacyHistoryVisibility { return when (this) { - SinceSelection -> SinceSelection - SinceInvite -> Anyone + SinceSelection, + SinceInvite -> SinceSelection Anyone -> SinceInvite } } @@ -64,3 +66,7 @@ enum class SecurityAndPrivacyHistoryVisibility { enum class SecurityAndPrivacyRoomAccess { InviteOnly, AskToJoin, Anyone, SpaceMember } + +sealed class SecurityAndPrivacyFailures : Exception() { + data object SaveFailed : SecurityAndPrivacyFailures() +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt index 8bf9b9757e3..d09bcf617f8 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt @@ -8,6 +8,7 @@ package io.element.android.features.roomdetails.impl.securityandprivacy import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.roomdetails.impl.securityandprivacy.permissions.SecurityAndPrivacyPermissions import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData @@ -27,7 +28,7 @@ open class SecurityAndPrivacyStateProvider : PreviewParameterProvider = AsyncData.Uninitialized, ) = SecurityAndPrivacySettings( roomAccess = roomAccess, @@ -67,6 +68,12 @@ fun aSecurityAndPrivacyState( homeserverName: String = "myserver.xyz", showEncryptionConfirmation: Boolean = false, saveAction: AsyncAction = AsyncAction.Uninitialized, + permissions: SecurityAndPrivacyPermissions = SecurityAndPrivacyPermissions( + canChangeRoomAccess = true, + canChangeHistoryVisibility = true, + canChangeEncryption = true, + canChangeRoomVisibility = true + ), eventSink: (SecurityAndPrivacyEvents) -> Unit = {} ) = SecurityAndPrivacyState( editedSettings = editedSettings, @@ -74,5 +81,6 @@ fun aSecurityAndPrivacyState( homeserverName = homeserverName, showEncryptionConfirmation = showEncryptionConfirmation, saveAction = saveAction, + permissions = permissions, eventSink = eventSink ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt index 552d80186aa..0b5054c3634 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt @@ -256,7 +256,6 @@ private fun RoomAddressSection( supportingContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_address_section_footer)) }, onClick = onRoomAddressClick, colors = ListItemDefaults.colors(trailingIconColor = ElementTheme.colors.iconAccentPrimary), - alwaysClickable = true ) ListItem( From ba0a85703b3bcfbe24114323bbace47d229a5ce5 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 24 Jan 2025 15:55:30 +0100 Subject: [PATCH 18/30] feat(security&privacy) : update strings --- .../SecurityAndPrivacyView.kt | 52 +++++++++---------- .../editroomaddress/EditRoomAddressView.kt | 7 +-- .../impl/src/main/res/values/localazy.xml | 36 +++++++++++++ .../src/main/res/values/localazy.xml | 35 ------------- tools/localazy/config.json | 4 +- 5 files changed, 69 insertions(+), 65 deletions(-) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt index 0b5054c3634..aa0321dc77f 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt @@ -184,31 +184,31 @@ private fun RoomAccessSection( modifier: Modifier = Modifier, ) { SecurityAndPrivacySection( - title = stringResource(CommonStrings.screen_security_and_privacy_room_access_section_header), + title = stringResource(R.string.screen_security_and_privacy_room_access_section_header), modifier = modifier, ) { ListItem( - headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_access_invite_only_option_title)) }, - supportingContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_access_invite_only_option_description)) }, + headlineContent = { Text(text = stringResource(R.string.screen_security_and_privacy_room_access_invite_only_option_title)) }, + supportingContent = { Text(text = stringResource(R.string.screen_security_and_privacy_room_access_invite_only_option_description)) }, trailingContent = ListItemContent.RadioButton(selected = edited == SecurityAndPrivacyRoomAccess.InviteOnly), onClick = { onSelected(SecurityAndPrivacyRoomAccess.InviteOnly) }, ) ListItem( - headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_ask_to_join_option_title)) }, - supportingContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_ask_to_join_option_description)) }, + 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 = { onSelected(SecurityAndPrivacyRoomAccess.AskToJoin) }, ) ListItem( - headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_access_anyone_option_title)) }, - supportingContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_access_anyone_option_description)) }, + headlineContent = { Text(text = stringResource(R.string.screen_security_and_privacy_room_access_anyone_option_title)) }, + supportingContent = { Text(text = stringResource(R.string.screen_security_and_privacy_room_access_anyone_option_description)) }, trailingContent = ListItemContent.RadioButton(selected = edited == SecurityAndPrivacyRoomAccess.Anyone), onClick = { onSelected(SecurityAndPrivacyRoomAccess.Anyone) }, ) if (saved == SecurityAndPrivacyRoomAccess.SpaceMember) { ListItem( - headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_access_space_members_option_title)) }, - supportingContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_access_space_members_option_description)) }, + 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_description)) }, trailingContent = ListItemContent.RadioButton(selected = true, enabled = false), enabled = false, ) @@ -222,12 +222,12 @@ private fun RoomVisibilitySection( modifier: Modifier = Modifier, ) { SecurityAndPrivacySection( - title = stringResource(CommonStrings.screen_security_and_privacy_room_visibility_section_header), + title = stringResource(R.string.screen_security_and_privacy_room_visibility_section_header), modifier = modifier, ) { Spacer(Modifier.height(12.dp)) Text( - text = stringResource(CommonStrings.screen_security_and_privacy_room_visibility_section_footer, homeserverName), + text = stringResource(R.string.screen_security_and_privacy_room_visibility_section_footer, homeserverName), style = ElementTheme.typography.fontBodyMdRegular, color = ElementTheme.colors.textSecondary, modifier = Modifier.padding(horizontal = 16.dp), @@ -245,23 +245,23 @@ private fun RoomAddressSection( modifier: Modifier = Modifier, ) { SecurityAndPrivacySection( - title = stringResource(CommonStrings.screen_security_and_privacy_room_address_section_header), + title = stringResource(R.string.screen_security_and_privacy_room_address_section_header), modifier = modifier, ) { ListItem( headlineContent = { - Text(text = roomAddress ?: stringResource(CommonStrings.screen_security_and_privacy_add_room_address_action)) + Text(text = roomAddress ?: stringResource(R.string.screen_security_and_privacy_add_room_address_action)) }, trailingContent = if (roomAddress.isNullOrEmpty()) ListItemContent.Icon(IconSource.Vector(CompoundIcons.Plus())) else null, - supportingContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_address_section_footer)) }, + supportingContent = { Text(text = stringResource(R.string.screen_security_and_privacy_room_address_section_footer)) }, onClick = onRoomAddressClick, colors = ListItemDefaults.colors(trailingIconColor = ElementTheme.colors.iconAccentPrimary), ) ListItem( - headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_directory_visibility_toggle_title)) }, + headlineContent = { Text(text = stringResource(R.string.screen_security_and_privacy_room_directory_visibility_toggle_title)) }, supportingContent = { - Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_directory_visibility_section_footer, homeserverName)) + Text(text = stringResource(R.string.screen_security_and_privacy_room_directory_visibility_section_footer, homeserverName)) }, trailingContent = when (isVisibleInRoomDirectory) { @@ -303,12 +303,12 @@ private fun EncryptionSection( modifier: Modifier = Modifier, ) { SecurityAndPrivacySection( - title = stringResource(CommonStrings.screen_security_and_privacy_encryption_section_header), + title = stringResource(R.string.screen_security_and_privacy_encryption_section_header), modifier = modifier, ) { ListItem( - headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_encryption_toggle_title)) }, - supportingContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_encryption_section_footer)) }, + headlineContent = { Text(text = stringResource(R.string.screen_security_and_privacy_encryption_toggle_title)) }, + supportingContent = { Text(text = stringResource(R.string.screen_security_and_privacy_encryption_section_footer)) }, trailingContent = ListItemContent.Switch( checked = isRoomEncrypted, enabled = canToggleEncryption, @@ -319,9 +319,9 @@ private fun EncryptionSection( } if (showConfirmation) { ConfirmationDialog( - title = stringResource(CommonStrings.screen_security_and_privacy_enable_encryption_alert_title), - content = stringResource(CommonStrings.screen_security_and_privacy_enable_encryption_alert_description), - submitText = stringResource(CommonStrings.screen_security_and_privacy_enable_encryption_alert_confirm_button_title), + title = stringResource(R.string.screen_security_and_privacy_enable_encryption_alert_title), + content = stringResource(R.string.screen_security_and_privacy_enable_encryption_alert_description), + submitText = stringResource(R.string.screen_security_and_privacy_enable_encryption_alert_confirm_button_title), onSubmitClick = onConfirmEncryption, onDismiss = onDismissConfirmation, ) @@ -337,7 +337,7 @@ private fun HistoryVisibilitySection( modifier: Modifier = Modifier, ) { SecurityAndPrivacySection( - title = stringResource(CommonStrings.screen_security_and_privacy_room_history_section_header), + title = stringResource(R.string.screen_security_and_privacy_room_history_section_header), modifier = modifier, ) { Spacer(Modifier.height(16.dp)) @@ -369,9 +369,9 @@ private fun HistoryVisibilityItem( isEnabled: Boolean = true, ) { val headlineText = when (option) { - SecurityAndPrivacyHistoryVisibility.SinceSelection -> stringResource(CommonStrings.screen_security_and_privacy_room_history_since_selecting_option_title) - SecurityAndPrivacyHistoryVisibility.SinceInvite -> stringResource(CommonStrings.screen_security_and_privacy_room_history_since_invite_option_title) - SecurityAndPrivacyHistoryVisibility.Anyone -> stringResource(CommonStrings.screen_security_and_privacy_room_history_anyone_option_title) + SecurityAndPrivacyHistoryVisibility.SinceSelection -> stringResource(R.string.screen_security_and_privacy_room_history_since_selecting_option_title) + SecurityAndPrivacyHistoryVisibility.SinceInvite -> stringResource(R.string.screen_security_and_privacy_room_history_since_invite_option_title) + SecurityAndPrivacyHistoryVisibility.Anyone -> stringResource(R.string.screen_security_and_privacy_room_history_anyone_option_title) } ListItem( headlineContent = { Text(text = headlineText) }, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressView.kt index 4c15feea3bf..efe224ba58b 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressView.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme +import io.element.android.features.roomdetails.impl.R import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults import io.element.android.libraries.designsystem.components.button.BackButton @@ -66,8 +67,8 @@ fun EditRoomAddressView( onAddressChange = { state.eventSink(EditRoomAddressEvents.RoomAddressChanged(it)) }, - label = stringResource(CommonStrings.screen_edit_room_address_title), - supportingText = stringResource(CommonStrings.screen_edit_room_address_room_address_section_footer), + label = stringResource(R.string.screen_edit_room_address_title), + supportingText = stringResource(R.string.screen_edit_room_address_room_address_section_footer), modifier = Modifier .fillMaxWidth() .padding(all = 16.dp) @@ -101,7 +102,7 @@ private fun EditRoomAddressTopBar( modifier = modifier, title = { Text( - text = stringResource(CommonStrings.screen_edit_room_address_title), + text = stringResource(R.string.screen_edit_room_address_title), style = ElementTheme.typography.aliasScreenTitle, ) }, diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml index bd602d3ed32..6d3a9a5c0b2 100644 --- a/features/roomdetails/impl/src/main/res/values/localazy.xml +++ b/features/roomdetails/impl/src/main/res/values/localazy.xml @@ -1,5 +1,7 @@ + "You’ll need a room address in order to make it visible in the directory." + "Room address" "An error occurred while updating the notification setting." "Your homeserver does not support this option in encrypted rooms, you may not get notified in some rooms." "Polls" @@ -54,6 +56,7 @@ "Default" "Notifications" "Pinned messages" + "Profile" "Requests to join" "Roles and permissions" "Room name" @@ -117,4 +120,37 @@ "Roles" "Room details" "Roles and permissions" + "Add room address" + "Anyone can ask to join the room but an administrator or moderator will have to accept the request." + "Ask to join" + "Yes, enable encryption" + "Once enabled, encryption for a room cannot be disabled, Message history will only be visible for room members since they were invited or since they joined the room. +No one besides the room members will be able to read messages. This may prevent bots and bridges to work correctly. +We do not recommend enabling encryption for rooms that anyone can find and join." + "Enable encryption?" + "Once enabled, encryption cannot be disabled." + "Encryption" + "Enable end-to-end encryption" + "Anyone can find and join" + "Anyone" + "People can only join if they are invited" + "Invite only" + "Room access" + "Spaces are not currently supported" + "Space members" + "You’ll need a room address in order to make it visible in the room directory." + "Room address" + "Allow for this room to be found by searching %1$s public room directory" + "Visible in public room directory" + "Anyone" + "Who can read history" + "Members only since they were invited" + "Members only since selecting this option" + "Room addresses are ways to find and access rooms. This also ensures you can easily share your room with others. +You can choose to publish your room in your homeserver public room directory." + "Room publishing" + "Room addresses are ways to find and access rooms. This also ensures you can easily share your room with others. +The address is also required to make the room visible in %1$s public room directory." + "Room visibility" + "Security & privacy" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 4a31e05852e..1d2f2e2d383 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -308,8 +308,6 @@ Reason: %1$s." "Hey, talk to me on %1$s: %2$s" "%1$s Android" "Rageshake to report bug" - "You’ll need a room address in order to make it visible in the directory." - "Room address" "Failed selecting media, please try again." "Captions might not be visible to people using older apps." "Failed processing media to upload, please try again." @@ -339,39 +337,6 @@ Reason: %1$s." "View All" "Chat" "Request to join sent" - "Add room address" - "Anyone can ask to join the room but an administrator or moderator will have to accept the request." - "Ask to join" - "Yes, enable encryption" - "Once enabled, encryption for a room cannot be disabled, Message history will only be visible for room members since they were invited or since they joined the room. -No one besides the room members will be able to read messages. This may prevent bots and bridges to work correctly. -We do not recommend enabling encryption for rooms that anyone can find and join." - "Enable encryption?" - "Once enabled, encryption cannot be disabled." - "Encryption" - "Enable end-to-end encryption" - "Anyone can find and join" - "Anyone" - "People can only join if they are invited" - "Invite only" - "Room access" - "Spaces are not currently supported" - "Space members" - "You’ll need a room address in order to make it visible in the room directory." - "Room address" - "Allow for this room to be found by searching %1$s public room directory" - "Visible in public room directory" - "Anyone" - "Who can read history" - "Members only since they were invited" - "Members only since selecting this option" - "Room addresses are ways to find and access rooms. This also ensures you can easily share your room with others. -You can choose to publish your room in your homeserver public room directory." - "Room publishing" - "Room addresses are ways to find and access rooms. This also ensures you can easily share your room with others. -The address is also required to make the room visible in %1$s public room directory." - "Room visibility" - "Security & privacy" "Share location" "Share my location" "Open in Apple Maps" diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 2efd8eac976..b75c0380ffe 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -185,7 +185,9 @@ "screen_polls_history_title", "screen_notification_settings_mentions_only_disclaimer", "screen_room_change_.*", - "screen_room_roles_.*" + "screen_room_roles_.*", + "screen\\.edit_room_address\\..*", + "screen\\.security_and_privacy\\..*" ] }, { From c07a7d926c38d87ac7af0fcd28b2a3a18bb0a31d Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 24 Jan 2025 16:27:24 +0100 Subject: [PATCH 19/30] feat(security&privacy) : make the whole RoomDirectoryVisibility item clickable --- .../impl/securityandprivacy/SecurityAndPrivacyEvents.kt | 2 +- .../securityandprivacy/SecurityAndPrivacyPresenter.kt | 7 +++++-- .../impl/securityandprivacy/SecurityAndPrivacyView.kt | 9 +++++---- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt index be076b9c523..352bccf4537 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt @@ -15,6 +15,6 @@ sealed interface SecurityAndPrivacyEvents { data object CancelEnableEncryption : SecurityAndPrivacyEvents data object ConfirmEnableEncryption: SecurityAndPrivacyEvents data class ChangeHistoryVisibility(val historyVisibility: SecurityAndPrivacyHistoryVisibility) : SecurityAndPrivacyEvents - data class ChangeRoomVisibility(val isVisibleInRoomDirectory: Boolean) : SecurityAndPrivacyEvents + data object ToggleRoomVisibility : SecurityAndPrivacyEvents data object DismissSaveError : SecurityAndPrivacyEvents } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt index fcd2946f85e..2450786dc12 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt @@ -120,8 +120,11 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor( is SecurityAndPrivacyEvents.ChangeHistoryVisibility -> { editedHistoryVisibility = event.historyVisibility } - is SecurityAndPrivacyEvents.ChangeRoomVisibility -> { - editedVisibleInRoomDirectory = AsyncData.Success(event.isVisibleInRoomDirectory) + SecurityAndPrivacyEvents.ToggleRoomVisibility -> { + editedVisibleInRoomDirectory = when (val edited = editedVisibleInRoomDirectory) { + is AsyncData.Success -> AsyncData.Success(!edited.data) + else -> edited + } } SecurityAndPrivacyEvents.EditRoomAddress -> navigator.openEditRoomAddress() SecurityAndPrivacyEvents.CancelEnableEncryption -> { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt index aa0321dc77f..10d2a7d546c 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt @@ -91,8 +91,8 @@ fun SecurityAndPrivacyView( homeserverName = state.homeserverName, onRoomAddressClick = { state.eventSink(SecurityAndPrivacyEvents.EditRoomAddress) }, isVisibleInRoomDirectory = state.editedSettings.isVisibleInRoomDirectory, - onVisibilityChange = { isVisible -> - state.eventSink(SecurityAndPrivacyEvents.ChangeRoomVisibility(isVisible)) + onVisibilityChange = { + state.eventSink(SecurityAndPrivacyEvents.ToggleRoomVisibility) }, ) } @@ -241,7 +241,7 @@ private fun RoomAddressSection( homeserverName: String, isVisibleInRoomDirectory: AsyncData, onRoomAddressClick: () -> Unit, - onVisibilityChange: (Boolean) -> Unit, + onVisibilityChange: () -> Unit, modifier: Modifier = Modifier, ) { SecurityAndPrivacySection( @@ -263,6 +263,7 @@ private fun RoomAddressSection( supportingContent = { Text(text = stringResource(R.string.screen_security_and_privacy_room_directory_visibility_section_footer, homeserverName)) }, + onClick = if (isVisibleInRoomDirectory.isSuccess()) onVisibilityChange else null, trailingContent = when (isVisibleInRoomDirectory) { is AsyncData.Uninitialized, is AsyncData.Loading -> { @@ -284,7 +285,7 @@ private fun RoomAddressSection( is AsyncData.Success -> { ListItemContent.Switch( checked = isVisibleInRoomDirectory.data, - onChange = onVisibilityChange, + onChange = { onVisibilityChange() }, ) } } From fdc4f1b0fe1bc8d5de90d6df6eebf14917b0355c Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 27 Jan 2025 16:36:53 +0100 Subject: [PATCH 20/30] feat(security&privacy) : start writing tests --- .../SecurityAndPrivacyPresenter.kt | 4 +- .../SecurityAndPrivacyState.kt | 7 +- .../SecurityAndPrivacyStateProvider.kt | 2 +- .../SecurityAndPrivacyView.kt | 2 +- .../roomdetails/impl/RoomDetailsViewTest.kt | 17 + .../FakeSecurityAndPrivacyNavigator.kt | 24 ++ .../SecurityAndPrivacyPresenterTest.kt | 335 ++++++++++++++++++ .../SecurityAndPrivacyPresenterViewTest.kt | 55 +++ 8 files changed, 441 insertions(+), 5 deletions(-) create mode 100644 features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/FakeSecurityAndPrivacyNavigator.kt create mode 100644 features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/SecurityAndPrivacyPresenterTest.kt create mode 100644 features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/SecurityAndPrivacyPresenterViewTest.kt diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt index 2450786dc12..0fa653c4623 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt @@ -69,7 +69,7 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor( isEncrypted = room.isEncrypted, isVisibleInRoomDirectory = savedIsVisibleInRoomDirectory.value, historyVisibility = roomInfo?.historyVisibility.map(), - addressName = roomInfo?.firstDisplayableAlias(homeserverName)?.value + address = roomInfo?.firstDisplayableAlias(homeserverName)?.value ) } } @@ -91,7 +91,7 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor( isEncrypted = editedIsEncrypted, isVisibleInRoomDirectory = editedVisibleInRoomDirectory, historyVisibility = editedHistoryVisibility, - addressName = savedSettings.addressName, + address = savedSettings.address, ) var showEncryptionConfirmation by remember(savedSettings.isEncrypted) { mutableStateOf(false) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt index 6d3c835d9a4..3d2fc250e21 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt @@ -23,6 +23,8 @@ data class SecurityAndPrivacyState( val eventSink: (SecurityAndPrivacyEvents) -> Unit ) { + + val canBeSaved = savedSettings != editedSettings val availableHistoryVisibilities = buildSet { @@ -38,13 +40,16 @@ data class SecurityAndPrivacyState( val showRoomVisibilitySections = permissions.canChangeRoomVisibility && editedSettings.roomAccess != SecurityAndPrivacyRoomAccess.InviteOnly val showHistoryVisibilitySection = permissions.canChangeHistoryVisibility val showEncryptionSection = permissions.canChangeEncryption + override fun toString(): String { + return "SecurityAndPrivacyState(savedSettings=$savedSettings, editedSettings=$editedSettings, homeserverName='$homeserverName', showEncryptionConfirmation=$showEncryptionConfirmation, saveAction=$saveAction, canBeSaved=$canBeSaved)" + } } data class SecurityAndPrivacySettings( val roomAccess: SecurityAndPrivacyRoomAccess, val isEncrypted: Boolean, val historyVisibility: SecurityAndPrivacyHistoryVisibility, - val addressName: String?, + val address: String?, val isVisibleInRoomDirectory: AsyncData ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt index d09bcf617f8..78dba62ca19 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt @@ -57,7 +57,7 @@ fun aSecurityAndPrivacySettings( ) = SecurityAndPrivacySettings( roomAccess = roomAccess, isEncrypted = isEncrypted, - addressName = formattedAddress, + address = formattedAddress, historyVisibility = historyVisibility, isVisibleInRoomDirectory = isVisibleInRoomDirectory ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt index 10d2a7d546c..24fdf8bc2a4 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt @@ -87,7 +87,7 @@ fun SecurityAndPrivacyView( if (state.showRoomVisibilitySections) { RoomVisibilitySection(state.homeserverName) RoomAddressSection( - roomAddress = state.editedSettings.addressName, + roomAddress = state.editedSettings.address, homeserverName = state.homeserverName, onRoomAddressClick = { state.eventSink(SecurityAndPrivacyEvents.EditRoomAddress) }, isVisibleInRoomDirectory = state.editedSettings.isVisibleInRoomDirectory, diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt index b6209464895..8cc1af16385 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt @@ -144,6 +144,21 @@ class RoomDetailsViewTest { } } + @Config(qualifiers = "h1024dp") + @Test + fun `click on security and privacy invokes expected callback`() { + ensureCalledOnce { callback -> + rule.setRoomDetailView( + state = aRoomDetailsState( + eventSink = EventsRecorder(expectEvents = false), + canShowSecurityAndPrivacy = true, + ), + onSecurityAndPrivacyClick = callback, + ) + rule.clickOn(R.string.screen_room_details_security_and_privacy_title) + } + } + @Config(qualifiers = "h1024dp") @Test fun `click on add topic emit expected event`() { @@ -298,6 +313,7 @@ private fun AndroidComposeTestRule.setRoomD onJoinCallClick: () -> Unit = EnsureNeverCalled(), onPinnedMessagesClick: () -> Unit = EnsureNeverCalled(), onKnockRequestsClick: () -> Unit = EnsureNeverCalled(), + onSecurityAndPrivacyClick: () -> Unit = EnsureNeverCalled(), ) { setContent { RoomDetailsView( @@ -315,6 +331,7 @@ private fun AndroidComposeTestRule.setRoomD onJoinCallClick = onJoinCallClick, onPinnedMessagesClick = onPinnedMessagesClick, onKnockRequestsClick = onKnockRequestsClick, + onSecurityAndPrivacyClick = onSecurityAndPrivacyClick, ) } } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/FakeSecurityAndPrivacyNavigator.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/FakeSecurityAndPrivacyNavigator.kt new file mode 100644 index 00000000000..e6c91096f36 --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/FakeSecurityAndPrivacyNavigator.kt @@ -0,0 +1,24 @@ +/* + * 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.roomdetails.securityandprivacy + +import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyNavigator +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeSecurityAndPrivacyNavigator( + private val openEditRoomAddressLambda: () -> Unit = { lambdaError() }, + private val closeEditorRoomAddressLambda: () -> Unit = { lambdaError() }, +) : SecurityAndPrivacyNavigator { + override fun openEditRoomAddress() { + openEditRoomAddressLambda() + } + + override fun closeEditorRoomAddress() { + closeEditorRoomAddressLambda() + } +} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/SecurityAndPrivacyPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/SecurityAndPrivacyPresenterTest.kt new file mode 100644 index 00000000000..aca3e97c541 --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/SecurityAndPrivacyPresenterTest.kt @@ -0,0 +1,335 @@ +/* + * 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.roomdetails.securityandprivacy + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyEvents +import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyHistoryVisibility +import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyNavigator +import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyPresenter +import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyRoomAccess +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.room.MatrixRoom +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.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.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.tests.testutils.lambda.assert +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(showEncryptionConfirmation).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(showEncryptionConfirmation).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 = FakeMatrixRoom( + canSendStateResult = { _, _ -> Result.success(true) }, + ) + val presenter = createSecurityAndPrivacyPresenter(room = room) + presenter.test { + skipItems(2) + room.givenRoomInfo( + aRoomInfo( + joinRule = JoinRule.Public, + historyVisibility = RoomHistoryVisibility.WorldReadable, + canonicalAlias = A_ROOM_ALIAS, + ) + ) + with(awaitItem()) { + assertThat(editedSettings).isEqualTo(savedSettings) + assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.Anyone) + assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.Anyone) + 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(SecurityAndPrivacyEvents.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone)) + } + with(awaitItem()) { + assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.Anyone) + assertThat(showRoomVisibilitySections).isTrue() + assertThat(canBeSaved).isTrue() + eventSink(SecurityAndPrivacyEvents.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.SinceSelection) + eventSink(SecurityAndPrivacyEvents.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.SinceInvite)) + } + with(awaitItem()) { + assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.SinceInvite) + assertThat(canBeSaved).isTrue() + eventSink(SecurityAndPrivacyEvents.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.SinceSelection)) + } + with(awaitItem()) { + assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.SinceSelection) + assertThat(canBeSaved).isFalse() + } + } + } + + @Test + fun `present - enable encryption`() = runTest { + val presenter = createSecurityAndPrivacyPresenter() + presenter.test { + skipItems(1) + with(awaitItem()) { + assertThat(editedSettings.isEncrypted).isFalse() + eventSink(SecurityAndPrivacyEvents.ToggleEncryptionState) + } + with(awaitItem()) { + assertThat(showEncryptionConfirmation).isTrue() + eventSink(SecurityAndPrivacyEvents.CancelEnableEncryption) + } + with(awaitItem()) { + assertThat(showEncryptionConfirmation).isFalse() + eventSink(SecurityAndPrivacyEvents.ToggleEncryptionState) + } + with(awaitItem()) { + assertThat(showEncryptionConfirmation).isTrue() + eventSink(SecurityAndPrivacyEvents.ConfirmEnableEncryption) + } + skipItems(1) + with(awaitItem()) { + assertThat(editedSettings.isEncrypted).isTrue() + assertThat(showEncryptionConfirmation).isFalse() + assertThat(canBeSaved).isTrue() + eventSink(SecurityAndPrivacyEvents.ToggleEncryptionState) + } + with(awaitItem()) { + assertThat(editedSettings.isEncrypted).isFalse() + assertThat(canBeSaved).isFalse() + } + } + } + + @Test + fun `present - room visibility loading and change`() = runTest { + val room = FakeMatrixRoom( + canSendStateResult = { _, _ -> Result.success(true) }, + roomVisibilityResult = { Result.success(RoomVisibility.Private) } + ) + 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(SecurityAndPrivacyEvents.ToggleRoomVisibility) + } + with(awaitItem()) { + assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(true)) + assertThat(canBeSaved).isTrue() + eventSink(SecurityAndPrivacyEvents.ToggleRoomVisibility) + } + with(awaitItem()) { + assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(false)) + assertThat(canBeSaved).isFalse() + } + } + } + + @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 = FakeMatrixRoom( + canSendStateResult = { _, _ -> Result.success(true) }, + enableEncryptionResult = enableEncryptionLambda, + updateJoinRuleResult = updateJoinRuleLambda, + updateRoomVisibilityResult = updateRoomVisibilityLambda, + updateRoomHistoryVisibilityResult = updateRoomHistoryVisibilityLambda, + roomVisibilityResult = { Result.success(RoomVisibility.Private) } + ) + val presenter = createSecurityAndPrivacyPresenter(room = room) + presenter.test { + skipItems(2) + with(awaitItem()) { + assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly) + eventSink(SecurityAndPrivacyEvents.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone)) + } + with(awaitItem()) { + eventSink(SecurityAndPrivacyEvents.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.Anyone)) + } + with(awaitItem()) { + assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.Anyone) + eventSink(SecurityAndPrivacyEvents.ConfirmEnableEncryption) + } + skipItems(1) + with(awaitItem()) { + assertThat(editedSettings.isEncrypted).isTrue() + eventSink(SecurityAndPrivacyEvents.ToggleRoomVisibility) + } + with(awaitItem()) { + assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(true)) + eventSink(SecurityAndPrivacyEvents.Save) + } + with(awaitItem()) { + assertThat(saveAction).isEqualTo(AsyncAction.Loading) + } + + room.givenRoomInfo( + aRoomInfo( + joinRule = JoinRule.Public, + historyVisibility = RoomHistoryVisibility.WorldReadable, + ) + ) + // Saved settings are updated 3 times to match the edited settings + skipItems(3) + 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() + } + } + + @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 = FakeMatrixRoom( + canSendStateResult = { _, _ -> Result.success(true) }, + enableEncryptionResult = enableEncryptionLambda, + updateJoinRuleResult = updateJoinRuleLambda, + updateRoomVisibilityResult = updateRoomVisibilityLambda, + updateRoomHistoryVisibilityResult = updateRoomHistoryVisibilityLambda, + roomVisibilityResult = { Result.success(RoomVisibility.Private) } + ) + val presenter = createSecurityAndPrivacyPresenter(room = room) + presenter.test { + skipItems(2) + with(awaitItem()) { + assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly) + eventSink(SecurityAndPrivacyEvents.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone)) + } + with(awaitItem()) { + eventSink(SecurityAndPrivacyEvents.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.Anyone)) + } + with(awaitItem()) { + assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.Anyone) + eventSink(SecurityAndPrivacyEvents.ConfirmEnableEncryption) + } + skipItems(1) + with(awaitItem()) { + assertThat(editedSettings.isEncrypted).isTrue() + eventSink(SecurityAndPrivacyEvents.ToggleRoomVisibility) + } + with(awaitItem()) { + assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(true)) + eventSink(SecurityAndPrivacyEvents.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) + with(awaitItem()) { + 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() + } + } + + private fun createSecurityAndPrivacyPresenter( + serverName: String = "matrix.org", + room: MatrixRoom = FakeMatrixRoom( + canSendStateResult = { _, _ -> Result.success(true) }, + roomVisibilityResult = { Result.success(RoomVisibility.Private) } + ), + navigator: SecurityAndPrivacyNavigator = FakeSecurityAndPrivacyNavigator(), + ): SecurityAndPrivacyPresenter { + return SecurityAndPrivacyPresenter( + room = room, + matrixClient = FakeMatrixClient( + userIdServerNameLambda = { serverName }, + ), + navigator = navigator + ) + } +} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/SecurityAndPrivacyPresenterViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/SecurityAndPrivacyPresenterViewTest.kt new file mode 100644 index 00000000000..66b05bf01b9 --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/SecurityAndPrivacyPresenterViewTest.kt @@ -0,0 +1,55 @@ +/* + * 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.roomdetails.securityandprivacy + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyState +import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyView +import io.element.android.features.roomdetails.impl.securityandprivacy.aSecurityAndPrivacyState +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.pressBack +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SecurityAndPrivacyPresenterViewTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `click on back invokes expected callback`() { + ensureCalledOnce { callback -> + rule.setSecurityAndPrivacyView( + onBackClick = callback, + ) + rule.pressBack() + } + } + +} + + +private fun AndroidComposeTestRule.setSecurityAndPrivacyView( + state: SecurityAndPrivacyState = aSecurityAndPrivacyState( + eventSink = EventsRecorder(expectEvents = false), + ), + onBackClick: () -> Unit = EnsureNeverCalled(), +) { + setContent { + SecurityAndPrivacyView( + state = state, + onBackClick = onBackClick, + ) + } +} From c0c7d5b659d1019d92246890fc722804728c6486 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 27 Jan 2025 21:39:25 +0100 Subject: [PATCH 21/30] feat(security&privacy) : write View tests --- .../SecurityAndPrivacyStateProvider.kt | 4 +- .../SecurityAndPrivacyPresenterViewTest.kt | 55 ------ .../SecurityAndPrivacyViewTest.kt | 171 ++++++++++++++++++ 3 files changed, 173 insertions(+), 57 deletions(-) delete mode 100644 features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/SecurityAndPrivacyPresenterViewTest.kt create mode 100644 features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/SecurityAndPrivacyViewTest.kt diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt index 78dba62ca19..eb77b89ecad 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt @@ -51,13 +51,13 @@ open class SecurityAndPrivacyStateProvider : PreviewParameterProvider = AsyncData.Uninitialized, ) = SecurityAndPrivacySettings( roomAccess = roomAccess, isEncrypted = isEncrypted, - address = formattedAddress, + address = address, historyVisibility = historyVisibility, isVisibleInRoomDirectory = isVisibleInRoomDirectory ) diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/SecurityAndPrivacyPresenterViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/SecurityAndPrivacyPresenterViewTest.kt deleted file mode 100644 index 66b05bf01b9..00000000000 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/SecurityAndPrivacyPresenterViewTest.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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.roomdetails.securityandprivacy - -import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyState -import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyView -import io.element.android.features.roomdetails.impl.securityandprivacy.aSecurityAndPrivacyState -import io.element.android.tests.testutils.EnsureNeverCalled -import io.element.android.tests.testutils.EventsRecorder -import io.element.android.tests.testutils.ensureCalledOnce -import io.element.android.tests.testutils.pressBack -import org.junit.Rule -import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class SecurityAndPrivacyPresenterViewTest { - @get:Rule val rule = createAndroidComposeRule() - - @Test - fun `click on back invokes expected callback`() { - ensureCalledOnce { callback -> - rule.setSecurityAndPrivacyView( - onBackClick = callback, - ) - rule.pressBack() - } - } - -} - - -private fun AndroidComposeTestRule.setSecurityAndPrivacyView( - state: SecurityAndPrivacyState = aSecurityAndPrivacyState( - eventSink = EventsRecorder(expectEvents = false), - ), - onBackClick: () -> Unit = EnsureNeverCalled(), -) { - setContent { - SecurityAndPrivacyView( - state = state, - onBackClick = onBackClick, - ) - } -} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/SecurityAndPrivacyViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/SecurityAndPrivacyViewTest.kt new file mode 100644 index 00000000000..558fbcc4278 --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/SecurityAndPrivacyViewTest.kt @@ -0,0 +1,171 @@ +/* + * 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.roomdetails.securityandprivacy + +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.features.roomdetails.impl.R +import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyEvents +import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyHistoryVisibility +import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyRoomAccess +import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyState +import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyView +import io.element.android.features.roomdetails.impl.securityandprivacy.aSecurityAndPrivacySettings +import io.element.android.features.roomdetails.impl.securityandprivacy.aSecurityAndPrivacyState +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.pressBack +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +class SecurityAndPrivacyViewTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `click on back invokes expected callback`() { + ensureCalledOnce { callback -> + rule.setSecurityAndPrivacyView( + onBackClick = callback, + ) + rule.pressBack() + } + } + + @Test + fun `click on room access item emits the expected event`() { + val recorder = EventsRecorder() + val state = aSecurityAndPrivacyState( + eventSink = recorder, + ) + rule.setSecurityAndPrivacyView(state) + rule.clickOn(R.string.screen_security_and_privacy_room_access_invite_only_option_title) + recorder.assertSingle(SecurityAndPrivacyEvents.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.InviteOnly)) + } + + @Test + fun `click on disabled save doesn't emit event`() { + val recorder = EventsRecorder(expectEvents = false) + val state = aSecurityAndPrivacyState(eventSink = recorder) + rule.setSecurityAndPrivacyView(state) + rule.clickOn(CommonStrings.action_save) + recorder.assertEmpty() + } + + @Test + fun `click on enabled save emits the expected event`() { + val recorder = EventsRecorder() + val state = aSecurityAndPrivacyState( + eventSink = recorder, + editedSettings = aSecurityAndPrivacySettings( + roomAccess = SecurityAndPrivacyRoomAccess.Anyone, + ) + ) + rule.setSecurityAndPrivacyView(state) + rule.clickOn(CommonStrings.action_save) + recorder.assertSingle(SecurityAndPrivacyEvents.Save) + } + + @Test + @Config(qualifiers = "h640dp") + fun `click on room address item emits the expected event`() { + val address = "@alias:matrix.org" + val recorder = EventsRecorder() + val state = aSecurityAndPrivacyState( + eventSink = recorder, + editedSettings = aSecurityAndPrivacySettings( + address = address, + roomAccess = SecurityAndPrivacyRoomAccess.Anyone, + ), + ) + rule.setSecurityAndPrivacyView(state) + rule.onNodeWithText(address).performClick() + recorder.assertSingle(SecurityAndPrivacyEvents.EditRoomAddress) + } + + @Test + @Config(qualifiers = "h640dp") + fun `click on room visibility item emits the expected event`() { + val recorder = EventsRecorder() + val state = aSecurityAndPrivacyState( + eventSink = recorder, + editedSettings = aSecurityAndPrivacySettings( + roomAccess = SecurityAndPrivacyRoomAccess.Anyone, + isVisibleInRoomDirectory = AsyncData.Success(false), + ), + ) + rule.setSecurityAndPrivacyView(state) + rule.clickOn(R.string.screen_security_and_privacy_room_directory_visibility_toggle_title) + recorder.assertSingle(SecurityAndPrivacyEvents.ToggleRoomVisibility) + } + + @Test + @Config(qualifiers = "h640dp") + fun `click on history visibility item emits the expected event`() { + val recorder = EventsRecorder() + val state = aSecurityAndPrivacyState( + eventSink = recorder, + editedSettings = aSecurityAndPrivacySettings( + historyVisibility = SecurityAndPrivacyHistoryVisibility.SinceSelection, + ), + ) + rule.setSecurityAndPrivacyView(state) + rule.clickOn(R.string.screen_security_and_privacy_room_history_since_selecting_option_title) + recorder.assertSingle(SecurityAndPrivacyEvents.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.SinceSelection)) + } + + @Test + @Config(qualifiers = "h640dp") + fun `click on encryption item emits the expected event`() { + val recorder = EventsRecorder() + val state = aSecurityAndPrivacyState( + eventSink = recorder, + savedSettings = aSecurityAndPrivacySettings(isEncrypted = false), + ) + rule.setSecurityAndPrivacyView(state) + rule.clickOn(R.string.screen_security_and_privacy_encryption_toggle_title) + recorder.assertSingle(SecurityAndPrivacyEvents.ToggleEncryptionState) + } + + @Test + fun `click on encryption confirm emits the expected event`() { + val recorder = EventsRecorder() + val state = aSecurityAndPrivacyState( + eventSink = recorder, + showEncryptionConfirmation = true, + ) + rule.setSecurityAndPrivacyView(state) + rule.clickOn(R.string.screen_security_and_privacy_enable_encryption_alert_confirm_button_title) + recorder.assertSingle(SecurityAndPrivacyEvents.ConfirmEnableEncryption) + } +} + +private fun AndroidComposeTestRule.setSecurityAndPrivacyView( + state: SecurityAndPrivacyState = aSecurityAndPrivacyState( + eventSink = EventsRecorder(expectEvents = false), + ), + onBackClick: () -> Unit = EnsureNeverCalled(), +) { + setContent { + SecurityAndPrivacyView( + state = state, + onBackClick = onBackClick, + ) + } +} From 876d06f5108fefbc6e5ef62a928b34d1270cd542 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 27 Jan 2025 22:43:10 +0100 Subject: [PATCH 22/30] feat(security&privacy) : add all tests for EditRoomAddress classes --- .../ConfigureRoomPresenterTest.kt | 1 + .../SecurityAndPrivacyNavigator.kt | 4 +- .../EditRoomAddressPresenter.kt | 2 +- .../EditRoomAddressStateProvider.kt | 12 +- .../FakeSecurityAndPrivacyNavigator.kt | 6 +- .../SecurityAndPrivacyPresenterTest.kt | 14 + .../EditRoomAddressPresenterTest.kt | 355 ++++++++++++++++++ .../EditRoomAddressViewTest.kt | 123 ++++++ .../ui/room/address/RoomAddressField.kt | 4 +- .../android/libraries/testtags/TestTags.kt | 7 + 10 files changed, 515 insertions(+), 13 deletions(-) create mode 100644 features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/editroomaddress/EditRoomAddressPresenterTest.kt create mode 100644 features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/editroomaddress/EditRoomAddressViewTest.kt diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTest.kt index 714c375d03f..60698b8e981 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTest.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTest.kt @@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.alias.FakeRoomAliasHelper import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.media.AvatarAction +import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity import io.element.android.libraries.mediapickers.api.PickerProvider import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediaupload.api.MediaPreProcessor diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNavigator.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNavigator.kt index 8b6fb3312e8..ade6479fd9a 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNavigator.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNavigator.kt @@ -14,7 +14,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push interface SecurityAndPrivacyNavigator : Plugin { fun openEditRoomAddress() - fun closeEditorRoomAddress() + fun closeEditRoomAddress() } class BackstackSecurityAndPrivacyNavigator( @@ -24,7 +24,7 @@ class BackstackSecurityAndPrivacyNavigator( backStack.push(SecurityAndPrivacyFlowNode.NavTarget.EditRoomAddress) } - override fun closeEditorRoomAddress() { + override fun closeEditRoomAddress() { backStack.pop() } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt index 3402e2f174f..884034ca1e6 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt @@ -128,7 +128,7 @@ class EditRoomAddressPresenter @AssistedInject constructor( room.updateCanonicalAlias(savedCanonicalAlias, newAlternativeAliases).getOrThrow() } } - navigator.closeEditorRoomAddress() + navigator.closeEditRoomAddress() }.runCatchingUpdatingState(saveAction) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressStateProvider.kt index 733e9cb8198..6ef8658bcdc 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressStateProvider.kt @@ -14,15 +14,15 @@ import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity open class EditRoomAddressStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - aEditRoomAddressState(), - aEditRoomAddressState(roomAddressValidity = RoomAddressValidity.NotAvailable), - aEditRoomAddressState(roomAddressValidity = RoomAddressValidity.InvalidSymbols), - aEditRoomAddressState(roomAddressValidity = RoomAddressValidity.Valid), - aEditRoomAddressState(roomAddressValidity = RoomAddressValidity.Valid, saveAction = AsyncAction.Loading), + anEditRoomAddressState(), + anEditRoomAddressState(roomAddressValidity = RoomAddressValidity.NotAvailable), + anEditRoomAddressState(roomAddressValidity = RoomAddressValidity.InvalidSymbols), + anEditRoomAddressState(roomAddressValidity = RoomAddressValidity.Valid), + anEditRoomAddressState(roomAddressValidity = RoomAddressValidity.Valid, saveAction = AsyncAction.Loading), ) } -fun aEditRoomAddressState( +fun anEditRoomAddressState( roomAddress: String = "therapy", roomAddressValidity: RoomAddressValidity = RoomAddressValidity.Unknown, homeserverName: String = ":myserver.org", diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/FakeSecurityAndPrivacyNavigator.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/FakeSecurityAndPrivacyNavigator.kt index e6c91096f36..e4dfbcdbbad 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/FakeSecurityAndPrivacyNavigator.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/FakeSecurityAndPrivacyNavigator.kt @@ -12,13 +12,13 @@ import io.element.android.tests.testutils.lambda.lambdaError class FakeSecurityAndPrivacyNavigator( private val openEditRoomAddressLambda: () -> Unit = { lambdaError() }, - private val closeEditorRoomAddressLambda: () -> Unit = { lambdaError() }, + private val closeEditRoomAddressLambda: () -> Unit = { lambdaError() }, ) : SecurityAndPrivacyNavigator { override fun openEditRoomAddress() { openEditRoomAddressLambda() } - override fun closeEditorRoomAddress() { - closeEditorRoomAddressLambda() + override fun closeEditRoomAddress() { + closeEditRoomAddressLambda() } } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/SecurityAndPrivacyPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/SecurityAndPrivacyPresenterTest.kt index aca3e97c541..c46cdc9d9cb 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/SecurityAndPrivacyPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/SecurityAndPrivacyPresenterTest.kt @@ -192,6 +192,20 @@ class SecurityAndPrivacyPresenterTest { } } + @Test + fun `present - edit room address`() = runTest { + val openEditRoomAddressLambda = lambdaRecorder { } + val navigator = FakeSecurityAndPrivacyNavigator(openEditRoomAddressLambda) + val presenter = createSecurityAndPrivacyPresenter(navigator = navigator) + presenter.test { + skipItems(1) + with(awaitItem()) { + eventSink(SecurityAndPrivacyEvents.EditRoomAddress) + } + assert(openEditRoomAddressLambda).isCalledOnce() + } + } + @Test fun `present - save success`() = runTest { val enableEncryptionLambda = lambdaRecorder> { Result.success(Unit) } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/editroomaddress/EditRoomAddressPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/editroomaddress/EditRoomAddressPresenterTest.kt new file mode 100644 index 00000000000..0210ddac82c --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/editroomaddress/EditRoomAddressPresenterTest.kt @@ -0,0 +1,355 @@ +/* + * 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.roomdetails.securityandprivacy.editroomaddress + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyNavigator +import io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress.EditRoomAddressEvents +import io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress.EditRoomAddressPresenter +import io.element.android.features.roomdetails.securityandprivacy.FakeSecurityAndPrivacyNavigator +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.core.RoomAlias +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias +import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper +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.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.alias.FakeRoomAliasHelper +import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity +import io.element.android.tests.testutils.lambda.assert +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.coroutines.test.runTest +import org.junit.Test +import java.util.Optional + +class EditRoomAddressPresenterTest { + + @Test + fun `present - initial state no address`() = runTest { + val presenter = createEditRoomAddressPresenter() + presenter.test { + with(awaitItem()) { + assertThat(homeserverName).isEqualTo("matrix.org") + assertThat(canBeSaved).isFalse() + assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized) + assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Unknown) + assertThat(roomAddress).isEmpty() + } + } + } + + @Test + fun `present - initial state address matching own homeserver`() = runTest { + val room = FakeMatrixRoom( + canonicalAlias = RoomAlias("#canonical:matrix.org"), + ) + val presenter = createEditRoomAddressPresenter(room = room) + presenter.test { + with(awaitItem()) { + assertThat(homeserverName).isEqualTo("matrix.org") + assertThat(canBeSaved).isFalse() + assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized) + assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Unknown) + assertThat(roomAddress).isEqualTo("canonical") + } + } + } + + @Test + fun `present - initial state address not matching own homeserver`() = runTest { + val room = FakeMatrixRoom( + canonicalAlias = RoomAlias("#canonical:notmatrix.org"), + ) + val presenter = createEditRoomAddressPresenter(room = room) + presenter.test { + with(awaitItem()) { + assertThat(homeserverName).isEqualTo("matrix.org") + assertThat(canBeSaved).isFalse() + assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized) + assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Unknown) + assertThat(roomAddress).isEmpty() + } + } + } + + @Test + fun `present - room address change invalid state`() = runTest { + val roomAliasHelper = FakeRoomAliasHelper( + isRoomAliasValidLambda = { false } + ) + val presenter = createEditRoomAddressPresenter(roomAliasHelper = roomAliasHelper) + presenter.test { + with(awaitItem()) { + eventSink(EditRoomAddressEvents.RoomAddressChanged("invalid")) + } + with(awaitItem()) { + assertThat(roomAddress).isEqualTo("invalid") + assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Unknown) + } + with(awaitItem()) { + assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.InvalidSymbols) + assertThat(canBeSaved).isFalse() + } + } + } + + @Test + fun `present - room address change valid state`() = runTest { + val presenter = createEditRoomAddressPresenter() + presenter.test { + with(awaitItem()) { + eventSink(EditRoomAddressEvents.RoomAddressChanged("valid")) + } + with(awaitItem()) { + assertThat(roomAddress).isEqualTo("valid") + assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Unknown) + } + with(awaitItem()) { + assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Valid) + assertThat(canBeSaved).isTrue() + } + } + } + + @Test + fun `present - room address change alias unavailable`() = runTest { + val client = createMatrixClient(isAliasAvailable = false) + val presenter = createEditRoomAddressPresenter(client = client) + presenter.test { + with(awaitItem()) { + eventSink(EditRoomAddressEvents.RoomAddressChanged("valid")) + } + with(awaitItem()) { + assertThat(roomAddress).isEqualTo("valid") + assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Unknown) + } + with(awaitItem()) { + assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.NotAvailable) + assertThat(canBeSaved).isFalse() + } + } + } + + @Test + fun `present - save success no current alias`() = runTest { + val publishAliasInRoomDirectoryResult = lambdaRecorder> { _ -> Result.success(true) } + val updateCanonicalAliasResult = lambdaRecorder, Result> { _, _ -> Result.success(Unit) } + val removeAliasFromRoomDirectoryResult = lambdaRecorder> { _ -> Result.success(true) } + val closeEditAddressLambda = lambdaRecorder { } + val navigator = FakeSecurityAndPrivacyNavigator( + closeEditRoomAddressLambda = closeEditAddressLambda + ) + val room = FakeMatrixRoom( + updateCanonicalAliasResult = updateCanonicalAliasResult, + publishRoomAliasInRoomDirectoryResult = publishAliasInRoomDirectoryResult + ) + val presenter = createEditRoomAddressPresenter(room = room, navigator = navigator) + presenter.test { + with(awaitItem()) { + eventSink(EditRoomAddressEvents.RoomAddressChanged("valid")) + } + skipItems(1) + with(awaitItem()) { + assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Valid) + assertThat(canBeSaved).isTrue() + eventSink(EditRoomAddressEvents.Save) + } + with(awaitItem()) { + assertThat(saveAction).isEqualTo(AsyncAction.Loading) + } + with(awaitItem()) { + assertThat(saveAction).isEqualTo(AsyncAction.Success(Unit)) + } + + val createdAlias = RoomAlias("#valid:matrix.org") + assert(updateCanonicalAliasResult) + .isCalledOnce() + .with(value(createdAlias), value(emptyList())) + + assert(publishAliasInRoomDirectoryResult) + .isCalledOnce() + .with(value(createdAlias)) + + assert(removeAliasFromRoomDirectoryResult).isNeverCalled() + + assert(closeEditAddressLambda).isCalledOnce() + } + } + + @Test + fun `present - save success current canonical alias from own homeserver`() = runTest { + val publishAliasInRoomDirectoryResult = lambdaRecorder> { _ -> Result.success(true) } + val removeAliasFromRoomDirectoryResult = lambdaRecorder> { _ -> Result.success(true) } + val updateCanonicalAliasResult = lambdaRecorder, Result> { _, _ -> Result.success(Unit) } + val closeEditAddressLambda = lambdaRecorder { } + + val navigator = FakeSecurityAndPrivacyNavigator(closeEditRoomAddressLambda = closeEditAddressLambda) + val canonicalAlias = RoomAlias("#canonical:matrix.org") + val room = FakeMatrixRoom( + canonicalAlias = canonicalAlias, + updateCanonicalAliasResult = updateCanonicalAliasResult, + publishRoomAliasInRoomDirectoryResult = publishAliasInRoomDirectoryResult, + removeRoomAliasFromRoomDirectoryResult = removeAliasFromRoomDirectoryResult + ) + val presenter = createEditRoomAddressPresenter(room = room, navigator = navigator) + presenter.test { + with(awaitItem()) { + eventSink(EditRoomAddressEvents.RoomAddressChanged("valid")) + } + skipItems(1) + with(awaitItem()) { + assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Valid) + assertThat(canBeSaved).isTrue() + eventSink(EditRoomAddressEvents.Save) + } + with(awaitItem()) { + assertThat(saveAction).isEqualTo(AsyncAction.Loading) + } + with(awaitItem()) { + assertThat(saveAction).isEqualTo(AsyncAction.Success(Unit)) + } + + val createdAlias = RoomAlias("#valid:matrix.org") + assert(updateCanonicalAliasResult) + .isCalledOnce() + .with(value(createdAlias), value(emptyList())) + + assert(publishAliasInRoomDirectoryResult) + .isCalledOnce() + .with(value(createdAlias)) + + assert(removeAliasFromRoomDirectoryResult) + .isCalledOnce() + .with(value(canonicalAlias)) + + assert(closeEditAddressLambda).isCalledOnce() + } + } + + @Test + fun `present - save success current canonical alias from other homeserver`() = runTest { + val publishAliasInRoomDirectoryResult = lambdaRecorder> { _ -> Result.success(true) } + val removeAliasFromRoomDirectoryResult = lambdaRecorder> { _ -> Result.success(true) } + val updateCanonicalAliasResult = lambdaRecorder, Result> { _, _ -> Result.success(Unit) } + val closeEditAddressLambda = lambdaRecorder { } + + val navigator = FakeSecurityAndPrivacyNavigator(closeEditRoomAddressLambda = closeEditAddressLambda) + val canonicalAlias = RoomAlias("#canonical:notmatrix.org") + val room = FakeMatrixRoom( + canonicalAlias = canonicalAlias, + updateCanonicalAliasResult = updateCanonicalAliasResult, + publishRoomAliasInRoomDirectoryResult = publishAliasInRoomDirectoryResult, + removeRoomAliasFromRoomDirectoryResult = removeAliasFromRoomDirectoryResult + ) + val presenter = createEditRoomAddressPresenter(room = room, navigator = navigator) + presenter.test { + with(awaitItem()) { + eventSink(EditRoomAddressEvents.RoomAddressChanged("valid")) + } + skipItems(1) + with(awaitItem()) { + assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Valid) + assertThat(canBeSaved).isTrue() + eventSink(EditRoomAddressEvents.Save) + } + with(awaitItem()) { + assertThat(saveAction).isEqualTo(AsyncAction.Loading) + } + with(awaitItem()) { + assertThat(saveAction).isEqualTo(AsyncAction.Success(Unit)) + } + + val createdAlias = RoomAlias("#valid:matrix.org") + assert(updateCanonicalAliasResult) + .isCalledOnce() + .with(value(canonicalAlias), value(listOf(createdAlias))) + + assert(publishAliasInRoomDirectoryResult) + .isCalledOnce() + .with(value(createdAlias)) + + assert(removeAliasFromRoomDirectoryResult).isNeverCalled() + + assert(closeEditAddressLambda).isCalledOnce() + } + } + + @Test + fun `present - save failure`() = runTest { + val closeEditAddressLambda = lambdaRecorder { } + val navigator = FakeSecurityAndPrivacyNavigator( + closeEditRoomAddressLambda = closeEditAddressLambda + ) + val presenter = createEditRoomAddressPresenter(navigator = navigator) + presenter.test { + with(awaitItem()) { + eventSink(EditRoomAddressEvents.RoomAddressChanged("valid")) + } + skipItems(1) + with(awaitItem()) { + assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Valid) + assertThat(canBeSaved).isTrue() + eventSink(EditRoomAddressEvents.Save) + } + with(awaitItem()) { + assertThat(saveAction).isEqualTo(AsyncAction.Loading) + } + with(awaitItem()) { + assertThat(saveAction).isInstanceOf(AsyncAction.Failure::class.java) + } + + assert(closeEditAddressLambda).isNeverCalled() + } + } + + @Test + fun `present - dismiss error`() = runTest { + val presenter = createEditRoomAddressPresenter() + presenter.test { + with(awaitItem()) { + eventSink(EditRoomAddressEvents.Save) + } + with(awaitItem()) { + assertThat(saveAction).isInstanceOf(AsyncAction.Failure::class.java) + eventSink(EditRoomAddressEvents.DismissError) + } + with(awaitItem()) { + assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized) + } + } + } + + private fun createMatrixClient(isAliasAvailable: Boolean = true) = FakeMatrixClient( + userIdServerNameLambda = { "matrix.org" }, + resolveRoomAliasResult = { + val resolvedRoomAlias = if (isAliasAvailable) { + Optional.empty() + } else { + Optional.of(ResolvedRoomAlias(A_ROOM_ID, emptyList())) + } + Result.success(resolvedRoomAlias) + } + ) + + private fun createEditRoomAddressPresenter( + client: FakeMatrixClient = createMatrixClient(), + room: MatrixRoom = FakeMatrixRoom(), + navigator: SecurityAndPrivacyNavigator = FakeSecurityAndPrivacyNavigator(), + roomAliasHelper: RoomAliasHelper = FakeRoomAliasHelper() + ): EditRoomAddressPresenter { + return EditRoomAddressPresenter( + room = room, + client = client, + roomAliasHelper = roomAliasHelper, + navigator = navigator + ) + } +} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/editroomaddress/EditRoomAddressViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/editroomaddress/EditRoomAddressViewTest.kt new file mode 100644 index 00000000000..75924e8f596 --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/editroomaddress/EditRoomAddressViewTest.kt @@ -0,0 +1,123 @@ +/* + * 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.roomdetails.securityandprivacy.editroomaddress + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performTextInput +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress.EditRoomAddressEvents +import io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress.EditRoomAddressState +import io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress.EditRoomAddressView +import io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress.anEditRoomAddressState +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity +import io.element.android.libraries.testtags.TestTags +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.pressBack +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class EditRoomAddressViewTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `click on back invokes expected callback`() { + ensureCalledOnce { callback -> + rule.setEditRoomAddressView(onBackClick = callback) + rule.pressBack() + } + } + + @Test + fun `click on disabled save doesn't emit event`() { + val recorder = EventsRecorder(expectEvents = false) + val state = anEditRoomAddressState(eventSink = recorder) + rule.setEditRoomAddressView(state) + rule.clickOn(CommonStrings.action_save) + recorder.assertEmpty() + } + + @Test + fun `click on enabled save emits the expected event`() { + val recorder = EventsRecorder() + val state = anEditRoomAddressState( + roomAddress = "room", + roomAddressValidity = RoomAddressValidity.Valid, + eventSink = recorder + ) + rule.setEditRoomAddressView(state) + rule.clickOn(CommonStrings.action_save) + recorder.assertSingle(EditRoomAddressEvents.Save) + } + + @Test + fun `text changes on text field emits the expected event`() { + val recorder = EventsRecorder() + val state = anEditRoomAddressState( + roomAddress = "", + eventSink = recorder + ) + rule.setEditRoomAddressView(state) + + rule.onNodeWithTag(TestTags.roomAddressField.value).performTextInput("alias") + recorder.assertSingle(EditRoomAddressEvents.RoomAddressChanged("alias")) + } + + @Test + fun `click on dismiss error emits the expected event`() { + val recorder = EventsRecorder() + val state = anEditRoomAddressState( + roomAddress = "", + saveAction = AsyncAction.Failure(IllegalStateException()), + eventSink = recorder + ) + rule.setEditRoomAddressView(state) + rule.clickOn(CommonStrings.action_cancel) + recorder.assertSingle(EditRoomAddressEvents.DismissError) + } + + @Test + fun `click on retry error emits the expected event`() { + val recorder = EventsRecorder() + val state = anEditRoomAddressState( + roomAddress = "", + saveAction = AsyncAction.Failure(IllegalStateException()), + eventSink = recorder + ) + rule.setEditRoomAddressView(state) + rule.clickOn(CommonStrings.action_retry) + recorder.assertSingle(EditRoomAddressEvents.Save) + } + + +} + +private fun AndroidComposeTestRule.setEditRoomAddressView( + state: EditRoomAddressState = anEditRoomAddressState( + eventSink = EventsRecorder(expectEvents = false), + ), + onBackClick: () -> Unit = EnsureNeverCalled(), +) { + setContent { + EditRoomAddressView( + state = state, + onBackClick = onBackClick, + ) + } +} diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressField.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressField.kt index 590c1704745..c5b3a56514d 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressField.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressField.kt @@ -15,6 +15,8 @@ import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextField +import io.element.android.libraries.testtags.TestTags +import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings @Composable @@ -28,7 +30,7 @@ fun RoomAddressField( modifier: Modifier = Modifier, ) { TextField( - modifier = modifier, + modifier = modifier.testTag(TestTags.roomAddressField), value = address, label = label, leadingIcon = { diff --git a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt index a7557501848..63da929c945 100644 --- a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt +++ b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt @@ -111,4 +111,11 @@ object TestTags { * Generic call to action. */ val callToAction = TestTag("call_to_action") + + /** + * Room address field. + * + */ + val roomAddressField = TestTag("room_address_field") + } From d992f38fa59d9639a5d2c53d0cd38c235e0ab2d0 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 28 Jan 2025 14:39:27 +0100 Subject: [PATCH 23/30] feat(security&privacy) : clean code --- .../SecurityAndPrivacyEvents.kt | 4 +-- .../SecurityAndPrivacyFlowNode.kt | 1 - .../SecurityAndPrivacyNode.kt | 1 - .../SecurityAndPrivacyPresenter.kt | 15 +++++------ .../SecurityAndPrivacyState.kt | 18 ++++++------- .../SecurityAndPrivacyView.kt | 26 +++++++++---------- .../editroomaddress/EditRoomAddressNode.kt | 1 - .../editroomaddress/EditRoomAddressView.kt | 1 - .../SecurityAndPrivacyPermissions.kt | 1 - .../SecurityAndPrivacyPresenterTest.kt | 1 - .../EditRoomAddressPresenterTest.kt | 1 - .../EditRoomAddressViewTest.kt | 3 --- .../libraries/matrix/api/room/MatrixRoom.kt | 2 ++ .../matrix/impl/room/RustMatrixRoom.kt | 1 + .../impl/room/MatrixRoomInfoMapperTest.kt | 2 +- .../ui/room/address/RoomAddressField.kt | 2 +- .../android/libraries/testtags/TestTags.kt | 1 - 17 files changed, 35 insertions(+), 46 deletions(-) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt index 352bccf4537..5e09405998c 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt @@ -11,9 +11,9 @@ sealed interface SecurityAndPrivacyEvents { data object EditRoomAddress : SecurityAndPrivacyEvents data object Save : SecurityAndPrivacyEvents data class ChangeRoomAccess(val roomAccess: SecurityAndPrivacyRoomAccess) : SecurityAndPrivacyEvents - data object ToggleEncryptionState: SecurityAndPrivacyEvents + data object ToggleEncryptionState : SecurityAndPrivacyEvents data object CancelEnableEncryption : SecurityAndPrivacyEvents - data object ConfirmEnableEncryption: SecurityAndPrivacyEvents + data object ConfirmEnableEncryption : SecurityAndPrivacyEvents data class ChangeHistoryVisibility(val historyVisibility: SecurityAndPrivacyHistoryVisibility) : SecurityAndPrivacyEvents data object ToggleRoomVisibility : SecurityAndPrivacyEvents data object DismissSaveError : SecurityAndPrivacyEvents diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyFlowNode.kt index 76d5a5b0052..22fecf6143c 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyFlowNode.kt @@ -36,7 +36,6 @@ class SecurityAndPrivacyFlowNode @AssistedInject constructor( buildContext = buildContext, plugins = plugins, ) { - sealed interface NavTarget : Parcelable { @Parcelize data object SecurityAndPrivacy : NavTarget diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNode.kt index ed483e807e4..537306f44f2 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNode.kt @@ -24,7 +24,6 @@ class SecurityAndPrivacyNode @AssistedInject constructor( @Assisted plugins: List, presenterFactory: SecurityAndPrivacyPresenter.Factory, ) : Node(buildContext, plugins = plugins) { - private val navigator = plugins().first() private val presenter = presenterFactory.create(navigator) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt index 0fa653c4623..fe0a6f4c88d 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt @@ -60,7 +60,9 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor( val roomInfo by room.roomInfoFlow.collectAsState(null) val savedIsVisibleInRoomDirectory = remember { mutableStateOf>(AsyncData.Uninitialized) } - IsRoomVisibleInRoomDirectoryEffect(savedIsVisibleInRoomDirectory) + LaunchedEffect(Unit) { + isRoomVisibleInRoomDirectory(savedIsVisibleInRoomDirectory) + } val savedSettings by remember { derivedStateOf { @@ -159,12 +161,9 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor( return state } - @Composable - private fun IsRoomVisibleInRoomDirectoryEffect(isRoomVisible: MutableState>) { - LaunchedEffect(Unit) { - isRoomVisible.runUpdatingState { - room.getRoomVisibility().map { it == RoomVisibility.Public } - } + private fun CoroutineScope.isRoomVisibleInRoomDirectory(isRoomVisible: MutableState>) = launch { + isRoomVisible.runUpdatingState { + room.getRoomVisibility().map { it == RoomVisibility.Public } } } @@ -264,7 +263,6 @@ private fun RoomHistoryVisibility?.map(): SecurityAndPrivacyHistoryVisibility { RoomHistoryVisibility.Shared, is RoomHistoryVisibility.Custom, null -> SecurityAndPrivacyHistoryVisibility.SinceSelection - } } @@ -279,4 +277,3 @@ private fun SecurityAndPrivacyHistoryVisibility.map(): RoomHistoryVisibility { private fun MatrixRoomInfo.firstDisplayableAlias(serverName: String): RoomAlias? { return aliases.firstOrNull { it.matchesServer(serverName) } ?: aliases.firstOrNull() } - diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt index 3d2fc250e21..75b53bbd58d 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt @@ -10,6 +10,7 @@ package io.element.android.features.roomdetails.impl.securityandprivacy import io.element.android.features.roomdetails.impl.securityandprivacy.permissions.SecurityAndPrivacyPermissions import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData +import kotlinx.collections.immutable.toImmutableSet data class SecurityAndPrivacyState( // the settings that are currently applied on the room. @@ -22,9 +23,6 @@ data class SecurityAndPrivacyState( private val permissions: SecurityAndPrivacyPermissions, val eventSink: (SecurityAndPrivacyEvents) -> Unit ) { - - - val canBeSaved = savedSettings != editedSettings val availableHistoryVisibilities = buildSet { @@ -34,15 +32,12 @@ data class SecurityAndPrivacyState( } else { add(SecurityAndPrivacyHistoryVisibility.SinceInvite) } - } + }.toImmutableSet() val showRoomAccessSection = permissions.canChangeRoomAccess val showRoomVisibilitySections = permissions.canChangeRoomVisibility && editedSettings.roomAccess != SecurityAndPrivacyRoomAccess.InviteOnly val showHistoryVisibilitySection = permissions.canChangeHistoryVisibility val showEncryptionSection = permissions.canChangeEncryption - override fun toString(): String { - return "SecurityAndPrivacyState(savedSettings=$savedSettings, editedSettings=$editedSettings, homeserverName='$homeserverName', showEncryptionConfirmation=$showEncryptionConfirmation, saveAction=$saveAction, canBeSaved=$canBeSaved)" - } } data class SecurityAndPrivacySettings( @@ -54,7 +49,9 @@ data class SecurityAndPrivacySettings( ) enum class SecurityAndPrivacyHistoryVisibility { - SinceSelection, SinceInvite, Anyone; + SinceSelection, + SinceInvite, + Anyone; /** * Returns the fallback visibility when the current visibility is not available. @@ -69,7 +66,10 @@ enum class SecurityAndPrivacyHistoryVisibility { } enum class SecurityAndPrivacyRoomAccess { - InviteOnly, AskToJoin, Anyone, SpaceMember + InviteOnly, + AskToJoin, + Anyone, + SpaceMember } sealed class SecurityAndPrivacyFailures : Exception() { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt index 24fdf8bc2a4..c58687fde40 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt @@ -49,6 +49,7 @@ 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.ui.strings.CommonStrings +import kotlinx.collections.immutable.ImmutableSet @Composable fun SecurityAndPrivacyView( @@ -81,7 +82,7 @@ fun SecurityAndPrivacyView( modifier = Modifier.padding(top = 24.dp), edited = state.editedSettings.roomAccess, saved = state.savedSettings.roomAccess, - onSelected = { state.eventSink(SecurityAndPrivacyEvents.ChangeRoomAccess(it)) }, + onSelectOption = { state.eventSink(SecurityAndPrivacyEvents.ChangeRoomAccess(it)) }, ) } if (state.showRoomVisibilitySections) { @@ -111,7 +112,7 @@ fun SecurityAndPrivacyView( editedOption = state.editedSettings.historyVisibility, savedOptions = state.savedSettings.historyVisibility, availableOptions = state.availableHistoryVisibilities, - onSelected = { state.eventSink(SecurityAndPrivacyEvents.ChangeHistoryVisibility(it)) }, + onSelectOption = { state.eventSink(SecurityAndPrivacyEvents.ChangeHistoryVisibility(it)) }, ) } } @@ -180,7 +181,7 @@ private fun SecurityAndPrivacySection( private fun RoomAccessSection( edited: SecurityAndPrivacyRoomAccess, saved: SecurityAndPrivacyRoomAccess, - onSelected: (SecurityAndPrivacyRoomAccess) -> Unit, + onSelectOption: (SecurityAndPrivacyRoomAccess) -> Unit, modifier: Modifier = Modifier, ) { SecurityAndPrivacySection( @@ -191,19 +192,19 @@ private fun RoomAccessSection( headlineContent = { Text(text = stringResource(R.string.screen_security_and_privacy_room_access_invite_only_option_title)) }, supportingContent = { Text(text = stringResource(R.string.screen_security_and_privacy_room_access_invite_only_option_description)) }, trailingContent = ListItemContent.RadioButton(selected = edited == SecurityAndPrivacyRoomAccess.InviteOnly), - onClick = { onSelected(SecurityAndPrivacyRoomAccess.InviteOnly) }, + onClick = { onSelectOption(SecurityAndPrivacyRoomAccess.InviteOnly) }, ) 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 = { onSelected(SecurityAndPrivacyRoomAccess.AskToJoin) }, + onClick = { onSelectOption(SecurityAndPrivacyRoomAccess.AskToJoin) }, ) ListItem( headlineContent = { Text(text = stringResource(R.string.screen_security_and_privacy_room_access_anyone_option_title)) }, supportingContent = { Text(text = stringResource(R.string.screen_security_and_privacy_room_access_anyone_option_description)) }, trailingContent = ListItemContent.RadioButton(selected = edited == SecurityAndPrivacyRoomAccess.Anyone), - onClick = { onSelected(SecurityAndPrivacyRoomAccess.Anyone) }, + onClick = { onSelectOption(SecurityAndPrivacyRoomAccess.Anyone) }, ) if (saved == SecurityAndPrivacyRoomAccess.SpaceMember) { ListItem( @@ -333,8 +334,8 @@ private fun EncryptionSection( private fun HistoryVisibilitySection( editedOption: SecurityAndPrivacyHistoryVisibility?, savedOptions: SecurityAndPrivacyHistoryVisibility?, - availableOptions: Set, - onSelected: (SecurityAndPrivacyHistoryVisibility) -> Unit, + availableOptions: ImmutableSet, + onSelectOption: (SecurityAndPrivacyHistoryVisibility) -> Unit, modifier: Modifier = Modifier, ) { SecurityAndPrivacySection( @@ -347,7 +348,7 @@ private fun HistoryVisibilitySection( HistoryVisibilityItem( option = availableOption, isSelected = isSelected, - onSelected = onSelected, + onSelectOption = onSelectOption, ) } if (savedOptions != null && !availableOptions.contains(savedOptions)) { @@ -355,7 +356,7 @@ private fun HistoryVisibilitySection( option = savedOptions, isSelected = true, isEnabled = false, - onSelected = {}, + onSelectOption = {}, ) } } @@ -365,7 +366,7 @@ private fun HistoryVisibilitySection( private fun HistoryVisibilityItem( option: SecurityAndPrivacyHistoryVisibility, isSelected: Boolean, - onSelected: (SecurityAndPrivacyHistoryVisibility) -> Unit, + onSelectOption: (SecurityAndPrivacyHistoryVisibility) -> Unit, modifier: Modifier = Modifier, isEnabled: Boolean = true, ) { @@ -377,7 +378,7 @@ private fun HistoryVisibilityItem( ListItem( headlineContent = { Text(text = headlineText) }, trailingContent = ListItemContent.RadioButton(selected = isSelected, enabled = isEnabled), - onClick = { onSelected(option) }, + onClick = { onSelectOption(option) }, enabled = isEnabled, modifier = modifier, ) @@ -401,4 +402,3 @@ private fun ContentToPreview(state: SecurityAndPrivacyState) { onBackClick = {}, ) } - diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressNode.kt index 9b1ece00cc4..efdae76b61d 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressNode.kt @@ -25,7 +25,6 @@ class EditRoomAddressNode @AssistedInject constructor( @Assisted plugins: List, presenterFactory: EditRoomAddressPresenter.Factory, ) : Node(buildContext, plugins = plugins) { - private val navigator = plugins().first() private val presenter = presenterFactory.create(navigator) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressView.kt index efe224ba58b..d39208ee955 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressView.kt @@ -86,7 +86,6 @@ fun EditRoomAddressView( onRetry = { state.eventSink(EditRoomAddressEvents.Save) }, onErrorDismiss = { state.eventSink(EditRoomAddressEvents.DismissError) }, ) - } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/permissions/SecurityAndPrivacyPermissions.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/permissions/SecurityAndPrivacyPermissions.kt index 84436057f49..50a676da1e4 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/permissions/SecurityAndPrivacyPermissions.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/permissions/SecurityAndPrivacyPermissions.kt @@ -47,4 +47,3 @@ fun MatrixRoom.securityAndPrivacyPermissionsAsState(updateKey: Long): State AndroidComposeTestRule.setEditRoomAddressView( diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 9e921140ab7..6dd27defee9 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -433,6 +433,7 @@ interface MatrixRoom : Closeable { * directory and can be found using it. */ suspend fun getRoomVisibility(): Result + /** * Publish a new room alias for this room in the room directory. * @@ -442,6 +443,7 @@ interface MatrixRoom : Closeable { * published. */ suspend fun publishRoomAliasInRoomDirectory(roomAlias: RoomAlias): Result + /** * Remove an existing room alias for this room in the room directory. * diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 58a76a91e8c..3028a3ba48e 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -104,6 +104,7 @@ import org.matrix.rustcomponents.sdk.KnockRequest as InnerKnockRequest import org.matrix.rustcomponents.sdk.Room as InnerRoom import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline +@Suppress("LargeClass") class RustMatrixRoom( override val sessionId: SessionId, private val deviceId: DeviceId, diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapperTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapperTest.kt index 0639f8e9e40..aaa8ecc9ec5 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapperTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapperTest.kt @@ -34,8 +34,8 @@ import kotlinx.collections.immutable.toPersistentList import org.junit.Test import org.matrix.rustcomponents.sdk.Membership import org.matrix.rustcomponents.sdk.JoinRule as RustJoinRule -import org.matrix.rustcomponents.sdk.RoomNotificationMode as RustRoomNotificationMode import org.matrix.rustcomponents.sdk.RoomHistoryVisibility as RustRoomHistoryVisibility +import org.matrix.rustcomponents.sdk.RoomNotificationMode as RustRoomNotificationMode class MatrixRoomInfoMapperTest { @Test diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressField.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressField.kt index c5b3a56514d..acd7723b875 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressField.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressField.kt @@ -64,7 +64,7 @@ fun RoomAddressField( @PreviewsDayNight @Composable -fun RoomAddressFieldPreview() = ElementPreview { +internal fun RoomAddressFieldPreview() = ElementPreview { RoomAddressField( address = "room", homeserverName = "element.io", diff --git a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt index 63da929c945..ddc720ed401 100644 --- a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt +++ b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt @@ -117,5 +117,4 @@ object TestTags { * */ val roomAddressField = TestTag("room_address_field") - } From 58918b18ff2122471265745ea65369a2d89ca53f Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 28 Jan 2025 20:09:45 +0100 Subject: [PATCH 24/30] feat(security&privacy) : update canShowSecurityAndPrivacy check --- .../features/roomdetails/impl/RoomDetailsPresenter.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index 009882b537b..6b0845e9e2a 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -148,7 +148,12 @@ class RoomDetailsPresenter @Inject constructor( val roomMemberDetailsState = roomMemberDetailsPresenter?.present() - val securityAndPrivacyPermissions by room.securityAndPrivacyPermissionsAsState(syncUpdateFlow.value) + val securityAndPrivacyPermissions = room.securityAndPrivacyPermissionsAsState(syncUpdateFlow.value) + val canShowSecurityAndPrivacy by remember { + derivedStateOf { + isKnockRequestsEnabled && roomType is RoomDetailsType.Room && securityAndPrivacyPermissions.value.hasAny + } + } return RoomDetailsState( roomId = room.roomId, @@ -175,7 +180,7 @@ class RoomDetailsPresenter @Inject constructor( pinnedMessagesCount = pinnedMessagesCount, canShowKnockRequests = canShowKnockRequests, knockRequestsCount = knockRequestsCount, - canShowSecurityAndPrivacy = securityAndPrivacyPermissions.hasAny, + canShowSecurityAndPrivacy = canShowSecurityAndPrivacy, eventSink = ::handleEvents, ) } From d586bdc8153624f1442af159f670ec85a0cdfb3d Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 28 Jan 2025 20:09:58 +0100 Subject: [PATCH 25/30] feat(security&privacy) : some more clean up --- .../roomdetails/impl/RoomDetailsPresenter.kt | 21 ++++++++----------- .../SecurityAndPrivacyPresenter.kt | 8 +++---- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index 6b0845e9e2a..1bf44730e2f 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -9,7 +9,6 @@ package io.element.android.features.roomdetails.impl import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -105,7 +104,7 @@ class RoomDetailsPresenter @Inject constructor( val dmMember by room.getDirectRoomMember(membersState) val currentMember by room.getCurrentRoomMember(membersState) val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMember) - val roomType by getRoomType(dmMember, currentMember) + val roomType = getRoomType(dmMember, currentMember) val roomCallState = roomCallStatePresenter.present() val topicState = remember(canEditTopic, roomTopic, roomType) { @@ -196,16 +195,14 @@ class RoomDetailsPresenter @Inject constructor( private fun getRoomType( dmMember: RoomMember?, currentMember: RoomMember?, - ): State = remember(dmMember, currentMember) { - derivedStateOf { - if (dmMember != null && currentMember != null) { - RoomDetailsType.Dm( - me = currentMember, - otherMember = dmMember, - ) - } else { - RoomDetailsType.Room - } + ): RoomDetailsType = remember(dmMember, currentMember) { + if (dmMember != null && currentMember != null) { + RoomDetailsType.Dm( + me = currentMember, + otherMember = dmMember, + ) + } else { + RoomDetailsType.Room } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt index fe0a6f4c88d..199cd607f23 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt @@ -57,7 +57,7 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor( val saveAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } val homeserverName = remember { matrixClient.userIdServerName() } val syncUpdateFlow = room.syncUpdateFlow.collectAsState() - val roomInfo by room.roomInfoFlow.collectAsState(null) + val roomInfo = room.roomInfoFlow.collectAsState(null) val savedIsVisibleInRoomDirectory = remember { mutableStateOf>(AsyncData.Uninitialized) } LaunchedEffect(Unit) { @@ -67,11 +67,11 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor( val savedSettings by remember { derivedStateOf { SecurityAndPrivacySettings( - roomAccess = roomInfo?.joinRule.map(), + roomAccess = roomInfo.value?.joinRule.map(), isEncrypted = room.isEncrypted, isVisibleInRoomDirectory = savedIsVisibleInRoomDirectory.value, - historyVisibility = roomInfo?.historyVisibility.map(), - address = roomInfo?.firstDisplayableAlias(homeserverName)?.value + historyVisibility = roomInfo.value?.historyVisibility.map(), + address = roomInfo.value?.firstDisplayableAlias(homeserverName)?.value, ) } } From 706b298c3f5116b0de55f2a90f1cac0dcd43f3cb Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 28 Jan 2025 21:08:14 +0100 Subject: [PATCH 26/30] feat(security&privacy) : fix tests after some rework landed --- .../IdentityChangeStatePresenterTest.kt | 5 +- .../SecurityAndPrivacyStateProvider.kt | 4 +- .../features/roomdetails/MatrixRoomFixture.kt | 5 + .../impl/RoomDetailsPresenterTest.kt | 92 +++++++++++++++---- .../matrix/ui/room/MatrixRoomMembers.kt | 3 +- 5 files changed, 87 insertions(+), 22 deletions(-) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt index 416df67a7f2..115e14c82c2 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt @@ -67,7 +67,10 @@ class IdentityChangeStatePresenterTest { @Test fun `present - when the clear room emits identity change, the presenter does not emit new state`() = runTest { - val room = FakeMatrixRoom(isEncrypted = false) + val room = FakeMatrixRoom( + isEncrypted = false, + enableEncryptionResult = { Result.success(Unit) } + ) val presenter = createIdentityChangeStatePresenter(room) presenter.test { val initialState = awaitItem() diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt index eb77b89ecad..7fe1fea52f6 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt @@ -1,8 +1,8 @@ /* * Copyright 2025 New Vector Ltd. * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. + * 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.roomdetails.impl.securityandprivacy diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/MatrixRoomFixture.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/MatrixRoomFixture.kt index 06795281c48..2bc200a0d8f 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/MatrixRoomFixture.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/MatrixRoomFixture.kt @@ -11,6 +11,7 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.StateEventType +import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_NAME @@ -29,9 +30,11 @@ fun aMatrixRoom( isEncrypted: Boolean = true, isPublic: Boolean = true, isDirect: Boolean = false, + joinRule: JoinRule? = null, notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), emitRoomInfo: Boolean = false, canInviteResult: (UserId) -> Result = { lambdaError() }, + canBanResult: (UserId) -> Result = { lambdaError() }, canSendStateResult: (UserId, StateEventType) -> Result = { _, _ -> lambdaError() }, userDisplayNameResult: (UserId) -> Result = { lambdaError() }, userAvatarUrlResult: () -> Result = { lambdaError() }, @@ -51,6 +54,7 @@ fun aMatrixRoom( isDirect = isDirect, notificationSettingsService = notificationSettingsService, canInviteResult = canInviteResult, + canBanResult = canBanResult, canSendStateResult = canSendStateResult, userDisplayNameResult = userDisplayNameResult, userAvatarUrlResult = userAvatarUrlResult, @@ -70,6 +74,7 @@ fun aMatrixRoom( avatarUrl = avatarUrl, isDirect = isDirect, isPublic = isPublic, + joinRule = joinRule, ) ) } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt index 952363e0188..81e95cc051e 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt @@ -24,6 +24,7 @@ import io.element.android.features.roomdetails.impl.members.details.RoomMemberDe import io.element.android.features.userprofile.shared.aUserProfileState import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers +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.core.UserId @@ -31,6 +32,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.StateEventType +import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_NAME @@ -75,6 +77,12 @@ class RoomDetailsPresenterTest { dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), analyticsService: AnalyticsService = FakeAnalyticsService(), + featureFlagService: FeatureFlagService = FakeFeatureFlagService( + mapOf( + FeatureFlags.NotificationSettings.key to true, + FeatureFlags.Knock.key to false, + ) + ), isPinnedMessagesFeatureEnabled: Boolean = true, ): RoomDetailsPresenter { val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService) @@ -89,9 +97,6 @@ class RoomDetailsPresenterTest { ) } } - val featureFlagService = FakeFeatureFlagService( - mapOf(FeatureFlags.NotificationSettings.key to true) - ) return RoomDetailsPresenter( client = matrixClient, room = room, @@ -133,6 +138,7 @@ class RoomDetailsPresenterTest { assertThat(initialState.isEncrypted).isEqualTo(room.isEncrypted) assertThat(initialState.canShowPinnedMessages).isTrue() assertThat(initialState.pinnedMessagesCount).isNull() + assertThat(initialState.canShowSecurityAndPrivacy).isFalse() } } @@ -270,8 +276,7 @@ class RoomDetailsPresenterTest { when (stateEventType) { StateEventType.ROOM_TOPIC -> Result.success(true) StateEventType.ROOM_NAME -> Result.success(false) - StateEventType.ROOM_AVATAR -> Result.failure(Throwable("Whelp")) - else -> lambdaError() + else -> Result.failure(Throwable("Whelp")) } }, canInviteResult = { Result.success(false) }, @@ -297,10 +302,10 @@ class RoomDetailsPresenterTest { isDirect = true, canSendStateResult = { _, stateEventType -> when (stateEventType) { - StateEventType.ROOM_TOPIC -> Result.success(true) - StateEventType.ROOM_NAME -> Result.success(true) + StateEventType.ROOM_TOPIC, + StateEventType.ROOM_NAME, StateEventType.ROOM_AVATAR -> Result.success(true) - else -> lambdaError() + else -> Result.failure(Throwable("Whelp")) } }, canInviteResult = { Result.success(false) }, @@ -343,7 +348,7 @@ class RoomDetailsPresenterTest { StateEventType.ROOM_AVATAR, StateEventType.ROOM_TOPIC, StateEventType.ROOM_NAME -> Result.success(true) - else -> lambdaError() + else -> Result.failure(Throwable("Whelp")) } }, canInviteResult = { Result.success(true) }, @@ -376,10 +381,10 @@ class RoomDetailsPresenterTest { val room = aMatrixRoom( canSendStateResult = { _, stateEventType -> when (stateEventType) { - StateEventType.ROOM_TOPIC -> Result.success(true) - StateEventType.ROOM_NAME -> Result.success(true) + StateEventType.ROOM_TOPIC, + StateEventType.ROOM_NAME, StateEventType.ROOM_AVATAR -> Result.success(true) - else -> lambdaError() + else -> Result.failure(Throwable("Whelp")) } }, canInviteResult = { @@ -403,10 +408,10 @@ class RoomDetailsPresenterTest { val room = aMatrixRoom( canSendStateResult = { _, stateEventType -> when (stateEventType) { - StateEventType.ROOM_TOPIC -> Result.success(false) - StateEventType.ROOM_NAME -> Result.success(false) + StateEventType.ROOM_TOPIC, + StateEventType.ROOM_NAME, StateEventType.ROOM_AVATAR -> Result.success(false) - else -> lambdaError() + else -> Result.failure(Throwable("Whelp")) } }, canInviteResult = { @@ -432,7 +437,7 @@ class RoomDetailsPresenterTest { StateEventType.ROOM_AVATAR, StateEventType.ROOM_NAME -> Result.success(true) StateEventType.ROOM_TOPIC -> Result.success(false) - else -> lambdaError() + else -> Result.failure(Throwable("Whelp")) } }, canInviteResult = { @@ -458,7 +463,7 @@ class RoomDetailsPresenterTest { StateEventType.ROOM_AVATAR, StateEventType.ROOM_TOPIC, StateEventType.ROOM_NAME -> Result.success(true) - else -> lambdaError() + else -> Result.failure(Throwable("Whelp")) } }, canInviteResult = { @@ -632,4 +637,57 @@ class RoomDetailsPresenterTest { cancelAndIgnoreRemainingEvents() } } + + @Test + fun `present - show knock requests`() = runTest { + val room = aMatrixRoom( + emitRoomInfo = true, + canInviteResult = { Result.success(true) }, + canUserJoinCallResult = { Result.success(true) }, + canSendStateResult = { _, _ -> Result.success(true) }, + joinRule = JoinRule.Knock, + ) + val featureFlagService = FakeFeatureFlagService( + mapOf(FeatureFlags.Knock.key to false) + ) + val presenter = createRoomDetailsPresenter( + room = room, + featureFlagService = featureFlagService, + ) + presenter.test { + skipItems(1) + with(awaitItem()) { + assertThat(canShowKnockRequests).isFalse() + } + featureFlagService.setFeatureEnabled(FeatureFlags.Knock, true) + with(awaitItem()) { + assertThat(canShowKnockRequests).isTrue() + } + room.givenRoomInfo(aRoomInfo(joinRule = JoinRule.Private)) + with(awaitItem()) { + assertThat(canShowKnockRequests).isFalse() + } + } + } + + @Test + fun `present - show security and privacy`() = runTest { + val room = aMatrixRoom( + canInviteResult = { Result.success(true) }, + canUserJoinCallResult = { Result.success(true) }, + canSendStateResult = { _, _ -> Result.success(true) }, + ) + val featureFlagService = FakeFeatureFlagService() + val presenter = createRoomDetailsPresenter(room = room, featureFlagService = featureFlagService) + presenter.test { + skipItems(1) + with(awaitItem()) { + assertThat(canShowSecurityAndPrivacy).isFalse() + } + featureFlagService.setFeatureEnabled(FeatureFlags.Knock, true) + with(awaitItem()) { + assertThat(canShowSecurityAndPrivacy).isTrue() + } + } + } } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt index c6a8b75816c..8ab1a4b9f50 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt @@ -17,7 +17,6 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.RoomMember -import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.room.roomMembers @Composable @@ -45,7 +44,7 @@ fun MatrixRoom.getDirectRoomMember(roomMembersState: MatrixRoomMembersState): St derivedStateOf { roomMembers ?.filter { it.membership.isActive() } - ?.takeIf { isDm } + ?.takeIf { it.size == 2 && isDirect } ?.find { it.userId != sessionId } } } From 82f59265e610301fc7996e99624d9d1209c66fa8 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Tue, 28 Jan 2025 20:29:01 +0000 Subject: [PATCH 27/30] Update screenshots --- ...ndprivacy.editroomaddress_EditRoomAddressView_Day_0_en.png | 3 +++ ...ndprivacy.editroomaddress_EditRoomAddressView_Day_1_en.png | 3 +++ ...ndprivacy.editroomaddress_EditRoomAddressView_Day_2_en.png | 3 +++ ...ndprivacy.editroomaddress_EditRoomAddressView_Day_3_en.png | 3 +++ ...ndprivacy.editroomaddress_EditRoomAddressView_Day_4_en.png | 3 +++ ...privacy.editroomaddress_EditRoomAddressView_Night_0_en.png | 3 +++ ...privacy.editroomaddress_EditRoomAddressView_Night_1_en.png | 3 +++ ...privacy.editroomaddress_EditRoomAddressView_Night_2_en.png | 3 +++ ...privacy.editroomaddress_EditRoomAddressView_Night_3_en.png | 3 +++ ...privacy.editroomaddress_EditRoomAddressView_Night_4_en.png | 3 +++ ...mpl.securityandprivacy_SecurityAndPrivacyViewDark_0_en.png | 3 +++ ...mpl.securityandprivacy_SecurityAndPrivacyViewDark_1_en.png | 3 +++ ...mpl.securityandprivacy_SecurityAndPrivacyViewDark_2_en.png | 3 +++ ...mpl.securityandprivacy_SecurityAndPrivacyViewDark_3_en.png | 3 +++ ...mpl.securityandprivacy_SecurityAndPrivacyViewDark_4_en.png | 3 +++ ...mpl.securityandprivacy_SecurityAndPrivacyViewDark_5_en.png | 3 +++ ...mpl.securityandprivacy_SecurityAndPrivacyViewDark_6_en.png | 3 +++ ...pl.securityandprivacy_SecurityAndPrivacyViewLight_0_en.png | 3 +++ ...pl.securityandprivacy_SecurityAndPrivacyViewLight_1_en.png | 3 +++ ...pl.securityandprivacy_SecurityAndPrivacyViewLight_2_en.png | 3 +++ ...pl.securityandprivacy_SecurityAndPrivacyViewLight_3_en.png | 3 +++ ...pl.securityandprivacy_SecurityAndPrivacyViewLight_4_en.png | 3 +++ ...pl.securityandprivacy_SecurityAndPrivacyViewLight_5_en.png | 3 +++ ...pl.securityandprivacy_SecurityAndPrivacyViewLight_6_en.png | 3 +++ .../images/features.roomdetails.impl_RoomDetailsDark_0_en.png | 4 ++-- .../features.roomdetails.impl_RoomDetailsDark_10_en.png | 4 ++-- .../features.roomdetails.impl_RoomDetailsDark_11_en.png | 4 ++-- .../features.roomdetails.impl_RoomDetailsDark_12_en.png | 4 ++-- .../features.roomdetails.impl_RoomDetailsDark_13_en.png | 4 ++-- .../features.roomdetails.impl_RoomDetailsDark_14_en.png | 4 ++-- .../features.roomdetails.impl_RoomDetailsDark_15_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetailsDark_1_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetailsDark_2_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetailsDark_3_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetailsDark_4_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetailsDark_5_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetailsDark_6_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetailsDark_7_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetailsDark_8_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetailsDark_9_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetails_0_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetails_10_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetails_11_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetails_12_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetails_13_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetails_14_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetails_15_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetails_1_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetails_2_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetails_3_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetails_4_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetails_5_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetails_6_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetails_7_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetails_8_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetails_9_en.png | 4 ++-- ...aries.matrix.ui.room.address_RoomAddressField_Day_0_en.png | 3 +++ ...ies.matrix.ui.room.address_RoomAddressField_Night_0_en.png | 3 +++ 58 files changed, 142 insertions(+), 64 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_5_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_6_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_5_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_6_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.matrix.ui.room.address_RoomAddressField_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.matrix.ui.room.address_RoomAddressField_Night_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_0_en.png new file mode 100644 index 00000000000..8e0dd979faa --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2701eb3f171f1724f328260dd80b0bd882e6f6f62adfb0caa76c0e727501b78d +size 24635 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_1_en.png new file mode 100644 index 00000000000..9054ab58529 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0d5f98545b38207b0b61cc055112524b2b87c9969b41246a90d46554143905e +size 28628 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_2_en.png new file mode 100644 index 00000000000..ea9fcab7f4e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c963f28503909ff8f965910922cefd82d4873904cee34bbbf4189b3a29b2fddb +size 30877 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_3_en.png new file mode 100644 index 00000000000..09f45d7b602 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8965c0672a72d1af44bbbc6931ffa056a3b456265f641777c9f5bad8f9aed7fe +size 24543 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_4_en.png new file mode 100644 index 00000000000..9911625ab97 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:465436bd517d319aa8f5355e8e92630ac14d1253ff7f0dc5c856fca3e6dca816 +size 26504 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_0_en.png new file mode 100644 index 00000000000..b1257020438 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8a158a83fe144502b24056f5ca3b439a7ee1934924394fe7ecbae01bf71ad40 +size 24170 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_1_en.png new file mode 100644 index 00000000000..bd707d64d36 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8073fcc88a65c07810373c176f790ebc1f41953294a36b44a6d21bd102482d4c +size 28075 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_2_en.png new file mode 100644 index 00000000000..1b4e23cb3ba --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f3e187b8590ec247fa2640db033fb34b8f8eb8c5bfee6a4598514049cf87da9 +size 29909 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_3_en.png new file mode 100644 index 00000000000..383b6e36d73 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b323513f415948d783b7a5d378e59195127a3e006ff3a52521a3153ec6003fac +size 24017 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_4_en.png new file mode 100644 index 00000000000..c9a65a5a326 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f90998d844da64b84b8a50506af0dd1a15f3a31591d9e2f3a7945792927365b8 +size 25141 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_0_en.png new file mode 100644 index 00000000000..60363c684ed --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a5339ef524ed3bb5bd2399f3467afd35f22d2f44d64e923af76557f31b11663 +size 39675 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_1_en.png new file mode 100644 index 00000000000..a1d49291969 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8b8e7540627707d399579100fb807b170a1291f58aaa58218f23d4c83b27aafc +size 61755 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_2_en.png new file mode 100644 index 00000000000..fc75b939ab1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9addb6800b01883a97c2ce59f9680298ca541b2ac3cbab72dd152531c547ab3b +size 61890 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_3_en.png new file mode 100644 index 00000000000..d0cd9caf9f9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78f2e0bc28d05baad8db0de7d3d1f9917c015a2e1fec8faae9425c0c66fdeba2 +size 60873 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_4_en.png new file mode 100644 index 00000000000..f8112e51132 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ca121a0983d794ba2cea29016edb68c2e361fdf50f9bcba286f433a47f08e06 +size 39464 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_5_en.png new file mode 100644 index 00000000000..f8112e51132 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ca121a0983d794ba2cea29016edb68c2e361fdf50f9bcba286f433a47f08e06 +size 39464 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_6_en.png new file mode 100644 index 00000000000..462134421f8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:249d48df6f0b7e8b0ba8cced66edb29a0b2679c364cf99cfecf8672ecd4d5a87 +size 43716 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_0_en.png new file mode 100644 index 00000000000..c0b4d011f05 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a713418b41e03da12fca6c65895e8928074af4cf9a2599e81124b0cf34f668c +size 41217 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_1_en.png new file mode 100644 index 00000000000..05575fa95ed --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83d8d1eb701b4b557993fa0691f3a465e6ebe5c76db6ce4d205673a957384480 +size 63708 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_2_en.png new file mode 100644 index 00000000000..27b2609efc5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e2f287ae525408a680edf525cbd4142ceddd8c33fda00cadb2ca6b69906a6399 +size 63937 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_3_en.png new file mode 100644 index 00000000000..141d93c3fc0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa90d947a78dc8b71492107a03117ab30c6123a402781dcdc3223b93a982756a +size 62738 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_4_en.png new file mode 100644 index 00000000000..0c4fd8df988 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6f137fd2f9280a6e023992fc149a0bd4cdf1cc82dc5bd5676fe3df825054cf66 +size 41046 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_5_en.png new file mode 100644 index 00000000000..0c4fd8df988 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6f137fd2f9280a6e023992fc149a0bd4cdf1cc82dc5bd5676fe3df825054cf66 +size 41046 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_6_en.png new file mode 100644 index 00000000000..16e006428d8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b084c7f1dc01dbb08eddd6d2197d7e7f679258700433816a869262115326432b +size 45893 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_0_en.png index 4bd3a2e3666..fe561de9dbc 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:709bd4347a1188fefe43a4da9b9c0c893f79383768b3f7a7c455f5057b824f13 -size 41767 +oid sha256:f883df9f8dd09fe8ac29cdeb0358e1b1f82fd0bf4f3b00778dcf2a436dc332a3 +size 42513 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_10_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_10_en.png index 7c4181f0597..2d1351f34b3 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:576724d5b9958897fad4544d20a0618303729297c4b172a6908bdfd51e06692f -size 39781 +oid sha256:ec59f34715f2bdcc1dc1ca9718f05366aebf7973a027b261633934680f85d9b4 +size 41133 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_11_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_11_en.png index ced12028979..5da528088f5 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d061fabbae9b28c800b553429b9ef747cea50ee17e219ed2aed9101f052beac3 -size 38712 +oid sha256:6cb196faae29f31e36f7e5a2f711e71755dbd76b0d7b2126cb79575628bef1f2 +size 40103 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_12_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_12_en.png index e7be02c262b..81da08c8ebb 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ba395fd405b9e7ff9e0d412e4f47001e09fbd7c7d22ccec267482d5b4c64580 -size 41921 +oid sha256:d17ebd1e51a73a8e213f792dc220125c77bc9143c9327bce9d25cb5573b63d89 +size 43278 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_13_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_13_en.png index 282e789cf5b..a61f6a87bdd 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60fb9372f93da45389dd0b6f97a3996e04ab46b5139472498ae29379e3fec64c -size 40171 +oid sha256:bc0c8d01736a19447d3e0c810fe594578b3cbacd5de063a7e314df8da3b81cb6 +size 41415 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_14_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_14_en.png index 68cdd560991..e7bb5b3e8b7 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_14_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de95e549a07106bdc8a016afd3e6449bdbe4006c25a9fae1c44893f28be74ace -size 41391 +oid sha256:0c36a541e25043a7c85d2aeab088005afeb84b36354e8231ac10dcbe95c9c278 +size 42216 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_15_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_15_en.png index 695a089fb50..a9ae3fc4b4b 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_15_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_15_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe506b8c3e544d7aa97db244334928fb0752ad507af7a648ca7f9b14516f6b7a -size 41972 +oid sha256:afc35cdc58f53f14f4584dbed43744435aacfa6d3ddb0b3b5f0e0ab7e9a7cc16 +size 42799 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_1_en.png index 4c210b43a62..142b78cefc9 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2c33cb4423e91f474aaf2461ba1edefa0c3188110fc28c875f54ec2ec595113b -size 39734 +oid sha256:79061fd2609f0e61df7af26919d68f22b78864ce031a6fa2f397af419601c367 +size 32253 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_2_en.png index 9c0c9ccaad7..5d7829db3c1 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c69a4deb8ff7bc7d49319458a14e1bf4c58965b683109b87386ce56a502b4516 -size 39845 +oid sha256:879817d4155151a61cdf642b91ccc344f38bfb1edbf75129302fd0a115fc16d6 +size 34378 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_3_en.png index 02c6bdab686..228ea7493f0 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:42a45c5f60ef568a36f36e62f867b96f9efdde927e4359d67a891995c8ad51fb -size 40477 +oid sha256:984798b0b880ee29f4f328e25ad531d56cd992f3fd3771b46d94fcbe1c1b9d8e +size 41626 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_4_en.png index 78a81600690..56a3c5e8b65 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:983cd78c88bdfcdc634183ad1fa477a9e0d45480b3404a7216341befcc0da0da -size 40916 +oid sha256:fec1fc0c0932de4f00d7146120708380b64358123b97a227458bfba5117ef40b +size 40488 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_5_en.png index 02308b114b1..28dac2ec2c5 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b52fa2d5d7f840b302d4bda533e7bd1539f4a1027f89745b1d9f1915edb2347c -size 39956 +oid sha256:4a8c62eb5deae233cce822d39d85f2faad5152b84469c84ace57c042499d4f51 +size 38727 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_6_en.png index 5f134c3e646..a0132efcad3 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:943fd6c08995800a0e06fdc6138998e557298169fa22775b6ada5e7e46d802ab -size 40433 +oid sha256:c1dd220efe1e2b30ebc14e10bb359b97a7be9c9d25c35e339ad74384a18150a2 +size 42675 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_7_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_7_en.png index 07df5b931b3..434fb525f3d 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4dfe015521687b91967d080ff5fa28cbceb18287134e559f94d29ecfa90576dd -size 41045 +oid sha256:58349ebc2291b8fe7daefd39db44983b6b10712b6d56984745cee8b4e1fbe96e +size 42429 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_8_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_8_en.png index b043de46369..c530fc7ff03 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e7697394fc4e3b6c904e2b64f37b0ef246c8974f374da32a99c52d5e088464d -size 40054 +oid sha256:9c8ed00bd704382cdb1b12507f54c7fb258b0b15f95cd376a816a457dda95d40 +size 41413 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_9_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_9_en.png index ff37ac939bc..f4788aff681 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0749fc60229f062442f3aa9e2412decb948ebbd73a7252c99cae60e6deb3fe59 -size 40089 +oid sha256:a04aa114cb44b5aecfb9a5520bb1ba88d068c3722ba074858f0e1102f2d38766 +size 41436 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_0_en.png index 0b883b941fb..b83dad80c2c 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e31fcab521339fba5673be9c5877a6941c6b860bb503cc0250ffa66cb3e8ff85 -size 42778 +oid sha256:67ca921d06a4017b701ebcd3703eb58e7032a54a6e1b19fc02808fbaf3285a00 +size 43501 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_10_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_10_en.png index efe721ed2b0..eb62c634e12 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f6b82228e783338f897979a3abb97a0fed034aa7b7eae4636ad8765891218e07 -size 40565 +oid sha256:34237556029910293162d2ea98eeb9bf55f8393f2439d152d0afb7a7e40e511e +size 42032 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_11_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_11_en.png index d204487fd3d..3a8e618495e 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6dfd9fd3df8f4525ecce0cabed01b260a4353c7669fecb7066124b3b50bc96d4 -size 39575 +oid sha256:ad2042b9223b191b36cde6e0bfb1be25babcc24566ff80b3c23acdcfd5906385 +size 41016 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_12_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_12_en.png index f4a4b3e4f1d..f55cfdaba4c 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac302fccfce9af09fdbc9724629a4931721c734c9cca20996f98d482ed28c6c3 -size 42433 +oid sha256:4d1ea80b3990d355b42356ea6ed8419705a4d8174851869742b1372f5d815648 +size 43868 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_13_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_13_en.png index 834ccd76d64..e9f7c3b7e1c 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3ef732bcc856038402a797fd490ac9416331b6dc053135b0b5317e2e059f8a4 -size 41014 +oid sha256:3ca66de9f5bbedce4f33c86710cc6c63b96ff872ecf8514cf7dd39043fb6ba0c +size 42379 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_14_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_14_en.png index 5fe17f4dac7..8916044198e 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_14_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:807e94b33b8526b744ad282aaa0e2be4b0c38575f153b188518a05398daa0587 -size 42314 +oid sha256:5bf22a62baea13b14d736ec6166401c73f9f8704f8436ba9ca631ba05e3425a6 +size 43179 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_15_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_15_en.png index 7fdcbd4112a..f03fe00b2c5 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_15_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_15_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9f6fc09195fb3637c7b5987653b43107d0b59bba2fcfcd9a6d2b3518e640bbc -size 42930 +oid sha256:1627c9920140b37b99dd482ce8bca5ee12882cb90ff1968f66daf60837b7cf53 +size 43790 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_1_en.png index 01caa4477a4..90569a9e7e0 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ff95b2f7eff613ea2f902f17c5574c2dbb26d004c27db735cb2aa66284f91bc -size 40844 +oid sha256:47954db34506ba2560550d94cbe2cb1278866ab2843e34f92f8e1ae535bbd173 +size 33090 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_2_en.png index b72ff1f24db..35db5282958 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f3cbdf099ceb529945b70916ef2bd54e16b3f331dfb97f68cbb0ff88c84813c1 -size 40946 +oid sha256:30aa5d530772b143947ef620736128832dd47c8a2692a7bfbaee74ef66107f02 +size 35273 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_3_en.png index 1f18c75f477..4e678941ef6 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:334f324214726b17df8fa73b1d8440154ac7878cbaa5d6cb02fc74342ccaea2a -size 41230 +oid sha256:82670330ea89664b305e223c23489e2746f645c5cbcdc33da8f2c629aeb9739f +size 42505 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_4_en.png index 165683b68f8..bf9d831f5e1 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a531b62c8415b8289e338ddfd29d726aedc61537ec19e5fe7f38aa507e773af -size 41938 +oid sha256:15c1291fd3c6547285a9f62b5c6c8516fbe714fd79392c9738531cbcf1d97997 +size 41450 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_5_en.png index 9373b7a1488..e177fad5e2b 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:27f0e1a0609d3e513f74dce2d3615e46e13884bbb03731138b356254c798056a -size 40804 +oid sha256:a4882133a2dd3460a7567e01de5803d502199c2b79b72d6f66b1a8c6df3021f6 +size 39498 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_6_en.png index d759503e0db..e20889197a0 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce4f440bff9c4f7d4ae846ae7010c0c31a78d7fef1c8cb5697d44cd4a1aa78c1 -size 41673 +oid sha256:c93a5ad4c336ea01f6f17ce17c6c933066c2298c587b3db9bf24eff48c286f4d +size 44073 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_7_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_7_en.png index a64ac133f6e..99bbb99d554 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d9cf42f259898b68d38b04ef4f4f7467cf74960a65bdc31813e9f7869c9b29f8 -size 42086 +oid sha256:1e3b4b5ab82004736abf58e29ba4db7f628e25c8344a782263cada5544ae3748 +size 43528 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_8_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_8_en.png index 47e1cf460e6..8e8cef64dab 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:36c86532bf3764338e7de7683e87c6bbb552e188fff0da8c49de89c94994416c -size 40979 +oid sha256:6edfe665ed9121e32914884f32fb0801918b26574e04c2ddb07a0f2e66bef567 +size 42449 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_9_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_9_en.png index 110c6c25038..39b22edf338 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a38bbf527dd323955331baeb1c0b2578faa101ddeb83cecf38e94bc303f05e38 -size 40940 +oid sha256:35a491ed5aba0d99f1db99019f3f22d09c74a4dce7693e95e1a356a0353df626 +size 42405 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.room.address_RoomAddressField_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.room.address_RoomAddressField_Day_0_en.png new file mode 100644 index 00000000000..2eaadf7ac1a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.room.address_RoomAddressField_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd0f5901d95cd96782fbea2d3d6ce43db120d8f9b5d5f19bbfbac08a9f7e7ee9 +size 16581 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.room.address_RoomAddressField_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.room.address_RoomAddressField_Night_0_en.png new file mode 100644 index 00000000000..2a633b52bb7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.room.address_RoomAddressField_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c39cd0a2bea7160a7dc2c1590f7b3faa60ef18ca40702c886e70df21a6dfd09d +size 16254 From 8880eeda407aac4c587552d61e9318bccce6e959 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 29 Jan 2025 14:16:57 +0100 Subject: [PATCH 28/30] feat(security&privacy) : improve and document code after PR review. --- .../SecurityAndPrivacyPresenter.kt | 26 +++++++++++-------- .../SecurityAndPrivacyState.kt | 2 +- .../SecurityAndPrivacyStateProvider.kt | 2 +- .../SecurityAndPrivacyView.kt | 6 +++-- .../EditRoomAddressPresenter.kt | 10 +++---- .../SecurityAndPrivacyPresenterTest.kt | 12 ++++----- .../libraries/matrix/api/room/MatrixRoom.kt | 4 +-- 7 files changed, 32 insertions(+), 30 deletions(-) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt index 199cd607f23..769ff87baee 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt @@ -96,7 +96,7 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor( address = savedSettings.address, ) - var showEncryptionConfirmation by remember(savedSettings.isEncrypted) { mutableStateOf(false) } + var showEnableEncryptionConfirmation by remember(savedSettings.isEncrypted) { mutableStateOf(false) } val permissions by room.securityAndPrivacyPermissionsAsState(syncUpdateFlow.value) fun handleEvents(event: SecurityAndPrivacyEvents) { @@ -116,7 +116,7 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor( if (editedIsEncrypted) { editedIsEncrypted = false } else { - showEncryptionConfirmation = true + showEnableEncryptionConfirmation = true } } is SecurityAndPrivacyEvents.ChangeHistoryVisibility -> { @@ -130,10 +130,10 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor( } SecurityAndPrivacyEvents.EditRoomAddress -> navigator.openEditRoomAddress() SecurityAndPrivacyEvents.CancelEnableEncryption -> { - showEncryptionConfirmation = false + showEnableEncryptionConfirmation = false } SecurityAndPrivacyEvents.ConfirmEnableEncryption -> { - showEncryptionConfirmation = false + showEnableEncryptionConfirmation = false editedIsEncrypted = true } SecurityAndPrivacyEvents.DismissSaveError -> { @@ -146,7 +146,7 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor( savedSettings = savedSettings, editedSettings = editedSettings, homeserverName = homeserverName, - showEncryptionConfirmation = showEncryptionConfirmation, + showEnableEncryptionConfirmation = showEnableEncryptionConfirmation, saveAction = saveAction.value, permissions = permissions, eventSink = ::handleEvents @@ -189,8 +189,9 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor( } } val updateJoinRule = async { - if (editedSettings.roomAccess != savedSettings.roomAccess) { - room.updateJoinRule(editedSettings.roomAccess.map()) + val joinRule = editedSettings.roomAccess.map() + if (editedSettings.roomAccess != savedSettings.roomAccess && joinRule != null) { + room.updateJoinRule(joinRule) } else { Result.success(Unit) } @@ -239,19 +240,21 @@ private fun JoinRule?.map(): SecurityAndPrivacyRoomAccess { JoinRule.Public -> SecurityAndPrivacyRoomAccess.Anyone JoinRule.Knock, is JoinRule.KnockRestricted -> SecurityAndPrivacyRoomAccess.AskToJoin is JoinRule.Restricted -> SecurityAndPrivacyRoomAccess.SpaceMember + JoinRule.Invite -> SecurityAndPrivacyRoomAccess.InviteOnly + // All other cases are not supported so we default to InviteOnly is JoinRule.Custom, - JoinRule.Invite, JoinRule.Private, null -> SecurityAndPrivacyRoomAccess.InviteOnly } } -private fun SecurityAndPrivacyRoomAccess.map(): JoinRule { +private fun SecurityAndPrivacyRoomAccess.map(): JoinRule? { return when (this) { SecurityAndPrivacyRoomAccess.Anyone -> JoinRule.Public SecurityAndPrivacyRoomAccess.AskToJoin -> JoinRule.Knock SecurityAndPrivacyRoomAccess.InviteOnly -> JoinRule.Private - SecurityAndPrivacyRoomAccess.SpaceMember -> error("Unsupported") + // SpaceMember can't be selected in the ui + SecurityAndPrivacyRoomAccess.SpaceMember -> null } } @@ -260,7 +263,8 @@ private fun RoomHistoryVisibility?.map(): SecurityAndPrivacyHistoryVisibility { RoomHistoryVisibility.WorldReadable -> SecurityAndPrivacyHistoryVisibility.Anyone RoomHistoryVisibility.Joined, RoomHistoryVisibility.Invited -> SecurityAndPrivacyHistoryVisibility.SinceInvite - RoomHistoryVisibility.Shared, + RoomHistoryVisibility.Shared -> SecurityAndPrivacyHistoryVisibility.SinceSelection + // All other cases are not supported so we default to SinceSelection is RoomHistoryVisibility.Custom, null -> SecurityAndPrivacyHistoryVisibility.SinceSelection } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt index 75b53bbd58d..eb22ec25972 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt @@ -18,7 +18,7 @@ data class SecurityAndPrivacyState( // the settings the user wants to apply. val editedSettings: SecurityAndPrivacySettings, val homeserverName: String, - val showEncryptionConfirmation: Boolean, + val showEnableEncryptionConfirmation: Boolean, val saveAction: AsyncAction, private val permissions: SecurityAndPrivacyPermissions, val eventSink: (SecurityAndPrivacyEvents) -> Unit diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt index 7fe1fea52f6..907424108f5 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt @@ -79,7 +79,7 @@ fun aSecurityAndPrivacyState( editedSettings = editedSettings, savedSettings = savedSettings, homeserverName = homeserverName, - showEncryptionConfirmation = showEncryptionConfirmation, + showEnableEncryptionConfirmation = showEncryptionConfirmation, saveAction = saveAction, permissions = permissions, eventSink = eventSink diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt index c58687fde40..59dfeeaca01 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt @@ -100,9 +100,10 @@ fun SecurityAndPrivacyView( if (state.showEncryptionSection) { EncryptionSection( isRoomEncrypted = state.editedSettings.isEncrypted, + // encryption can't be disabled once enabled canToggleEncryption = !state.savedSettings.isEncrypted, onToggleEncryption = { state.eventSink(SecurityAndPrivacyEvents.ToggleEncryptionState) }, - showConfirmation = state.showEncryptionConfirmation, + showConfirmation = state.showEnableEncryptionConfirmation, onDismissConfirmation = { state.eventSink(SecurityAndPrivacyEvents.CancelEnableEncryption) }, onConfirmEncryption = { state.eventSink(SecurityAndPrivacyEvents.ConfirmEnableEncryption) }, ) @@ -206,6 +207,7 @@ private fun RoomAccessSection( trailingContent = ListItemContent.RadioButton(selected = edited == SecurityAndPrivacyRoomAccess.Anyone), onClick = { onSelectOption(SecurityAndPrivacyRoomAccess.Anyone) }, ) + // Show space member option, but disabled as we don't support this option for now. if (saved == SecurityAndPrivacyRoomAccess.SpaceMember) { ListItem( headlineContent = { Text(text = stringResource(R.string.screen_security_and_privacy_room_access_space_members_option_title)) }, @@ -342,7 +344,6 @@ private fun HistoryVisibilitySection( title = stringResource(R.string.screen_security_and_privacy_room_history_section_header), modifier = modifier, ) { - Spacer(Modifier.height(16.dp)) for (availableOption in availableOptions) { val isSelected = availableOption == editedOption HistoryVisibilityItem( @@ -351,6 +352,7 @@ private fun HistoryVisibilitySection( onSelectOption = onSelectOption, ) } + // Also show the saved option if it's not in the available options, but disabled if (savedOptions != null && !availableOptions.contains(savedOptions)) { HistoryVisibilityItem( option = savedOptions, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt index 884034ca1e6..153b751d080 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt @@ -118,12 +118,10 @@ class EditRoomAddressPresenter @AssistedInject constructor( // Otherwise, only update the alternative aliases and keep the current canonical alias else -> { val newAlternativeAliases = buildList { + // New alias is added first, so we make sure we pick it first add(newRoomAlias) - for (alias in room.alternativeAliases) { - if (alias != savedAliasFromHomeserver) { - add(alias) - } - } + // Add all other aliases, except the one we just removed from the room directory + addAll(room.alternativeAliases.filter { it != savedAliasFromHomeserver }) } room.updateCanonicalAlias(savedCanonicalAlias, newAlternativeAliases).getOrThrow() } @@ -141,5 +139,5 @@ private fun MatrixRoom.firstAliasMatching(serverName: String): RoomAlias? { if (canonicalAlias?.matchesServer(serverName) == true) { return canonicalAlias } - return alternativeAliases.firstOrNull { it.value.contains(serverName) } + return alternativeAliases.firstOrNull { it.matchesServer(serverName) } } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/SecurityAndPrivacyPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/SecurityAndPrivacyPresenterTest.kt index 6d1afe75826..1618e15b882 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/SecurityAndPrivacyPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/SecurityAndPrivacyPresenterTest.kt @@ -37,7 +37,7 @@ class SecurityAndPrivacyPresenterTest { with(awaitItem()) { assertThat(editedSettings).isEqualTo(savedSettings) assertThat(canBeSaved).isFalse() - assertThat(showEncryptionConfirmation).isFalse() + assertThat(showEnableEncryptionConfirmation).isFalse() assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized) assertThat(showRoomAccessSection).isFalse() assertThat(showRoomVisibilitySections).isFalse() @@ -47,7 +47,7 @@ class SecurityAndPrivacyPresenterTest { with(awaitItem()) { assertThat(editedSettings).isEqualTo(savedSettings) assertThat(canBeSaved).isFalse() - assertThat(showEncryptionConfirmation).isFalse() + assertThat(showEnableEncryptionConfirmation).isFalse() assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized) assertThat(showRoomAccessSection).isTrue() assertThat(showRoomVisibilitySections).isFalse() @@ -138,21 +138,21 @@ class SecurityAndPrivacyPresenterTest { eventSink(SecurityAndPrivacyEvents.ToggleEncryptionState) } with(awaitItem()) { - assertThat(showEncryptionConfirmation).isTrue() + assertThat(showEnableEncryptionConfirmation).isTrue() eventSink(SecurityAndPrivacyEvents.CancelEnableEncryption) } with(awaitItem()) { - assertThat(showEncryptionConfirmation).isFalse() + assertThat(showEnableEncryptionConfirmation).isFalse() eventSink(SecurityAndPrivacyEvents.ToggleEncryptionState) } with(awaitItem()) { - assertThat(showEncryptionConfirmation).isTrue() + assertThat(showEnableEncryptionConfirmation).isTrue() eventSink(SecurityAndPrivacyEvents.ConfirmEnableEncryption) } skipItems(1) with(awaitItem()) { assertThat(editedSettings.isEncrypted).isTrue() - assertThat(showEncryptionConfirmation).isFalse() + assertThat(showEnableEncryptionConfirmation).isFalse() assertThat(canBeSaved).isTrue() eventSink(SecurityAndPrivacyEvents.ToggleEncryptionState) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 6dd27defee9..a5640ff1d62 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -428,9 +428,7 @@ interface MatrixRoom : Closeable { /** * Returns the visibility for this room in the room directory. - * - * [Public](`RoomVisibility::Public`) rooms are listed in the room - * directory and can be found using it. + * If the room is not published, the result will be [RoomVisibility.Private]. */ suspend fun getRoomVisibility(): Result From 15f2d0bf83e4246c8f79d6eeebedbcf00dd8be33 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 29 Jan 2025 16:47:39 +0100 Subject: [PATCH 29/30] feat(security&privacy) : add more previews --- .../SecurityAndPrivacyStateProvider.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt index 907424108f5..00001ff69db 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt @@ -32,6 +32,12 @@ open class SecurityAndPrivacyStateProvider : PreviewParameterProvider Date: Wed, 29 Jan 2025 16:01:07 +0000 Subject: [PATCH 30/30] Update screenshots --- ...mpl.securityandprivacy_SecurityAndPrivacyViewDark_0_en.png | 4 ++-- ...mpl.securityandprivacy_SecurityAndPrivacyViewDark_1_en.png | 4 ++-- ...mpl.securityandprivacy_SecurityAndPrivacyViewDark_2_en.png | 4 ++-- ...mpl.securityandprivacy_SecurityAndPrivacyViewDark_4_en.png | 4 ++-- ...mpl.securityandprivacy_SecurityAndPrivacyViewDark_5_en.png | 4 ++-- ...mpl.securityandprivacy_SecurityAndPrivacyViewDark_6_en.png | 4 ++-- ...mpl.securityandprivacy_SecurityAndPrivacyViewDark_7_en.png | 3 +++ ...mpl.securityandprivacy_SecurityAndPrivacyViewDark_8_en.png | 3 +++ ...pl.securityandprivacy_SecurityAndPrivacyViewLight_0_en.png | 4 ++-- ...pl.securityandprivacy_SecurityAndPrivacyViewLight_1_en.png | 4 ++-- ...pl.securityandprivacy_SecurityAndPrivacyViewLight_2_en.png | 4 ++-- ...pl.securityandprivacy_SecurityAndPrivacyViewLight_4_en.png | 4 ++-- ...pl.securityandprivacy_SecurityAndPrivacyViewLight_5_en.png | 4 ++-- ...pl.securityandprivacy_SecurityAndPrivacyViewLight_6_en.png | 4 ++-- ...pl.securityandprivacy_SecurityAndPrivacyViewLight_7_en.png | 3 +++ ...pl.securityandprivacy_SecurityAndPrivacyViewLight_8_en.png | 3 +++ 16 files changed, 36 insertions(+), 24 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_7_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_8_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_7_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_8_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_0_en.png index 60363c684ed..6ca9adcd12e 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a5339ef524ed3bb5bd2399f3467afd35f22d2f44d64e923af76557f31b11663 -size 39675 +oid sha256:9f0d221bd5db5a13c9ba07a35ab9bb75d62e441c33941cf0012bf1a7e29d8b0d +size 39650 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_1_en.png index a1d49291969..61704495fbd 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8b8e7540627707d399579100fb807b170a1291f58aaa58218f23d4c83b27aafc -size 61755 +oid sha256:e0ddf4bd9f8571350c5ed4aee212950a399f65484434bd3d83742fba75fd47f1 +size 62264 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_2_en.png index fc75b939ab1..5757ef8ce3a 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9addb6800b01883a97c2ce59f9680298ca541b2ac3cbab72dd152531c547ab3b -size 61890 +oid sha256:3f02a50e9a947a59b0ff52a8c02e4fb9a2e254f5f85c5965ec9786c5fd38f4a6 +size 62401 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_4_en.png index f8112e51132..be877d6e29a 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ca121a0983d794ba2cea29016edb68c2e361fdf50f9bcba286f433a47f08e06 -size 39464 +oid sha256:ddef290ff73c2b824a586a589ac4dffb251830ace9114492aad6bca984e44c39 +size 62755 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_5_en.png index f8112e51132..ad88c5f4bda 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ca121a0983d794ba2cea29016edb68c2e361fdf50f9bcba286f433a47f08e06 -size 39464 +oid sha256:7efc03a5388ae80383d4b60426f8664a5c54fd3c0a7e4d04bedf9280e8bba605 +size 39438 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_6_en.png index 462134421f8..ad88c5f4bda 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:249d48df6f0b7e8b0ba8cced66edb29a0b2679c364cf99cfecf8672ecd4d5a87 -size 43716 +oid sha256:7efc03a5388ae80383d4b60426f8664a5c54fd3c0a7e4d04bedf9280e8bba605 +size 39438 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_7_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_7_en.png new file mode 100644 index 00000000000..b2b93cf5717 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8146f71450291e3164b2bd64407a57a1fd9282f0a36335f6a217ffa47b71e855 +size 43720 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_8_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_8_en.png new file mode 100644 index 00000000000..b6fd175cef7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:902d7aabd41c431a16cd9dfeb0ebbca3998a0f3ce7c81ce296edb1716d94288e +size 33927 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_0_en.png index c0b4d011f05..9bc6ba88526 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a713418b41e03da12fca6c65895e8928074af4cf9a2599e81124b0cf34f668c -size 41217 +oid sha256:2b31ed0dcf7db120ce77099c26c55196be64354f0bec2f80ae5176ac85194629 +size 41188 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_1_en.png index 05575fa95ed..020f73a5478 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:83d8d1eb701b4b557993fa0691f3a465e6ebe5c76db6ce4d205673a957384480 -size 63708 +oid sha256:c1031ba27d74bda6dbf76f1d81494a06c485d75fac709ea49edfcae66d123f31 +size 64290 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_2_en.png index 27b2609efc5..bc4b39abf29 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e2f287ae525408a680edf525cbd4142ceddd8c33fda00cadb2ca6b69906a6399 -size 63937 +oid sha256:141c74d7629662ef323fa12f558fec221204087e0da9b08a72ac06b451fbaeb6 +size 64522 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_4_en.png index 0c4fd8df988..f0c97e30d9d 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6f137fd2f9280a6e023992fc149a0bd4cdf1cc82dc5bd5676fe3df825054cf66 -size 41046 +oid sha256:eeda9bd51bd1baec53af36d4102fd0264595710d2758807307d5abf5324b7262 +size 64828 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_5_en.png index 0c4fd8df988..d2ba0408e56 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6f137fd2f9280a6e023992fc149a0bd4cdf1cc82dc5bd5676fe3df825054cf66 -size 41046 +oid sha256:3f1a5367fe47330eb9add526cc4d4e437ae2fe7e21aaafe95a8759fd918bb896 +size 41017 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_6_en.png index 16e006428d8..d2ba0408e56 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b084c7f1dc01dbb08eddd6d2197d7e7f679258700433816a869262115326432b -size 45893 +oid sha256:3f1a5367fe47330eb9add526cc4d4e437ae2fe7e21aaafe95a8759fd918bb896 +size 41017 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_7_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_7_en.png new file mode 100644 index 00000000000..20e6e5ff1e2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf3619665bc8956d2b4bfc194464e95ff4ea1d397df4a17cc39e9c34d6751da1 +size 45886 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_8_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_8_en.png new file mode 100644 index 00000000000..95ea837744c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7fe40c0ee7536a8bbe308f786d8147f6b4482d35e6be6ee82801690cfec44ec8 +size 35160