diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt index 204c1a72e1..be076b9c52 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyEvents.kt @@ -16,4 +16,5 @@ sealed interface SecurityAndPrivacyEvents { data object ConfirmEnableEncryption: SecurityAndPrivacyEvents data class ChangeHistoryVisibility(val historyVisibility: SecurityAndPrivacyHistoryVisibility) : SecurityAndPrivacyEvents data class ChangeRoomVisibility(val isVisibleInRoomDirectory: Boolean) : SecurityAndPrivacyEvents + data object DismissSaveError : SecurityAndPrivacyEvents } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt index 257950e11a..8e5d678f09 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt @@ -8,25 +8,35 @@ package io.element.android.features.roomdetails.impl.securityandprivacy import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress.matchesServer +import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.runCatchingUpdatingState +import io.element.android.libraries.architecture.runUpdatingState import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility import io.element.android.libraries.matrix.api.room.join.JoinRule -import java.util.Optional +import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber class SecurityAndPrivacyPresenter @AssistedInject constructor( @Assisted private val navigator: SecurityAndPrivacyNavigator, @@ -40,67 +50,68 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor( @Composable override fun present(): SecurityAndPrivacyState { + val coroutineScope = rememberCoroutineScope() val homeserverName = remember { matrixClient.userIdServerName() } val roomInfo by room.roomInfoFlow.collectAsState(initial = null) - - val isVisibleInRoomDirectory = remember { - mutableStateOf>(AsyncData.Uninitialized) - } + val isVisibleInRoomDirectory by isRoomVisibleInRoomDirectory() val savedSettings by remember { derivedStateOf { SecurityAndPrivacySettings( roomAccess = roomInfo?.joinRule.map(), isEncrypted = room.isEncrypted, - isVisibleInRoomDirectory = Optional.ofNullable(isVisibleInRoomDirectory.value), - historyVisibility = Optional.ofNullable(roomInfo?.historyVisibility?.map()), - addressName = Optional.ofNullable(roomInfo?.firstDisplayableAlias(homeserverName)?.value), + isVisibleInRoomDirectory = isVisibleInRoomDirectory, + historyVisibility = roomInfo?.historyVisibility?.map(), + addressName = roomInfo?.firstDisplayableAlias(homeserverName)?.value ) } } - var currentRoomAccess by remember(savedSettings.roomAccess) { + var editedRoomAccess by remember(savedSettings.roomAccess) { mutableStateOf(savedSettings.roomAccess) } - var currentHistoryVisibility by remember(savedSettings.historyVisibility) { + var editedHistoryVisibility by remember(savedSettings.historyVisibility) { mutableStateOf(savedSettings.historyVisibility) } - var currentVisibleInRoomDirectory by remember(savedSettings.isVisibleInRoomDirectory) { + var editedVisibleInRoomDirectory by remember(savedSettings.isVisibleInRoomDirectory) { mutableStateOf(savedSettings.isVisibleInRoomDirectory) } - var currentIsEncrypted by remember(savedSettings.isEncrypted) { + var editedIsEncrypted by remember(savedSettings.isEncrypted) { mutableStateOf(savedSettings.isEncrypted) } - var showEncryptionConfirmation by remember { mutableStateOf(false) } + var showEncryptionConfirmation by remember(savedSettings.isEncrypted) { mutableStateOf(false) } - val currentSettings = SecurityAndPrivacySettings( - roomAccess = currentRoomAccess, - isEncrypted = currentIsEncrypted, - isVisibleInRoomDirectory = currentVisibleInRoomDirectory, - historyVisibility = currentHistoryVisibility, + val editedSettings = SecurityAndPrivacySettings( + roomAccess = editedRoomAccess, + isEncrypted = editedIsEncrypted, + isVisibleInRoomDirectory = editedVisibleInRoomDirectory, + historyVisibility = editedHistoryVisibility, addressName = savedSettings.addressName, ) + val saveAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } + fun handleEvents(event: SecurityAndPrivacyEvents) { when (event) { SecurityAndPrivacyEvents.Save -> { + coroutineScope.save(saveAction, savedSettings, editedSettings) } is SecurityAndPrivacyEvents.ChangeRoomAccess -> { - currentRoomAccess = event.roomAccess + editedRoomAccess = event.roomAccess } is SecurityAndPrivacyEvents.ToggleEncryptionState -> { - if(currentSettings.isEncrypted) { - currentIsEncrypted = false + if (editedSettings.isEncrypted) { + editedIsEncrypted = false } else { showEncryptionConfirmation = true } } is SecurityAndPrivacyEvents.ChangeHistoryVisibility -> { - currentHistoryVisibility = Optional.of(event.historyVisibility) + editedHistoryVisibility = event.historyVisibility } is SecurityAndPrivacyEvents.ChangeRoomVisibility -> { - currentVisibleInRoomDirectory = Optional.of(AsyncData.Success(event.isVisibleInRoomDirectory)) + editedVisibleInRoomDirectory = AsyncData.Success(event.isVisibleInRoomDirectory) } SecurityAndPrivacyEvents.EditRoomAddress -> navigator.openEditRoomAddress() SecurityAndPrivacyEvents.CancelEnableEncryption -> { @@ -108,60 +119,137 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor( } SecurityAndPrivacyEvents.ConfirmEnableEncryption -> { showEncryptionConfirmation = false - currentIsEncrypted = true + editedIsEncrypted = true + } + SecurityAndPrivacyEvents.DismissSaveError -> { + saveAction.value = AsyncAction.Uninitialized } } } - return SecurityAndPrivacyState( + + val state = SecurityAndPrivacyState( savedSettings = savedSettings, - currentSettings = currentSettings, + editedSettings = editedSettings, homeserverName = homeserverName, showEncryptionConfirmation = showEncryptionConfirmation, + saveAction = saveAction.value, eventSink = ::handleEvents ) + LaunchedEffect(state.availableHistoryVisibilities) { + editedSettings.historyVisibility?.also { + if (it !in state.availableHistoryVisibilities) { + editedHistoryVisibility = it.fallback() + } + } + } + return state + } + + @Composable + private fun isRoomVisibleInRoomDirectory(): State> { + val result = remember { mutableStateOf>(AsyncData.Uninitialized) } + LaunchedEffect(Unit) { + result.runUpdatingState { + room.getRoomVisibility().map { it == RoomVisibility.Public } + } + } + return result + } + + private fun CoroutineScope.save( + saveAction: MutableState>, + savedSettings: SecurityAndPrivacySettings, + editedSettings: SecurityAndPrivacySettings, + ) = launch { + suspend { + var somethingWentWrong = false + if (editedSettings.isEncrypted && !savedSettings.isEncrypted) { + room + .enableEncryption() + .onFailure { + Timber.d("Failed to enable encryption") + somethingWentWrong = true + } + } + if (editedSettings.historyVisibility != null && editedSettings.historyVisibility != savedSettings.historyVisibility) { + room + .updateHistoryVisibility(editedSettings.historyVisibility.map()) + .onFailure { + Timber.d("Failed to update history visibility") + somethingWentWrong = true + } + } + if (editedSettings.roomAccess != savedSettings.roomAccess) { + room + .updateJoinRule(editedSettings.roomAccess.map()) + .onFailure { + Timber.d("Failed to update join rule") + somethingWentWrong = true + } + } + + val editedIsVisibleInRoomDirectory = when (editedSettings.roomAccess) { + SecurityAndPrivacyRoomAccess.AskToJoin, + SecurityAndPrivacyRoomAccess.Anyone -> editedSettings.isVisibleInRoomDirectory.dataOrNull() + else -> false + } + val savedIsVisibleInRoomDirectory = savedSettings.isVisibleInRoomDirectory.dataOrNull() + if (editedIsVisibleInRoomDirectory != null && editedIsVisibleInRoomDirectory != savedIsVisibleInRoomDirectory) { + val roomVisibility = if (editedIsVisibleInRoomDirectory) RoomVisibility.Public else RoomVisibility.Private + room + .updateRoomVisibility(roomVisibility) + .onFailure { + Timber.d("Failed to update room visibility") + somethingWentWrong = true + } + } + if (somethingWentWrong) { + error("") + } + }.runCatchingUpdatingState(saveAction) + } + + private fun JoinRule?.map(): SecurityAndPrivacyRoomAccess { + return when (this) { + JoinRule.Public -> SecurityAndPrivacyRoomAccess.Anyone + JoinRule.Knock, is JoinRule.KnockRestricted -> SecurityAndPrivacyRoomAccess.AskToJoin + is JoinRule.Restricted -> SecurityAndPrivacyRoomAccess.SpaceMember + is JoinRule.Custom, + JoinRule.Invite, + JoinRule.Private, + null -> SecurityAndPrivacyRoomAccess.InviteOnly + } + } + + private fun SecurityAndPrivacyRoomAccess.map(): JoinRule { + return when (this) { + SecurityAndPrivacyRoomAccess.Anyone -> JoinRule.Public + SecurityAndPrivacyRoomAccess.AskToJoin -> JoinRule.Knock + SecurityAndPrivacyRoomAccess.InviteOnly -> JoinRule.Private + SecurityAndPrivacyRoomAccess.SpaceMember -> error("Unsupported") + } + } + + private fun RoomHistoryVisibility.map(): SecurityAndPrivacyHistoryVisibility { + return when (this) { + RoomHistoryVisibility.Joined, + RoomHistoryVisibility.Invited -> SecurityAndPrivacyHistoryVisibility.SinceInvite + RoomHistoryVisibility.Shared, + is RoomHistoryVisibility.Custom -> SecurityAndPrivacyHistoryVisibility.SinceSelection + RoomHistoryVisibility.WorldReadable -> SecurityAndPrivacyHistoryVisibility.Anyone + } + } + + private fun SecurityAndPrivacyHistoryVisibility.map(): RoomHistoryVisibility { + return when (this) { + SecurityAndPrivacyHistoryVisibility.SinceSelection -> RoomHistoryVisibility.Shared + SecurityAndPrivacyHistoryVisibility.SinceInvite -> RoomHistoryVisibility.Invited + SecurityAndPrivacyHistoryVisibility.Anyone -> RoomHistoryVisibility.WorldReadable + } + } + + private fun MatrixRoomInfo.firstDisplayableAlias(serverName: String): RoomAlias? { + return aliases.firstOrNull { it.matchesServer(serverName) } ?: aliases.firstOrNull() } } -private fun JoinRule?.map(): SecurityAndPrivacyRoomAccess { - return when (this) { - JoinRule.Public -> SecurityAndPrivacyRoomAccess.Anyone - JoinRule.Knock, is JoinRule.KnockRestricted -> SecurityAndPrivacyRoomAccess.AskToJoin - is JoinRule.Restricted -> SecurityAndPrivacyRoomAccess.SpaceMember - is JoinRule.Custom, - JoinRule.Invite, - JoinRule.Private, - null -> SecurityAndPrivacyRoomAccess.InviteOnly - } -} - -private fun SecurityAndPrivacyRoomAccess.map(): JoinRule { - return when (this) { - SecurityAndPrivacyRoomAccess.Anyone -> JoinRule.Public - SecurityAndPrivacyRoomAccess.AskToJoin -> JoinRule.Knock - SecurityAndPrivacyRoomAccess.InviteOnly -> JoinRule.Private - SecurityAndPrivacyRoomAccess.SpaceMember -> error("Unsupported") - } -} - -private fun RoomHistoryVisibility.map(): SecurityAndPrivacyHistoryVisibility { - return when (this) { - RoomHistoryVisibility.Joined, - RoomHistoryVisibility.Invited -> SecurityAndPrivacyHistoryVisibility.SinceInvite - RoomHistoryVisibility.Shared, - is RoomHistoryVisibility.Custom -> SecurityAndPrivacyHistoryVisibility.SinceSelection - RoomHistoryVisibility.WorldReadable -> SecurityAndPrivacyHistoryVisibility.Anyone - } -} - -private fun SecurityAndPrivacyHistoryVisibility.map(): RoomHistoryVisibility { - return when (this) { - SecurityAndPrivacyHistoryVisibility.SinceSelection -> RoomHistoryVisibility.Shared - SecurityAndPrivacyHistoryVisibility.SinceInvite -> RoomHistoryVisibility.Invited - SecurityAndPrivacyHistoryVisibility.Anyone -> RoomHistoryVisibility.WorldReadable - } -} - -private fun MatrixRoomInfo.firstDisplayableAlias(serverName: String): RoomAlias? { - return aliases.firstOrNull { it.matchesServer(serverName)} ?: aliases.firstOrNull() -} - diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt index 23c273dac9..3209c71b52 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyState.kt @@ -7,46 +7,58 @@ package io.element.android.features.roomdetails.impl.securityandprivacy +import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData -import java.util.Optional -import kotlin.jvm.optionals.getOrNull data class SecurityAndPrivacyState( + // the settings that are currently applied on the room. val savedSettings: SecurityAndPrivacySettings, - val currentSettings: SecurityAndPrivacySettings, + // the settings the user wants to apply. + val editedSettings: SecurityAndPrivacySettings, val homeserverName: String, val showEncryptionConfirmation: Boolean, + val saveAction: AsyncAction, val eventSink: (SecurityAndPrivacyEvents) -> Unit ) { - val canBeSaved = savedSettings != currentSettings + val canBeSaved = savedSettings != editedSettings - val showRoomVisibilitySections = currentSettings.roomAccess != SecurityAndPrivacyRoomAccess.InviteOnly && currentSettings.historyVisibility.isPresent val availableHistoryVisibilities = buildSet { add(SecurityAndPrivacyHistoryVisibility.SinceSelection) - if (currentSettings.roomAccess == SecurityAndPrivacyRoomAccess.Anyone && !currentSettings.isEncrypted) { + if (editedSettings.roomAccess == SecurityAndPrivacyRoomAccess.Anyone && !editedSettings.isEncrypted) { add(SecurityAndPrivacyHistoryVisibility.Anyone) } else { add(SecurityAndPrivacyHistoryVisibility.SinceInvite) } - if (savedSettings.historyVisibility.getOrNull() == SecurityAndPrivacyHistoryVisibility.SinceInvite) { - add(SecurityAndPrivacyHistoryVisibility.SinceInvite) - } } - val showRoomHistoryVisibilitySection = availableHistoryVisibilities.isNotEmpty() && currentSettings.historyVisibility.isPresent + val showRoomAccessSection: Boolean = true + val showRoomVisibilitySections = editedSettings.roomAccess != SecurityAndPrivacyRoomAccess.InviteOnly + val showHistoryVisibilitySection = editedSettings.historyVisibility != null + val showEncryptionSection = true } data class SecurityAndPrivacySettings( val roomAccess: SecurityAndPrivacyRoomAccess, val isEncrypted: Boolean, - val historyVisibility: Optional, - val addressName: Optional, - val isVisibleInRoomDirectory: Optional> + val historyVisibility: SecurityAndPrivacyHistoryVisibility?, + val addressName: String?, + val isVisibleInRoomDirectory: AsyncData ) enum class SecurityAndPrivacyHistoryVisibility { - SinceSelection, SinceInvite, Anyone + SinceSelection, SinceInvite, Anyone; + + /** + * Returns the fallback visibility when the current visibility is not available. + */ + fun fallback(): SecurityAndPrivacyHistoryVisibility { + return when (this) { + SinceSelection -> SinceSelection + SinceInvite -> Anyone + Anyone -> SinceInvite + } + } } enum class SecurityAndPrivacyRoomAccess { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt index a337014712..8bf9b9757e 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyStateProvider.kt @@ -8,37 +8,37 @@ package io.element.android.features.roomdetails.impl.securityandprivacy import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData -import java.util.Optional open class SecurityAndPrivacyStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aSecurityAndPrivacyState(), aSecurityAndPrivacyState( - currentSettings = aSecurityAndPrivacySettings( + editedSettings = aSecurityAndPrivacySettings( roomAccess = SecurityAndPrivacyRoomAccess.AskToJoin ) ), aSecurityAndPrivacyState( - currentSettings = aSecurityAndPrivacySettings( + editedSettings = aSecurityAndPrivacySettings( roomAccess = SecurityAndPrivacyRoomAccess.Anyone, isEncrypted = false, ) ), aSecurityAndPrivacyState( - currentSettings = aSecurityAndPrivacySettings( + editedSettings = aSecurityAndPrivacySettings( roomAccess = SecurityAndPrivacyRoomAccess.SpaceMember ) ), aSecurityAndPrivacyState( - currentSettings = aSecurityAndPrivacySettings( - isVisibleInRoomDirectory = Optional.of(AsyncData.Loading()) + editedSettings = aSecurityAndPrivacySettings( + isVisibleInRoomDirectory = AsyncData.Loading() ) ), aSecurityAndPrivacyState( - currentSettings = aSecurityAndPrivacySettings( - isVisibleInRoomDirectory = Optional.of(AsyncData.Success(true)) + editedSettings = aSecurityAndPrivacySettings( + isVisibleInRoomDirectory = AsyncData.Success(true) ) ), aSecurityAndPrivacyState( @@ -50,9 +50,9 @@ open class SecurityAndPrivacyStateProvider : PreviewParameterProvider = Optional.empty(), - historyVisibility: Optional = Optional.of(SecurityAndPrivacyHistoryVisibility.SinceSelection), - isVisibleInRoomDirectory: Optional> = Optional.empty() + formattedAddress: String? = null, + historyVisibility: SecurityAndPrivacyHistoryVisibility? = null, + isVisibleInRoomDirectory: AsyncData = AsyncData.Uninitialized, ) = SecurityAndPrivacySettings( roomAccess = roomAccess, isEncrypted = isEncrypted, @@ -62,15 +62,17 @@ fun aSecurityAndPrivacySettings( ) fun aSecurityAndPrivacyState( - currentSettings: SecurityAndPrivacySettings = aSecurityAndPrivacySettings(), - savedSettings: SecurityAndPrivacySettings = currentSettings, + savedSettings: SecurityAndPrivacySettings = aSecurityAndPrivacySettings(), + editedSettings: SecurityAndPrivacySettings = savedSettings, homeserverName: String = "myserver.xyz", showEncryptionConfirmation: Boolean = false, + saveAction: AsyncAction = AsyncAction.Uninitialized, eventSink: (SecurityAndPrivacyEvents) -> Unit = {} ) = SecurityAndPrivacyState( - currentSettings = currentSettings, + editedSettings = editedSettings, savedSettings = savedSettings, homeserverName = homeserverName, showEncryptionConfirmation = showEncryptionConfirmation, + saveAction = saveAction, eventSink = eventSink ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt index e6b14b6762..552d80186a 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt @@ -32,6 +32,8 @@ import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.roomdetails.impl.R import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage +import io.element.android.libraries.designsystem.components.async.AsyncActionView +import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.list.ListItemContent @@ -47,8 +49,6 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.ui.strings.CommonStrings -import java.util.Optional -import kotlin.jvm.optionals.getOrNull @Composable fun SecurityAndPrivacyView( @@ -76,40 +76,58 @@ fun SecurityAndPrivacyView( .consumeWindowInsets(padding), verticalArrangement = Arrangement.spacedBy(32.dp), ) { - RoomAccessSection( - modifier = Modifier.padding(top = 24.dp), - selected = state.currentSettings.roomAccess, - onSelected = { state.eventSink(SecurityAndPrivacyEvents.ChangeRoomAccess(it)) }, - ) + if (state.showRoomAccessSection) { + RoomAccessSection( + modifier = Modifier.padding(top = 24.dp), + edited = state.editedSettings.roomAccess, + saved = state.savedSettings.roomAccess, + onSelected = { state.eventSink(SecurityAndPrivacyEvents.ChangeRoomAccess(it)) }, + ) + } if (state.showRoomVisibilitySections) { RoomVisibilitySection(state.homeserverName) RoomAddressSection( - roomAddress = state.currentSettings.addressName, + roomAddress = state.editedSettings.addressName, homeserverName = state.homeserverName, onRoomAddressClick = { state.eventSink(SecurityAndPrivacyEvents.EditRoomAddress) }, - isVisibleInPublicDirectory = state.currentSettings.isVisibleInRoomDirectory, + isVisibleInRoomDirectory = state.editedSettings.isVisibleInRoomDirectory, onVisibilityChange = { isVisible -> state.eventSink(SecurityAndPrivacyEvents.ChangeRoomVisibility(isVisible)) }, ) } - EncryptionSection( - isRoomEncrypted = state.currentSettings.isEncrypted, - isSectionEnabled = !state.savedSettings.isEncrypted, - onToggleEncryption = { state.eventSink(SecurityAndPrivacyEvents.ToggleEncryptionState) }, - showConfirmation = state.showEncryptionConfirmation, - onDismissConfirmation = { state.eventSink(SecurityAndPrivacyEvents.CancelEnableEncryption) }, - onConfirmEncryption = { state.eventSink(SecurityAndPrivacyEvents.ConfirmEnableEncryption) }, - ) - if (state.showRoomHistoryVisibilitySection) { - RoomHistorySection( - selectedOption = state.currentSettings.historyVisibility.get(), + if (state.showEncryptionSection) { + EncryptionSection( + isRoomEncrypted = state.editedSettings.isEncrypted, + canToggleEncryption = !state.savedSettings.isEncrypted, + onToggleEncryption = { state.eventSink(SecurityAndPrivacyEvents.ToggleEncryptionState) }, + showConfirmation = state.showEncryptionConfirmation, + onDismissConfirmation = { state.eventSink(SecurityAndPrivacyEvents.CancelEnableEncryption) }, + onConfirmEncryption = { state.eventSink(SecurityAndPrivacyEvents.ConfirmEnableEncryption) }, + ) + } + if (state.showHistoryVisibilitySection) { + HistoryVisibilitySection( + editedOption = state.editedSettings.historyVisibility, + savedOptions = state.savedSettings.historyVisibility, availableOptions = state.availableHistoryVisibilities, onSelected = { state.eventSink(SecurityAndPrivacyEvents.ChangeHistoryVisibility(it)) }, ) } } } + AsyncActionView( + async = state.saveAction, + onSuccess = { }, + onErrorDismiss = { state.eventSink(SecurityAndPrivacyEvents.DismissSaveError) }, + errorMessage = { stringResource(CommonStrings.error_unknown) }, + progressDialog = { + AsyncActionViewDefaults.ProgressDialog( + progressText = stringResource(CommonStrings.common_saving), + ) + }, + onRetry = { state.eventSink(SecurityAndPrivacyEvents.Save) }, + ) } @OptIn(ExperimentalMaterial3Api::class) @@ -160,7 +178,8 @@ private fun SecurityAndPrivacySection( @Composable private fun RoomAccessSection( - selected: SecurityAndPrivacyRoomAccess, + edited: SecurityAndPrivacyRoomAccess, + saved: SecurityAndPrivacyRoomAccess, onSelected: (SecurityAndPrivacyRoomAccess) -> Unit, modifier: Modifier = Modifier, ) { @@ -171,26 +190,27 @@ private fun RoomAccessSection( ListItem( headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_access_invite_only_option_title)) }, supportingContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_access_invite_only_option_description)) }, - trailingContent = ListItemContent.RadioButton(selected = selected == SecurityAndPrivacyRoomAccess.InviteOnly), + trailingContent = ListItemContent.RadioButton(selected = edited == SecurityAndPrivacyRoomAccess.InviteOnly), onClick = { onSelected(SecurityAndPrivacyRoomAccess.InviteOnly) }, ) ListItem( headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_ask_to_join_option_title)) }, supportingContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_ask_to_join_option_description)) }, - trailingContent = ListItemContent.RadioButton(selected = selected == SecurityAndPrivacyRoomAccess.AskToJoin), + trailingContent = ListItemContent.RadioButton(selected = edited == SecurityAndPrivacyRoomAccess.AskToJoin), onClick = { onSelected(SecurityAndPrivacyRoomAccess.AskToJoin) }, ) ListItem( headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_access_anyone_option_title)) }, supportingContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_access_anyone_option_description)) }, - trailingContent = ListItemContent.RadioButton(selected = selected == SecurityAndPrivacyRoomAccess.Anyone), + trailingContent = ListItemContent.RadioButton(selected = edited == SecurityAndPrivacyRoomAccess.Anyone), onClick = { onSelected(SecurityAndPrivacyRoomAccess.Anyone) }, ) - if (selected == SecurityAndPrivacyRoomAccess.SpaceMember) { + if (saved == SecurityAndPrivacyRoomAccess.SpaceMember) { ListItem( headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_access_space_members_option_title)) }, supportingContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_access_space_members_option_description)) }, trailingContent = ListItemContent.RadioButton(selected = true, enabled = false), + enabled = false, ) } } @@ -217,9 +237,9 @@ private fun RoomVisibilitySection( @Composable private fun RoomAddressSection( - roomAddress: Optional, + roomAddress: String?, homeserverName: String, - isVisibleInPublicDirectory: Optional>, + isVisibleInRoomDirectory: AsyncData, onRoomAddressClick: () -> Unit, onVisibilityChange: (Boolean) -> Unit, modifier: Modifier = Modifier, @@ -230,54 +250,53 @@ private fun RoomAddressSection( ) { ListItem( headlineContent = { - Text(text = roomAddress.getOrNull() ?: stringResource(CommonStrings.screen_security_and_privacy_add_room_address_action)) + Text(text = roomAddress ?: stringResource(CommonStrings.screen_security_and_privacy_add_room_address_action)) }, - trailingContent = if (roomAddress.isEmpty) ListItemContent.Icon(IconSource.Vector(CompoundIcons.Plus())) else null, + trailingContent = if (roomAddress.isNullOrEmpty()) ListItemContent.Icon(IconSource.Vector(CompoundIcons.Plus())) else null, supportingContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_address_section_footer)) }, onClick = onRoomAddressClick, colors = ListItemDefaults.colors(trailingIconColor = ElementTheme.colors.iconAccentPrimary), alwaysClickable = true ) - if (isVisibleInPublicDirectory.isPresent) { - ListItem( - headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_directory_visibility_toggle_title)) }, - supportingContent = { - Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_directory_visibility_section_footer, homeserverName)) - }, - trailingContent = - when (val isVisible = isVisibleInPublicDirectory.get()) { - is AsyncData.Uninitialized, is AsyncData.Loading -> { - ListItemContent.Custom { - CircularProgressIndicator( - modifier = Modifier - .progressSemantics() - .size(20.dp), - strokeWidth = 2.dp - ) - } - } - is AsyncData.Failure -> { - ListItemContent.Switch( - checked = false, - enabled = false, - ) - } - is AsyncData.Success -> { - ListItemContent.Switch( - checked = isVisible.data, - onChange = onVisibilityChange + + ListItem( + headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_directory_visibility_toggle_title)) }, + supportingContent = { + Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_directory_visibility_section_footer, homeserverName)) + }, + trailingContent = + when (isVisibleInRoomDirectory) { + is AsyncData.Uninitialized, is AsyncData.Loading -> { + ListItemContent.Custom { + CircularProgressIndicator( + modifier = Modifier + .progressSemantics() + .size(20.dp), + strokeWidth = 2.dp ) } } - ) - } + is AsyncData.Failure -> { + ListItemContent.Switch( + checked = false, + enabled = false, + ) + } + is AsyncData.Success -> { + ListItemContent.Switch( + checked = isVisibleInRoomDirectory.data, + onChange = onVisibilityChange, + ) + } + } + ) } } @Composable private fun EncryptionSection( isRoomEncrypted: Boolean, - isSectionEnabled: Boolean, + canToggleEncryption: Boolean, showConfirmation: Boolean, onToggleEncryption: () -> Unit, onConfirmEncryption: () -> Unit, @@ -293,10 +312,10 @@ private fun EncryptionSection( supportingContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_encryption_section_footer)) }, trailingContent = ListItemContent.Switch( checked = isRoomEncrypted, - enabled = isSectionEnabled, + enabled = canToggleEncryption, onChange = { onToggleEncryption() }, ), - onClick = onToggleEncryption, + onClick = if (canToggleEncryption) onToggleEncryption else null ) } if (showConfirmation) { @@ -311,8 +330,9 @@ private fun EncryptionSection( } @Composable -private fun RoomHistorySection( - selectedOption: SecurityAndPrivacyHistoryVisibility, +private fun HistoryVisibilitySection( + editedOption: SecurityAndPrivacyHistoryVisibility?, + savedOptions: SecurityAndPrivacyHistoryVisibility?, availableOptions: Set, onSelected: (SecurityAndPrivacyHistoryVisibility) -> Unit, modifier: Modifier = Modifier, @@ -323,34 +343,46 @@ private fun RoomHistorySection( ) { Spacer(Modifier.height(16.dp)) for (availableOption in availableOptions) { - val isSelected = availableOption == selectedOption - when (availableOption) { - SecurityAndPrivacyHistoryVisibility.SinceSelection -> { - ListItem( - headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_history_since_selecting_option_title)) }, - trailingContent = ListItemContent.RadioButton(selected = isSelected), - onClick = { onSelected(availableOption) }, - ) - } - SecurityAndPrivacyHistoryVisibility.SinceInvite -> { - ListItem( - headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_history_since_invite_option_title)) }, - trailingContent = ListItemContent.RadioButton(selected = isSelected), - onClick = { onSelected(availableOption) }, - ) - } - SecurityAndPrivacyHistoryVisibility.Anyone -> { - ListItem( - headlineContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_room_history_anyone_option_title)) }, - trailingContent = ListItemContent.RadioButton(selected = isSelected), - onClick = { onSelected(availableOption) }, - ) - } - } + val isSelected = availableOption == editedOption + HistoryVisibilityItem( + option = availableOption, + isSelected = isSelected, + onSelected = onSelected, + ) + } + if (savedOptions != null && !availableOptions.contains(savedOptions)) { + HistoryVisibilityItem( + option = savedOptions, + isSelected = true, + isEnabled = false, + onSelected = {}, + ) } } } +@Composable +private fun HistoryVisibilityItem( + option: SecurityAndPrivacyHistoryVisibility, + isSelected: Boolean, + onSelected: (SecurityAndPrivacyHistoryVisibility) -> Unit, + modifier: Modifier = Modifier, + isEnabled: Boolean = true, +) { + val headlineText = when (option) { + SecurityAndPrivacyHistoryVisibility.SinceSelection -> stringResource(CommonStrings.screen_security_and_privacy_room_history_since_selecting_option_title) + SecurityAndPrivacyHistoryVisibility.SinceInvite -> stringResource(CommonStrings.screen_security_and_privacy_room_history_since_invite_option_title) + SecurityAndPrivacyHistoryVisibility.Anyone -> stringResource(CommonStrings.screen_security_and_privacy_room_history_anyone_option_title) + } + ListItem( + headlineContent = { Text(text = headlineText) }, + trailingContent = ListItemContent.RadioButton(selected = isSelected, enabled = isEnabled), + onClick = { onSelected(option) }, + enabled = isEnabled, + modifier = modifier, + ) +} + @PreviewWithLargeHeight @Composable internal fun SecurityAndPrivacyViewLightPreview(@PreviewParameter(SecurityAndPrivacyStateProvider::class) state: SecurityAndPrivacyState) =