feat(security&privacy) : manage save action and some edge cases.

This commit is contained in:
ganfra
2025-01-23 14:19:23 +01:00
parent 9ee5927489
commit be199e25ff
5 changed files with 320 additions and 185 deletions

View File

@@ -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
}

View File

@@ -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<Boolean>>(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<Unit>>(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<AsyncData<Boolean>> {
val result = remember { mutableStateOf<AsyncData<Boolean>>(AsyncData.Uninitialized) }
LaunchedEffect(Unit) {
result.runUpdatingState {
room.getRoomVisibility().map { it == RoomVisibility.Public }
}
}
return result
}
private fun CoroutineScope.save(
saveAction: MutableState<AsyncAction<Unit>>,
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()
}

View File

@@ -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<Unit>,
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<SecurityAndPrivacyHistoryVisibility>,
val addressName: Optional<String>,
val isVisibleInRoomDirectory: Optional<AsyncData<Boolean>>
val historyVisibility: SecurityAndPrivacyHistoryVisibility?,
val addressName: String?,
val isVisibleInRoomDirectory: AsyncData<Boolean>
)
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 {

View File

@@ -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<SecurityAndPrivacyState> {
override val values: Sequence<SecurityAndPrivacyState>
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<SecurityAn
fun aSecurityAndPrivacySettings(
roomAccess: SecurityAndPrivacyRoomAccess = SecurityAndPrivacyRoomAccess.InviteOnly,
isEncrypted: Boolean = true,
formattedAddress: Optional<String> = Optional.empty(),
historyVisibility: Optional<SecurityAndPrivacyHistoryVisibility> = Optional.of(SecurityAndPrivacyHistoryVisibility.SinceSelection),
isVisibleInRoomDirectory: Optional<AsyncData<Boolean>> = Optional.empty()
formattedAddress: String? = null,
historyVisibility: SecurityAndPrivacyHistoryVisibility? = null,
isVisibleInRoomDirectory: AsyncData<Boolean> = 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<Unit> = AsyncAction.Uninitialized,
eventSink: (SecurityAndPrivacyEvents) -> Unit = {}
) = SecurityAndPrivacyState(
currentSettings = currentSettings,
editedSettings = editedSettings,
savedSettings = savedSettings,
homeserverName = homeserverName,
showEncryptionConfirmation = showEncryptionConfirmation,
saveAction = saveAction,
eventSink = eventSink
)

View File

@@ -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<String>,
roomAddress: String?,
homeserverName: String,
isVisibleInPublicDirectory: Optional<AsyncData<Boolean>>,
isVisibleInRoomDirectory: AsyncData<Boolean>,
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<SecurityAndPrivacyHistoryVisibility>,
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) =