change(security and privacy) : display SaveChangesDialog on exit

This commit is contained in:
ganfra 2025-11-19 21:44:39 +01:00
parent 1b2d66989f
commit 40bd16b1eb
5 changed files with 72 additions and 21 deletions

View file

@ -11,6 +11,8 @@ package io.element.android.features.securityandprivacy.impl.root
sealed interface SecurityAndPrivacyEvents { sealed interface SecurityAndPrivacyEvents {
data object EditRoomAddress : SecurityAndPrivacyEvents data object EditRoomAddress : SecurityAndPrivacyEvents
data object Save : SecurityAndPrivacyEvents data object Save : SecurityAndPrivacyEvents
data object Exit: SecurityAndPrivacyEvents
data object DismissExitConfirmation : SecurityAndPrivacyEvents
data class ChangeRoomAccess(val roomAccess: SecurityAndPrivacyRoomAccess) : SecurityAndPrivacyEvents data class ChangeRoomAccess(val roomAccess: SecurityAndPrivacyRoomAccess) : SecurityAndPrivacyEvents
data object ToggleEncryptionState : SecurityAndPrivacyEvents data object ToggleEncryptionState : SecurityAndPrivacyEvents
data object CancelEnableEncryption : SecurityAndPrivacyEvents data object CancelEnableEncryption : SecurityAndPrivacyEvents

View file

@ -26,6 +26,7 @@ import io.element.android.features.securityandprivacy.impl.editroomaddress.match
import io.element.android.features.securityandprivacy.api.securityAndPrivacyPermissionsAsState import io.element.android.features.securityandprivacy.api.securityAndPrivacyPermissionsAsState
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.AsyncData.*
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.architecture.runUpdatingState import io.element.android.libraries.architecture.runUpdatingState
@ -64,6 +65,7 @@ class SecurityAndPrivacyPresenter(
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock) featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock)
}.collectAsState(false) }.collectAsState(false)
val saveAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) } val saveAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
var confirmExitAction by remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)}
val homeserverName = remember { matrixClient.userIdServerName() } val homeserverName = remember { matrixClient.userIdServerName() }
val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
val roomInfo by room.roomInfoFlow.collectAsState() val roomInfo by room.roomInfoFlow.collectAsState()
@ -133,7 +135,7 @@ class SecurityAndPrivacyPresenter(
} }
SecurityAndPrivacyEvents.ToggleRoomVisibility -> { SecurityAndPrivacyEvents.ToggleRoomVisibility -> {
editedVisibleInRoomDirectory = when (val edited = editedVisibleInRoomDirectory) { editedVisibleInRoomDirectory = when (val edited = editedVisibleInRoomDirectory) {
is AsyncData.Success -> AsyncData.Success(!edited.data) is Success -> Success(!edited.data)
else -> edited else -> edited
} }
} }
@ -148,6 +150,16 @@ class SecurityAndPrivacyPresenter(
SecurityAndPrivacyEvents.DismissSaveError -> { SecurityAndPrivacyEvents.DismissSaveError -> {
saveAction.value = AsyncAction.Uninitialized saveAction.value = AsyncAction.Uninitialized
} }
SecurityAndPrivacyEvents.Exit -> {
confirmExitAction = if (savedSettings == editedSettings || confirmExitAction.isConfirming()) {
AsyncAction.Success(Unit)
} else {
AsyncAction.ConfirmingNoParams
}
}
SecurityAndPrivacyEvents.DismissExitConfirmation -> {
confirmExitAction = AsyncAction.Uninitialized
}
} }
} }
@ -160,6 +172,7 @@ class SecurityAndPrivacyPresenter(
saveAction = saveAction.value, saveAction = saveAction.value,
permissions = permissions, permissions = permissions,
isSpace = roomInfo.isSpace, isSpace = roomInfo.isSpace,
confirmExitAction = confirmExitAction,
eventSink = ::handleEvent, eventSink = ::handleEvent,
) )

View file

@ -22,6 +22,7 @@ data class SecurityAndPrivacyState(
val showEnableEncryptionConfirmation: Boolean, val showEnableEncryptionConfirmation: Boolean,
val isKnockEnabled: Boolean, val isKnockEnabled: Boolean,
val saveAction: AsyncAction<Unit>, val saveAction: AsyncAction<Unit>,
val confirmExitAction: AsyncAction<Unit>,
val isSpace: Boolean, val isSpace: Boolean,
private val permissions: SecurityAndPrivacyPermissions, private val permissions: SecurityAndPrivacyPermissions,
val eventSink: (SecurityAndPrivacyEvents) -> Unit val eventSink: (SecurityAndPrivacyEvents) -> Unit

View file

@ -15,10 +15,29 @@ import io.element.android.libraries.architecture.AsyncData
open class SecurityAndPrivacyStateProvider : PreviewParameterProvider<SecurityAndPrivacyState> { open class SecurityAndPrivacyStateProvider : PreviewParameterProvider<SecurityAndPrivacyState> {
override val values: Sequence<SecurityAndPrivacyState> override val values: Sequence<SecurityAndPrivacyState>
get() = securityAndPrivacyStates(isSpace = false) + securityAndPrivacyStates(isSpace = true) get() = commonSecurityAndPrivacyStates(isSpace = false) +
commonSecurityAndPrivacyStates(isSpace = true) +
sequenceOf(
aSecurityAndPrivacyState(
saveAction = AsyncAction.Loading,
isSpace = false,
),
aSecurityAndPrivacyState(
saveAction = AsyncAction.Failure(SecurityAndPrivacyFailures.SaveFailed),
isSpace = false,
),
aSecurityAndPrivacyState(
confirmExitAction = AsyncAction.ConfirmingCancellation,
isSpace = false,
),
aSecurityAndPrivacyState(
showEncryptionConfirmation = true,
isSpace = false,
),
)
} }
private fun securityAndPrivacyStates(isSpace: Boolean): Sequence<SecurityAndPrivacyState> = sequenceOf( private fun commonSecurityAndPrivacyStates(isSpace: Boolean): Sequence<SecurityAndPrivacyState> = sequenceOf(
aSecurityAndPrivacyState(isSpace = isSpace), aSecurityAndPrivacyState(isSpace = isSpace),
aSecurityAndPrivacyState( aSecurityAndPrivacyState(
editedSettings = aSecurityAndPrivacySettings( editedSettings = aSecurityAndPrivacySettings(
@ -26,6 +45,13 @@ private fun securityAndPrivacyStates(isSpace: Boolean): Sequence<SecurityAndPriv
), ),
isSpace = isSpace, isSpace = isSpace,
), ),
aSecurityAndPrivacyState(
savedSettings = aSecurityAndPrivacySettings(
roomAccess = SecurityAndPrivacyRoomAccess.AskToJoin
),
isSpace = isSpace,
isKnockEnabled = false,
),
aSecurityAndPrivacyState( aSecurityAndPrivacyState(
editedSettings = aSecurityAndPrivacySettings( editedSettings = aSecurityAndPrivacySettings(
roomAccess = SecurityAndPrivacyRoomAccess.Anyone, roomAccess = SecurityAndPrivacyRoomAccess.Anyone,
@ -49,31 +75,18 @@ private fun securityAndPrivacyStates(isSpace: Boolean): Sequence<SecurityAndPriv
), ),
aSecurityAndPrivacyState( aSecurityAndPrivacyState(
editedSettings = aSecurityAndPrivacySettings( editedSettings = aSecurityAndPrivacySettings(
roomAccess = SecurityAndPrivacyRoomAccess.Anyone,
isVisibleInRoomDirectory = AsyncData.Loading() isVisibleInRoomDirectory = AsyncData.Loading()
), ),
isSpace = isSpace, isSpace = isSpace,
), ),
aSecurityAndPrivacyState( aSecurityAndPrivacyState(
editedSettings = aSecurityAndPrivacySettings( editedSettings = aSecurityAndPrivacySettings(
roomAccess = SecurityAndPrivacyRoomAccess.Anyone,
isVisibleInRoomDirectory = AsyncData.Success(true) isVisibleInRoomDirectory = AsyncData.Success(true)
), ),
isSpace = isSpace, isSpace = isSpace,
), ),
aSecurityAndPrivacyState(
showEncryptionConfirmation = true,
isSpace = isSpace,
),
aSecurityAndPrivacyState(
saveAction = AsyncAction.Loading,
isSpace = isSpace,
),
aSecurityAndPrivacyState(
savedSettings = aSecurityAndPrivacySettings(
roomAccess = SecurityAndPrivacyRoomAccess.AskToJoin
),
isSpace = isSpace,
isKnockEnabled = false,
),
) )
fun aSecurityAndPrivacySettings( fun aSecurityAndPrivacySettings(
@ -96,6 +109,7 @@ fun aSecurityAndPrivacyState(
homeserverName: String = "myserver.xyz", homeserverName: String = "myserver.xyz",
showEncryptionConfirmation: Boolean = false, showEncryptionConfirmation: Boolean = false,
saveAction: AsyncAction<Unit> = AsyncAction.Uninitialized, saveAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
confirmExitAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
permissions: SecurityAndPrivacyPermissions = SecurityAndPrivacyPermissions( permissions: SecurityAndPrivacyPermissions = SecurityAndPrivacyPermissions(
canChangeRoomAccess = true, canChangeRoomAccess = true,
canChangeHistoryVisibility = true, canChangeHistoryVisibility = true,
@ -103,7 +117,7 @@ fun aSecurityAndPrivacyState(
canChangeRoomVisibility = true canChangeRoomVisibility = true
), ),
isKnockEnabled: Boolean = true, isKnockEnabled: Boolean = true,
isSpace: Boolean = false, isSpace: Boolean,
eventSink: (SecurityAndPrivacyEvents) -> Unit = {} eventSink: (SecurityAndPrivacyEvents) -> Unit = {}
) = SecurityAndPrivacyState( ) = SecurityAndPrivacyState(
editedSettings = editedSettings, editedSettings = editedSettings,
@ -111,8 +125,9 @@ fun aSecurityAndPrivacyState(
homeserverName = homeserverName, homeserverName = homeserverName,
showEnableEncryptionConfirmation = showEncryptionConfirmation, showEnableEncryptionConfirmation = showEncryptionConfirmation,
saveAction = saveAction, saveAction = saveAction,
confirmExitAction = confirmExitAction,
isKnockEnabled = isKnockEnabled, isKnockEnabled = isKnockEnabled,
permissions = permissions, permissions = permissions,
isSpace = isSpace, isSpace = isSpace,
eventSink = eventSink eventSink = eventSink,
) )

View file

@ -8,6 +8,7 @@
package io.element.android.features.securityandprivacy.impl.root package io.element.android.features.securityandprivacy.impl.root
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
@ -31,12 +32,14 @@ import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.securityandprivacy.impl.R import io.element.android.features.securityandprivacy.impl.R
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage 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.AsyncActionView
import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults 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.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.components.dialogs.SaveChangesDialog
import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.ElementPreviewLight
@ -50,6 +53,7 @@ import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.ImmutableSet
import java.security.Security
@Composable @Composable
fun SecurityAndPrivacyView( fun SecurityAndPrivacyView(
@ -57,12 +61,17 @@ fun SecurityAndPrivacyView(
onBackClick: () -> Unit, onBackClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
BackHandler {
state.eventSink(SecurityAndPrivacyEvents.Exit)
}
Scaffold( Scaffold(
modifier = modifier, modifier = modifier,
topBar = { topBar = {
SecurityAndPrivacyToolbar( SecurityAndPrivacyToolbar(
isSaveActionEnabled = state.canBeSaved, isSaveActionEnabled = state.canBeSaved,
onBackClick = onBackClick, onBackClick = {
state.eventSink(SecurityAndPrivacyEvents.Exit)
},
onSaveClick = { onSaveClick = {
state.eventSink(SecurityAndPrivacyEvents.Save) state.eventSink(SecurityAndPrivacyEvents.Save)
}, },
@ -131,6 +140,17 @@ fun SecurityAndPrivacyView(
}, },
onRetry = { state.eventSink(SecurityAndPrivacyEvents.Save) }, onRetry = { state.eventSink(SecurityAndPrivacyEvents.Save) },
) )
AsyncActionView(
async = state.confirmExitAction,
onSuccess = { onBackClick() },
onErrorDismiss = { },
confirmationDialog = {
SaveChangesDialog(
onSubmitClick = { state.eventSink(SecurityAndPrivacyEvents.Exit) },
onDismiss = { state.eventSink(SecurityAndPrivacyEvents.DismissExitConfirmation) }
)
},
)
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)