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..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 @@ -31,10 +30,11 @@ 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.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> @@ -191,7 +161,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 +174,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/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/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/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/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/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt index 0393cae1e8e..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,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.SecurityAndPrivacyFlowNode 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/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index 4758f4d77a8..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 @@ -24,6 +23,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 @@ -104,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) { @@ -147,10 +147,17 @@ class RoomDetailsPresenter @Inject constructor( val roomMemberDetailsState = roomMemberDetailsPresenter?.present() + val securityAndPrivacyPermissions = room.securityAndPrivacyPermissionsAsState(syncUpdateFlow.value) + val canShowSecurityAndPrivacy by remember { + derivedStateOf { + isKnockRequestsEnabled && roomType is RoomDetailsType.Room && securityAndPrivacyPermissions.value.hasAny + } + } + return RoomDetailsState( roomId = room.roomId, roomName = roomName, - roomAlias = room.alias, + roomAlias = room.canonicalAlias, roomAvatarUrl = roomAvatar, roomTopic = topicState, memberCount = room.joinedMemberCount, @@ -172,6 +179,7 @@ class RoomDetailsPresenter @Inject constructor( pinnedMessagesCount = pinnedMessagesCount, canShowKnockRequests = canShowKnockRequests, knockRequestsCount = knockRequestsCount, + canShowSecurityAndPrivacy = canShowSecurityAndPrivacy, eventSink = ::handleEvents, ) } @@ -187,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/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 a5e5d1872c0..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 @@ -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( @@ -184,25 +183,14 @@ fun RoomDetailsView( state.eventSink(RoomDetailsEvent.SetFavorite(it)) } ) - - 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, + if (state.canShowSecurityAndPrivacy) { + 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 +202,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 +408,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 +520,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 +584,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 +654,6 @@ private fun ContentToPreview(state: RoomDetailsState) { onJoinCallClick = {}, onPinnedMessagesClick = {}, onKnockRequestsClick = {}, + onSecurityAndPrivacyClick = {}, ) } 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..5e09405998c --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt @@ -0,0 +1,20 @@ +/* + * 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 EditRoomAddress : SecurityAndPrivacyEvents + data object Save : SecurityAndPrivacyEvents + data class ChangeRoomAccess(val roomAccess: SecurityAndPrivacyRoomAccess) : SecurityAndPrivacyEvents + data object ToggleEncryptionState : SecurityAndPrivacyEvents + data object CancelEnableEncryption : 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 new file mode 100644 index 00000000000..22fecf6143c --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyFlowNode.kt @@ -0,0 +1,64 @@ +/* + * 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 + } + + private val navigator = BackstackSecurityAndPrivacyNavigator(backstack) + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return when (navTarget) { + NavTarget.SecurityAndPrivacy -> { + createNode(buildContext, plugins = listOf(navigator)) + } + NavTarget.EditRoomAddress -> { + createNode(buildContext, plugins = listOf(navigator)) + } + } + } + + @Composable + override fun View(modifier: Modifier) { + BackstackView(modifier) + } +} 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..ade6479fd9a --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNavigator.kt @@ -0,0 +1,30 @@ +/* + * 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.pop +import com.bumble.appyx.navmodel.backstack.operation.push + +interface SecurityAndPrivacyNavigator : Plugin { + fun openEditRoomAddress() + fun closeEditRoomAddress() +} + +class BackstackSecurityAndPrivacyNavigator( + private val backStack: BackStack +) : SecurityAndPrivacyNavigator { + override fun openEditRoomAddress() { + backStack.push(SecurityAndPrivacyFlowNode.NavTarget.EditRoomAddress) + } + + override fun closeEditRoomAddress() { + backStack.pop() + } +} 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..537306f44f2 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNode.kt @@ -0,0 +1,39 @@ +/* + * 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 com.bumble.appyx.core.plugin.plugins +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, + 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 = 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 new file mode 100644 index 00000000000..769ff87baee --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt @@ -0,0 +1,283 @@ +/* + * 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.LaunchedEffect +import androidx.compose.runtime.MutableState +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.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 +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 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 + +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 { + val coroutineScope = rememberCoroutineScope() + + val saveAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } + val homeserverName = remember { matrixClient.userIdServerName() } + val syncUpdateFlow = room.syncUpdateFlow.collectAsState() + val roomInfo = room.roomInfoFlow.collectAsState(null) + + val savedIsVisibleInRoomDirectory = remember { mutableStateOf>(AsyncData.Uninitialized) } + LaunchedEffect(Unit) { + isRoomVisibleInRoomDirectory(savedIsVisibleInRoomDirectory) + } + + val savedSettings by remember { + derivedStateOf { + SecurityAndPrivacySettings( + roomAccess = roomInfo.value?.joinRule.map(), + isEncrypted = room.isEncrypted, + isVisibleInRoomDirectory = savedIsVisibleInRoomDirectory.value, + historyVisibility = roomInfo.value?.historyVisibility.map(), + address = roomInfo.value?.firstDisplayableAlias(homeserverName)?.value, + ) + } + } + + var editedRoomAccess by remember(savedSettings.roomAccess) { + mutableStateOf(savedSettings.roomAccess) + } + var editedHistoryVisibility by remember(savedSettings.historyVisibility) { + mutableStateOf(savedSettings.historyVisibility) + } + var editedIsEncrypted by remember(savedSettings.isEncrypted) { + mutableStateOf(savedSettings.isEncrypted) + } + var editedVisibleInRoomDirectory by remember(savedIsVisibleInRoomDirectory.value) { + mutableStateOf(savedIsVisibleInRoomDirectory.value) + } + val editedSettings = SecurityAndPrivacySettings( + roomAccess = editedRoomAccess, + isEncrypted = editedIsEncrypted, + isVisibleInRoomDirectory = editedVisibleInRoomDirectory, + historyVisibility = editedHistoryVisibility, + address = savedSettings.address, + ) + + var showEnableEncryptionConfirmation by remember(savedSettings.isEncrypted) { mutableStateOf(false) } + val permissions by room.securityAndPrivacyPermissionsAsState(syncUpdateFlow.value) + + fun handleEvents(event: SecurityAndPrivacyEvents) { + when (event) { + SecurityAndPrivacyEvents.Save -> { + coroutineScope.save( + saveAction = saveAction, + isVisibleInRoomDirectory = savedIsVisibleInRoomDirectory, + savedSettings = savedSettings, + editedSettings = editedSettings + ) + } + is SecurityAndPrivacyEvents.ChangeRoomAccess -> { + editedRoomAccess = event.roomAccess + } + is SecurityAndPrivacyEvents.ToggleEncryptionState -> { + if (editedIsEncrypted) { + editedIsEncrypted = false + } else { + showEnableEncryptionConfirmation = true + } + } + is SecurityAndPrivacyEvents.ChangeHistoryVisibility -> { + editedHistoryVisibility = event.historyVisibility + } + SecurityAndPrivacyEvents.ToggleRoomVisibility -> { + editedVisibleInRoomDirectory = when (val edited = editedVisibleInRoomDirectory) { + is AsyncData.Success -> AsyncData.Success(!edited.data) + else -> edited + } + } + SecurityAndPrivacyEvents.EditRoomAddress -> navigator.openEditRoomAddress() + SecurityAndPrivacyEvents.CancelEnableEncryption -> { + showEnableEncryptionConfirmation = false + } + SecurityAndPrivacyEvents.ConfirmEnableEncryption -> { + showEnableEncryptionConfirmation = false + editedIsEncrypted = true + } + SecurityAndPrivacyEvents.DismissSaveError -> { + saveAction.value = AsyncAction.Uninitialized + } + } + } + + val state = SecurityAndPrivacyState( + savedSettings = savedSettings, + editedSettings = editedSettings, + homeserverName = homeserverName, + showEnableEncryptionConfirmation = showEnableEncryptionConfirmation, + saveAction = saveAction.value, + permissions = permissions, + eventSink = ::handleEvents + ) + + // If the history visibility is not available for the current access, use the fallback. + LaunchedEffect(state.availableHistoryVisibilities) { + if (editedSettings.historyVisibility !in state.availableHistoryVisibilities) { + editedHistoryVisibility = editedSettings.historyVisibility.fallback() + } + } + return state + } + + private fun CoroutineScope.isRoomVisibleInRoomDirectory(isRoomVisible: MutableState>) = launch { + isRoomVisible.runUpdatingState { + room.getRoomVisibility().map { it == RoomVisibility.Public } + } + } + + private fun CoroutineScope.save( + saveAction: MutableState>, + isVisibleInRoomDirectory: MutableState>, + savedSettings: SecurityAndPrivacySettings, + editedSettings: SecurityAndPrivacySettings, + ) = launch { + suspend { + val enableEncryption = async { + if (editedSettings.isEncrypted && !savedSettings.isEncrypted) { + room.enableEncryption() + } else { + Result.success(Unit) + } + } + val updateHistoryVisibility = async { + if (editedSettings.historyVisibility != savedSettings.historyVisibility) { + room.updateHistoryVisibility(editedSettings.historyVisibility.map()) + } else { + Result.success(Unit) + } + } + val updateJoinRule = async { + val joinRule = editedSettings.roomAccess.map() + if (editedSettings.roomAccess != savedSettings.roomAccess && joinRule != null) { + room.updateJoinRule(joinRule) + } else { + Result.success(Unit) + } + } + 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 artificialDelay = async { + // Artificial delay to make sure the user sees the loading state + delay(500) + Result.success(Unit) + } + 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 + JoinRule.Invite -> SecurityAndPrivacyRoomAccess.InviteOnly + // All other cases are not supported so we default to InviteOnly + is JoinRule.Custom, + 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 + // SpaceMember can't be selected in the ui + SecurityAndPrivacyRoomAccess.SpaceMember -> null + } +} + +private fun RoomHistoryVisibility?.map(): SecurityAndPrivacyHistoryVisibility { + return when (this) { + RoomHistoryVisibility.WorldReadable -> SecurityAndPrivacyHistoryVisibility.Anyone + RoomHistoryVisibility.Joined, + RoomHistoryVisibility.Invited -> SecurityAndPrivacyHistoryVisibility.SinceInvite + RoomHistoryVisibility.Shared -> SecurityAndPrivacyHistoryVisibility.SinceSelection + // All other cases are not supported so we default to SinceSelection + 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() +} 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..eb22ec25972 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt @@ -0,0 +1,77 @@ +/* + * 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.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. + val savedSettings: SecurityAndPrivacySettings, + // the settings the user wants to apply. + val editedSettings: SecurityAndPrivacySettings, + val homeserverName: String, + val showEnableEncryptionConfirmation: 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) { + add(SecurityAndPrivacyHistoryVisibility.Anyone) + } 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 +} + +data class SecurityAndPrivacySettings( + val roomAccess: SecurityAndPrivacyRoomAccess, + val isEncrypted: Boolean, + val historyVisibility: SecurityAndPrivacyHistoryVisibility, + val address: String?, + val isVisibleInRoomDirectory: AsyncData +) + +enum class SecurityAndPrivacyHistoryVisibility { + SinceSelection, + SinceInvite, + Anyone; + + /** + * Returns the fallback visibility when the current visibility is not available. + */ + fun fallback(): SecurityAndPrivacyHistoryVisibility { + return when (this) { + SinceSelection, + SinceInvite -> SinceSelection + Anyone -> SinceInvite + } + } +} + +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 new file mode 100644 index 00000000000..00001ff69db --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt @@ -0,0 +1,95 @@ +/* + * 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.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 + +open class SecurityAndPrivacyStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aSecurityAndPrivacyState(), + aSecurityAndPrivacyState( + editedSettings = aSecurityAndPrivacySettings( + roomAccess = SecurityAndPrivacyRoomAccess.AskToJoin + ) + ), + aSecurityAndPrivacyState( + editedSettings = aSecurityAndPrivacySettings( + roomAccess = SecurityAndPrivacyRoomAccess.Anyone, + isEncrypted = false, + ) + ), + aSecurityAndPrivacyState( + savedSettings = aSecurityAndPrivacySettings( + roomAccess = SecurityAndPrivacyRoomAccess.SpaceMember + ) + ), + aSecurityAndPrivacyState( + editedSettings = aSecurityAndPrivacySettings( + roomAccess = SecurityAndPrivacyRoomAccess.Anyone, + address = "#therapy:myserver.xyz" + ) + ), + aSecurityAndPrivacyState( + editedSettings = aSecurityAndPrivacySettings( + isVisibleInRoomDirectory = AsyncData.Loading() + ) + ), + aSecurityAndPrivacyState( + editedSettings = aSecurityAndPrivacySettings( + isVisibleInRoomDirectory = AsyncData.Success(true) + ) + ), + aSecurityAndPrivacyState( + showEncryptionConfirmation = true + ), + aSecurityAndPrivacyState( + saveAction = AsyncAction.Loading + ), + ) +} + +fun aSecurityAndPrivacySettings( + roomAccess: SecurityAndPrivacyRoomAccess = SecurityAndPrivacyRoomAccess.InviteOnly, + isEncrypted: Boolean = true, + address: String? = null, + historyVisibility: SecurityAndPrivacyHistoryVisibility = SecurityAndPrivacyHistoryVisibility.SinceSelection, + isVisibleInRoomDirectory: AsyncData = AsyncData.Uninitialized, +) = SecurityAndPrivacySettings( + roomAccess = roomAccess, + isEncrypted = isEncrypted, + address = address, + historyVisibility = historyVisibility, + isVisibleInRoomDirectory = isVisibleInRoomDirectory +) + +fun aSecurityAndPrivacyState( + savedSettings: SecurityAndPrivacySettings = aSecurityAndPrivacySettings(), + editedSettings: SecurityAndPrivacySettings = savedSettings, + 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, + savedSettings = savedSettings, + homeserverName = homeserverName, + 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 new file mode 100644 index 00000000000..59dfeeaca01 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt @@ -0,0 +1,406 @@ +/* + * 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.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 +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 kotlinx.collections.immutable.ImmutableSet + +@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), + ) { + if (state.showRoomAccessSection) { + RoomAccessSection( + modifier = Modifier.padding(top = 24.dp), + edited = state.editedSettings.roomAccess, + saved = state.savedSettings.roomAccess, + onSelectOption = { state.eventSink(SecurityAndPrivacyEvents.ChangeRoomAccess(it)) }, + ) + } + if (state.showRoomVisibilitySections) { + RoomVisibilitySection(state.homeserverName) + RoomAddressSection( + roomAddress = state.editedSettings.address, + homeserverName = state.homeserverName, + onRoomAddressClick = { state.eventSink(SecurityAndPrivacyEvents.EditRoomAddress) }, + isVisibleInRoomDirectory = state.editedSettings.isVisibleInRoomDirectory, + onVisibilityChange = { + state.eventSink(SecurityAndPrivacyEvents.ToggleRoomVisibility) + }, + ) + } + 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.showEnableEncryptionConfirmation, + 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, + onSelectOption = { 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) +@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( + edited: SecurityAndPrivacyRoomAccess, + saved: SecurityAndPrivacyRoomAccess, + onSelectOption: (SecurityAndPrivacyRoomAccess) -> Unit, + modifier: Modifier = Modifier, +) { + SecurityAndPrivacySection( + title = stringResource(R.string.screen_security_and_privacy_room_access_section_header), + modifier = modifier, + ) { + ListItem( + 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 = { 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 = { 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 = { onSelectOption(SecurityAndPrivacyRoomAccess.Anyone) }, + ) + // Show space member option, but disabled as we don't support this option for now. + if (saved == SecurityAndPrivacyRoomAccess.SpaceMember) { + ListItem( + headlineContent = { Text(text = stringResource(R.string.screen_security_and_privacy_room_access_space_members_option_title)) }, + supportingContent = { Text(text = stringResource(R.string.screen_security_and_privacy_room_access_space_members_option_description)) }, + trailingContent = ListItemContent.RadioButton(selected = true, enabled = false), + enabled = false, + ) + } + } +} + +@Composable +private fun RoomVisibilitySection( + homeserverName: String, + modifier: Modifier = Modifier, +) { + SecurityAndPrivacySection( + title = stringResource(R.string.screen_security_and_privacy_room_visibility_section_header), + modifier = modifier, + ) { + Spacer(Modifier.height(12.dp)) + Text( + 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), + ) + } +} + +@Composable +private fun RoomAddressSection( + roomAddress: String?, + homeserverName: String, + isVisibleInRoomDirectory: AsyncData, + onRoomAddressClick: () -> Unit, + onVisibilityChange: () -> Unit, + modifier: Modifier = Modifier, +) { + SecurityAndPrivacySection( + title = stringResource(R.string.screen_security_and_privacy_room_address_section_header), + modifier = modifier, + ) { + ListItem( + headlineContent = { + 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(R.string.screen_security_and_privacy_room_address_section_footer)) }, + onClick = onRoomAddressClick, + colors = ListItemDefaults.colors(trailingIconColor = ElementTheme.colors.iconAccentPrimary), + ) + + ListItem( + headlineContent = { Text(text = stringResource(R.string.screen_security_and_privacy_room_directory_visibility_toggle_title)) }, + 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 -> { + 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, + canToggleEncryption: Boolean, + showConfirmation: Boolean, + onToggleEncryption: () -> Unit, + onConfirmEncryption: () -> Unit, + onDismissConfirmation: () -> Unit, + modifier: Modifier = Modifier, +) { + SecurityAndPrivacySection( + title = stringResource(R.string.screen_security_and_privacy_encryption_section_header), + modifier = modifier, + ) { + ListItem( + 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, + onChange = { onToggleEncryption() }, + ), + onClick = if (canToggleEncryption) onToggleEncryption else null + ) + } + if (showConfirmation) { + ConfirmationDialog( + 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, + ) + } +} + +@Composable +private fun HistoryVisibilitySection( + editedOption: SecurityAndPrivacyHistoryVisibility?, + savedOptions: SecurityAndPrivacyHistoryVisibility?, + availableOptions: ImmutableSet, + onSelectOption: (SecurityAndPrivacyHistoryVisibility) -> Unit, + modifier: Modifier = Modifier, +) { + SecurityAndPrivacySection( + title = stringResource(R.string.screen_security_and_privacy_room_history_section_header), + modifier = modifier, + ) { + for (availableOption in availableOptions) { + val isSelected = availableOption == editedOption + HistoryVisibilityItem( + option = availableOption, + isSelected = isSelected, + 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, + isSelected = true, + isEnabled = false, + onSelectOption = {}, + ) + } + } +} + +@Composable +private fun HistoryVisibilityItem( + option: SecurityAndPrivacyHistoryVisibility, + isSelected: Boolean, + onSelectOption: (SecurityAndPrivacyHistoryVisibility) -> Unit, + modifier: Modifier = Modifier, + isEnabled: Boolean = true, +) { + val headlineText = when (option) { + 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) }, + trailingContent = ListItemContent.RadioButton(selected = isSelected, enabled = isEnabled), + onClick = { onSelectOption(option) }, + enabled = isEnabled, + modifier = modifier, + ) +} + +@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 = {}, + ) +} 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..485a9d2fa03 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressEvents.kt @@ -0,0 +1,14 @@ +/* + * 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 + 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 new file mode 100644 index 00000000000..efdae76b61d --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressNode.kt @@ -0,0 +1,40 @@ +/* + * 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 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, + 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() + 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 new file mode 100644 index 00000000000..153b751d080 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt @@ -0,0 +1,143 @@ +/* + * 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.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 kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +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) + } + val savedRoomAddress = remember { room.firstAliasMatching(homeserverName)?.addressName() } + val saveAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } + var newRoomAddress by remember { + mutableStateOf( + savedRoomAddress ?: roomAliasHelper.roomAliasNameFromRoomDisplayName(room.displayName) + ) + } + + fun handleEvents(event: EditRoomAddressEvents) { + when (event) { + EditRoomAddressEvents.Save -> coroutineScope.save( + saveAction = saveAction, + serverName = homeserverName, + newRoomAddress = newRoomAddress + ) + is EditRoomAddressEvents.RoomAddressChanged -> { + newRoomAddress = event.roomAddress + } + EditRoomAddressEvents.DismissError -> { + saveAction.value = AsyncAction.Uninitialized + } + } + } + + RoomAddressValidityEffect( + client = client, + roomAliasHelper = roomAliasHelper, + newRoomAddress = newRoomAddress, + knownRoomAddress = savedRoomAddress + ) { newRoomAddressValidity -> + roomAddressValidity.value = newRoomAddressValidity + } + + return EditRoomAddressState( + homeserverName = homeserverName, + roomAddressValidity = roomAddressValidity.value, + 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) -> { + val newAlternativeAliases = room.alternativeAliases.filter { it != savedAliasFromHomeserver } + room.updateCanonicalAlias(newRoomAlias, newAlternativeAliases).getOrThrow() + } + // 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) + // 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() + } + } + navigator.closeEditRoomAddress() + }.runCatchingUpdatingState(saveAction) + } +} + +/** + * 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) { + return canonicalAlias + } + return alternativeAliases.firstOrNull { it.matchesServer(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 new file mode 100644 index 00000000000..9bc256daba1 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressState.kt @@ -0,0 +1,21 @@ +/* + * 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.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 new file mode 100644 index 00000000000..6ef8658bcdc --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressStateProvider.kt @@ -0,0 +1,37 @@ +/* + * 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 +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( + anEditRoomAddressState(), + anEditRoomAddressState(roomAddressValidity = RoomAddressValidity.NotAvailable), + anEditRoomAddressState(roomAddressValidity = RoomAddressValidity.InvalidSymbols), + anEditRoomAddressState(roomAddressValidity = RoomAddressValidity.Valid), + anEditRoomAddressState(roomAddressValidity = RoomAddressValidity.Valid, saveAction = AsyncAction.Loading), + ) +} + +fun anEditRoomAddressState( + 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 new file mode 100644 index 00000000000..d39208ee955 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressView.kt @@ -0,0 +1,128 @@ +/* + * 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.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.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.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 +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, +) { + 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(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) + ) + } + 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) }, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun EditRoomAddressTopBar( + isSaveActionEnabled: Boolean, + onBackClick: () -> Unit, + onSaveClick: () -> Unit, + modifier: Modifier = Modifier, +) { + TopAppBar( + modifier = modifier, + title = { + Text( + text = stringResource(R.string.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( + @PreviewParameter(EditRoomAddressStateProvider::class) state: EditRoomAddressState +) = ElementPreview { + EditRoomAddressView( + state = state, + onBackClick = {}, + ) +} 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 +} 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..50a676da1e4 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/permissions/SecurityAndPrivacyPermissions.kt @@ -0,0 +1,49 @@ +/* + * 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 }, + ) + } +} diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml index dea48c90b5e..62a7e482954 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" @@ -118,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/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/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..e4dfbcdbbad --- /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 closeEditRoomAddressLambda: () -> Unit = { lambdaError() }, +) : SecurityAndPrivacyNavigator { + override fun openEditRoomAddress() { + openEditRoomAddressLambda() + } + + 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 new file mode 100644 index 00000000000..1618e15b882 --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/SecurityAndPrivacyPresenterTest.kt @@ -0,0 +1,348 @@ +/* + * 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(showEnableEncryptionConfirmation).isFalse() + assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized) + assertThat(showRoomAccessSection).isFalse() + assertThat(showRoomVisibilitySections).isFalse() + assertThat(showHistoryVisibilitySection).isFalse() + assertThat(showEncryptionSection).isFalse() + } + with(awaitItem()) { + assertThat(editedSettings).isEqualTo(savedSettings) + assertThat(canBeSaved).isFalse() + assertThat(showEnableEncryptionConfirmation).isFalse() + assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized) + assertThat(showRoomAccessSection).isTrue() + assertThat(showRoomVisibilitySections).isFalse() + assertThat(showHistoryVisibilitySection).isTrue() + assertThat(showEncryptionSection).isTrue() + } + } + } + + @Test + fun `present - room info change updates saved and edited settings`() = runTest { + val room = 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(showEnableEncryptionConfirmation).isTrue() + eventSink(SecurityAndPrivacyEvents.CancelEnableEncryption) + } + with(awaitItem()) { + assertThat(showEnableEncryptionConfirmation).isFalse() + eventSink(SecurityAndPrivacyEvents.ToggleEncryptionState) + } + with(awaitItem()) { + assertThat(showEnableEncryptionConfirmation).isTrue() + eventSink(SecurityAndPrivacyEvents.ConfirmEnableEncryption) + } + skipItems(1) + with(awaitItem()) { + assertThat(editedSettings.isEncrypted).isTrue() + assertThat(showEnableEncryptionConfirmation).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 - 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) } + 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/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, + ) + } +} 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..4c10fff350d --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/editroomaddress/EditRoomAddressPresenterTest.kt @@ -0,0 +1,354 @@ +/* + * 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..b5c3a8b3bd7 --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/editroomaddress/EditRoomAddressViewTest.kt @@ -0,0 +1,120 @@ +/* + * 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.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/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 4d5f8c7a193..1d8d1517163 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,13 @@ 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.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 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 @@ -43,7 +46,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? @@ -403,4 +406,60 @@ 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. + * If the room is not published, the result will be [RoomVisibility.Private]. + */ + 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 + + /** + * 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/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/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/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 911a62f376c..ef570f5ae32 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,14 @@ 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.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 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 +54,13 @@ 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.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 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 @@ -101,6 +107,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, @@ -306,7 +313,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 @@ -805,6 +812,54 @@ 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 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()) + } + } + + 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() + } + } + + 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/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..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 @@ -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 @@ -33,6 +34,7 @@ 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.RoomHistoryVisibility as RustRoomHistoryVisibility import org.matrix.rustcomponents.sdk.RoomNotificationMode as RustRoomNotificationMode class MatrixRoomInfoMapperTest { @@ -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 1fd95da36b7..d214f91f2b5 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,13 @@ 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.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 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 @@ -66,7 +69,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, @@ -145,6 +148,14 @@ 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() }, + 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 @@ -195,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) @@ -582,6 +595,34 @@ 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() + } + + override suspend fun publishRoomAliasInRoomDirectory(roomAlias: RoomAlias): Result = simulateLongTask { + publishRoomAliasInRoomDirectoryResult(roomAlias) + } + + override suspend fun removeRoomAliasFromRoomDirectory(roomAlias: RoomAlias): Result = simulateLongTask { + removeRoomAliasFromRoomDirectoryResult(roomAlias) + } + + override suspend fun updateJoinRule(joinRule: JoinRule): Result = simulateLongTask { + updateJoinRuleResult(joinRule) + } + 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, ) 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..acd7723b875 --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressField.kt @@ -0,0 +1,76 @@ +/* + * 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.testtags.TestTags +import io.element.android.libraries.testtags.testTag +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.testTag(TestTags.roomAddressField), + 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 +internal 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) + } + } + } +} 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..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 @@ -111,4 +111,10 @@ object TestTags { * Generic call to action. */ val callToAction = TestTag("call_to_action") + + /** + * Room address field. + * + */ + val roomAddressField = TestTag("room_address_field") } diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 26a87bde89a..2d6c7eea53c 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -310,8 +310,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." @@ -341,39 +339,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/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..6ca9adcd12e --- /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: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 new file mode 100644 index 00000000000..61704495fbd --- /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: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 new file mode 100644 index 00000000000..5757ef8ce3a --- /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:3f02a50e9a947a59b0ff52a8c02e4fb9a2e254f5f85c5965ec9786c5fd38f4a6 +size 62401 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..be877d6e29a --- /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: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 new file mode 100644 index 00000000000..ad88c5f4bda --- /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: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 new file mode 100644 index 00000000000..ad88c5f4bda --- /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: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 new file mode 100644 index 00000000000..9bc6ba88526 --- /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: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 new file mode 100644 index 00000000000..020f73a5478 --- /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: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 new file mode 100644 index 00000000000..bc4b39abf29 --- /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:141c74d7629662ef323fa12f558fec221204087e0da9b08a72ac06b451fbaeb6 +size 64522 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..f0c97e30d9d --- /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: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 new file mode 100644 index 00000000000..d2ba0408e56 --- /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: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 new file mode 100644 index 00000000000..d2ba0408e56 --- /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: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 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 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\\..*" ] }, {