change(security and privacy) : extract code to a separate module
This commit is contained in:
parent
da57eaadf2
commit
c6ba2f5d10
64 changed files with 1296 additions and 55 deletions
|
|
@ -40,7 +40,7 @@ import io.element.android.features.roomdetails.impl.invite.RoomInviteMembersNode
|
|||
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.securityandprivacy.SecurityAndPrivacyFlowNode
|
||||
import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyEntryPoint
|
||||
import io.element.android.features.userprofile.shared.UserProfileNodeHelper
|
||||
import io.element.android.features.verifysession.api.OutgoingVerificationEntryPoint
|
||||
import io.element.android.libraries.architecture.BackstackWithOverlayBox
|
||||
|
|
@ -84,6 +84,7 @@ class RoomDetailsFlowNode(
|
|||
private val reportRoomEntryPoint: ReportRoomEntryPoint,
|
||||
private val changeRoomMemberRolesEntryPoint: ChangeRoomMemberRolesEntryPoint,
|
||||
private val rolesAndPermissionsEntryPoint: RolesAndPermissionsEntryPoint,
|
||||
private val securityAndPrivacyEntryPoint: SecurityAndPrivacyEntryPoint,
|
||||
) : BaseFlowNode<RoomDetailsFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = plugins.filterIsInstance<RoomDetailsEntryPoint.Params>().first().initialElement.toNavTarget(),
|
||||
|
|
@ -381,7 +382,7 @@ class RoomDetailsFlowNode(
|
|||
knockRequestsListEntryPoint.createNode(this, buildContext)
|
||||
}
|
||||
NavTarget.SecurityAndPrivacy -> {
|
||||
createNode<SecurityAndPrivacyFlowNode>(buildContext)
|
||||
securityAndPrivacyEntryPoint.createNode(this, buildContext)
|
||||
}
|
||||
is NavTarget.VerifyUser -> {
|
||||
val params = OutgoingVerificationEntryPoint.Params(
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
|||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
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.features.securityandprivacy.api.securityAndPrivacyPermissionsAsState
|
||||
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* 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
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* 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 dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.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)
|
||||
@AssistedInject
|
||||
class SecurityAndPrivacyFlowNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
) : BaseFlowNode<SecurityAndPrivacyFlowNode.NavTarget>(
|
||||
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<SecurityAndPrivacyNode>(buildContext, plugins = listOf(navigator))
|
||||
}
|
||||
NavTarget.EditRoomAddress -> {
|
||||
createNode<EditRoomAddressNode>(buildContext, plugins = listOf(navigator))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
BackstackView(modifier)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* 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<SecurityAndPrivacyFlowNode.NavTarget>
|
||||
) : SecurityAndPrivacyNavigator {
|
||||
override fun openEditRoomAddress() {
|
||||
backStack.push(SecurityAndPrivacyFlowNode.NavTarget.EditRoomAddress)
|
||||
}
|
||||
|
||||
override fun closeEditRoomAddress() {
|
||||
backStack.pop()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* 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 dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
@AssistedInject
|
||||
class SecurityAndPrivacyNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
presenterFactory: SecurityAndPrivacyPresenter.Factory,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
private val navigator = plugins<SecurityAndPrivacyNavigator>().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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,293 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* 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 dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.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.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
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.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomInfo
|
||||
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
|
||||
|
||||
@AssistedInject
|
||||
class SecurityAndPrivacyPresenter(
|
||||
@Assisted private val navigator: SecurityAndPrivacyNavigator,
|
||||
private val matrixClient: MatrixClient,
|
||||
private val room: JoinedRoom,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
) : Presenter<SecurityAndPrivacyState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(navigator: SecurityAndPrivacyNavigator): SecurityAndPrivacyPresenter
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): SecurityAndPrivacyState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val isKnockEnabled by remember {
|
||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock)
|
||||
}.collectAsState(false)
|
||||
val saveAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||
val homeserverName = remember { matrixClient.userIdServerName() }
|
||||
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
||||
val roomInfo by room.roomInfoFlow.collectAsState()
|
||||
|
||||
val savedIsVisibleInRoomDirectory = remember { mutableStateOf<AsyncData<Boolean>>(AsyncData.Uninitialized) }
|
||||
LaunchedEffect(Unit) {
|
||||
isRoomVisibleInRoomDirectory(savedIsVisibleInRoomDirectory)
|
||||
}
|
||||
|
||||
val savedSettings by remember {
|
||||
derivedStateOf {
|
||||
val historyVisibility = roomInfo.historyVisibility.map()
|
||||
SecurityAndPrivacySettings(
|
||||
roomAccess = roomInfo.joinRule.map(),
|
||||
isEncrypted = roomInfo.isEncrypted == true,
|
||||
isVisibleInRoomDirectory = savedIsVisibleInRoomDirectory.value,
|
||||
historyVisibility = historyVisibility,
|
||||
address = roomInfo.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 handleEvent(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,
|
||||
isKnockEnabled = isKnockEnabled,
|
||||
saveAction = saveAction.value,
|
||||
permissions = permissions,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
|
||||
// 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<AsyncData<Boolean>>) = launch {
|
||||
isRoomVisible.runUpdatingState {
|
||||
room.getRoomVisibility().map { it == RoomVisibility.Public }
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.save(
|
||||
saveAction: MutableState<AsyncAction<Unit>>,
|
||||
isVisibleInRoomDirectory: MutableState<AsyncData<Boolean>>,
|
||||
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 RoomInfo.firstDisplayableAlias(serverName: String): RoomAlias? {
|
||||
return aliases.firstOrNull { it.matchesServer(serverName) } ?: aliases.firstOrNull()
|
||||
}
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* 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 isKnockEnabled: Boolean,
|
||||
val saveAction: AsyncAction<Unit>,
|
||||
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<Boolean>
|
||||
)
|
||||
|
||||
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()
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* 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<SecurityAndPrivacyState> {
|
||||
override val values: Sequence<SecurityAndPrivacyState>
|
||||
get() = sequenceOf(
|
||||
aSecurityAndPrivacyState(),
|
||||
aSecurityAndPrivacyState(
|
||||
editedSettings = aSecurityAndPrivacySettings(
|
||||
roomAccess = SecurityAndPrivacyRoomAccess.AskToJoin
|
||||
)
|
||||
),
|
||||
aSecurityAndPrivacyState(
|
||||
editedSettings = aSecurityAndPrivacySettings(
|
||||
roomAccess = SecurityAndPrivacyRoomAccess.Anyone,
|
||||
isEncrypted = false,
|
||||
)
|
||||
),
|
||||
aSecurityAndPrivacyState(
|
||||
savedSettings = aSecurityAndPrivacySettings(
|
||||
roomAccess = SecurityAndPrivacyRoomAccess.SpaceMember
|
||||
),
|
||||
isKnockEnabled = false,
|
||||
),
|
||||
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
|
||||
),
|
||||
aSecurityAndPrivacyState(
|
||||
savedSettings = aSecurityAndPrivacySettings(
|
||||
roomAccess = SecurityAndPrivacyRoomAccess.AskToJoin
|
||||
),
|
||||
isKnockEnabled = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun aSecurityAndPrivacySettings(
|
||||
roomAccess: SecurityAndPrivacyRoomAccess = SecurityAndPrivacyRoomAccess.InviteOnly,
|
||||
isEncrypted: Boolean = true,
|
||||
address: String? = null,
|
||||
historyVisibility: SecurityAndPrivacyHistoryVisibility = SecurityAndPrivacyHistoryVisibility.SinceSelection,
|
||||
isVisibleInRoomDirectory: AsyncData<Boolean> = 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<Unit> = AsyncAction.Uninitialized,
|
||||
permissions: SecurityAndPrivacyPermissions = SecurityAndPrivacyPermissions(
|
||||
canChangeRoomAccess = true,
|
||||
canChangeHistoryVisibility = true,
|
||||
canChangeEncryption = true,
|
||||
canChangeRoomVisibility = true
|
||||
),
|
||||
isKnockEnabled: Boolean = true,
|
||||
eventSink: (SecurityAndPrivacyEvents) -> Unit = {}
|
||||
) = SecurityAndPrivacyState(
|
||||
editedSettings = editedSettings,
|
||||
savedSettings = savedSettings,
|
||||
homeserverName = homeserverName,
|
||||
showEnableEncryptionConfirmation = showEncryptionConfirmation,
|
||||
saveAction = saveAction,
|
||||
isKnockEnabled = isKnockEnabled,
|
||||
permissions = permissions,
|
||||
eventSink = eventSink
|
||||
)
|
||||
|
|
@ -1,406 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* 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.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,
|
||||
isKnockEnabled = state.isKnockEnabled,
|
||||
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,
|
||||
titleStr = stringResource(R.string.screen_room_details_security_and_privacy_title),
|
||||
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,
|
||||
isKnockEnabled: Boolean,
|
||||
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) },
|
||||
)
|
||||
// Show Ask to join option in two cases:
|
||||
// - the Knock FF is enabled
|
||||
// - AskToJoin is the current saved value
|
||||
if (saved == SecurityAndPrivacyRoomAccess.AskToJoin || isKnockEnabled) {
|
||||
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) },
|
||||
enabled = isKnockEnabled,
|
||||
)
|
||||
}
|
||||
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<Boolean>,
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@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,
|
||||
),
|
||||
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<SecurityAndPrivacyHistoryVisibility>,
|
||||
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 = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* 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
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* 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 dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyNavigator
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
@AssistedInject
|
||||
class EditRoomAddressNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
presenterFactory: EditRoomAddressPresenter.Factory,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
private val navigator = plugins<SecurityAndPrivacyNavigator>().first()
|
||||
private val presenter = presenterFactory.create(navigator)
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
EditRoomAddressView(
|
||||
state = state,
|
||||
onBackClick = ::navigateUp,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,150 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* 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.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 dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.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.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomInfo
|
||||
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
|
||||
|
||||
@AssistedInject
|
||||
class EditRoomAddressPresenter(
|
||||
@Assisted private val navigator: SecurityAndPrivacyNavigator,
|
||||
private val client: MatrixClient,
|
||||
private val room: JoinedRoom,
|
||||
private val roomAliasHelper: RoomAliasHelper,
|
||||
) : Presenter<EditRoomAddressState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(navigator: SecurityAndPrivacyNavigator): EditRoomAddressPresenter
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): EditRoomAddressState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val roomInfo by room.roomInfoFlow.collectAsState()
|
||||
val homeserverName = remember { client.userIdServerName() }
|
||||
val roomAddressValidity = remember {
|
||||
mutableStateOf<RoomAddressValidity>(RoomAddressValidity.Unknown)
|
||||
}
|
||||
val savedRoomAddress by remember { derivedStateOf { roomInfo.firstAliasMatching(homeserverName)?.addressName() } }
|
||||
val saveAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||
var newRoomAddress by remember {
|
||||
mutableStateOf(
|
||||
savedRoomAddress ?: roomAliasHelper.roomAliasNameFromRoomDisplayName(roomInfo.name.orEmpty())
|
||||
)
|
||||
}
|
||||
|
||||
fun handleEvent(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 = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.save(
|
||||
saveAction: MutableState<AsyncAction<Unit>>,
|
||||
serverName: String,
|
||||
newRoomAddress: String,
|
||||
) = launch {
|
||||
suspend {
|
||||
val roomInfo = room.info()
|
||||
val savedCanonicalAlias = roomInfo.canonicalAlias
|
||||
val savedAliasFromHomeserver = roomInfo.firstAliasMatching(serverName)
|
||||
val newRoomAlias = client.roomAliasFromName(newRoomAddress) ?: throw IllegalArgumentException("Invalid room address")
|
||||
|
||||
// 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 = roomInfo.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(roomInfo.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 RoomInfo.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) }
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* 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<Unit>,
|
||||
val eventSink: (EditRoomAddressEvents) -> Unit
|
||||
) {
|
||||
val canBeSaved = roomAddressValidity == RoomAddressValidity.Valid
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* 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<EditRoomAddressState> {
|
||||
override val values: Sequence<EditRoomAddressState>
|
||||
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<Unit> = AsyncAction.Uninitialized,
|
||||
eventSink: (EditRoomAddressEvents) -> Unit = {}
|
||||
) = EditRoomAddressState(
|
||||
roomAddress = roomAddress,
|
||||
roomAddressValidity = roomAddressValidity,
|
||||
homeserverName = homeserverName,
|
||||
saveAction = saveAction,
|
||||
eventSink = eventSink
|
||||
)
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* 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.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.components.Scaffold
|
||||
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,
|
||||
titleStr = stringResource(R.string.screen_edit_room_address_title),
|
||||
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 = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* 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
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* 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.BaseRoom
|
||||
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 BaseRoom.securityAndPrivacyPermissionsAsState(updateKey: Long): State<SecurityAndPrivacyPermissions> {
|
||||
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 },
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue