feature(security&privacy): iterate on SpaceMember option

This commit is contained in:
ganfra
2026-01-06 15:15:38 +01:00
parent 96745c765a
commit 1930877a81
13 changed files with 226 additions and 88 deletions

View File

@@ -12,12 +12,14 @@ import android.os.Parcelable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
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 com.bumble.appyx.navmodel.backstack.operation.pop
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
@@ -31,12 +33,15 @@ import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.powerlevels.use
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
@ContributesNode(RoomScope::class)
@@ -61,7 +66,7 @@ class SecurityAndPrivacyFlowNode(
data object EditRoomAddress : NavTarget
@Parcelize
data object ManageAuthorizedSpaces : NavTarget
data class ManageAuthorizedSpaces(val initialSelection: List<RoomId>) : NavTarget
}
private val callback: SecurityAndPrivacyEntryPoint.Callback = callback()
@@ -83,6 +88,18 @@ class SecurityAndPrivacyFlowNode(
callback.onDone()
}
}
whenChildrenAttached { commonLifecycle: Lifecycle,
securityAndPrivacyNode: SecurityAndPrivacyNode,
manageAuthorizedSpacesNode: ManageAuthorizedSpacesNode ->
commonLifecycle.coroutineScope.launch {
val authorizedSpacesData = securityAndPrivacyNode.getAuthorizedSpacesData()
val selectedSpaces = manageAuthorizedSpacesNode.waitForCompletion(authorizedSpacesData)
withContext(NonCancellable) {
backstack.pop()
securityAndPrivacyNode.onAuthorizedSpacesSelected(selectedSpaces)
}
}
}
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@@ -93,7 +110,7 @@ class SecurityAndPrivacyFlowNode(
NavTarget.EditRoomAddress -> {
createNode<EditRoomAddressNode>(buildContext, plugins = listOf(navigator))
}
NavTarget.ManageAuthorizedSpaces -> {
is NavTarget.ManageAuthorizedSpaces -> {
createNode<ManageAuthorizedSpacesNode>(buildContext, plugins = listOf(navigator))
}
}

View File

@@ -13,12 +13,13 @@ import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push
import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyEntryPoint
import io.element.android.libraries.matrix.api.core.RoomId
interface SecurityAndPrivacyNavigator : Plugin {
fun onDone()
fun openEditRoomAddress()
fun closeEditRoomAddress()
fun openManageAuthorizedSpaces()
fun openManageAuthorizedSpaces(initialSelection: List<RoomId>)
fun closeManageAuthorizedSpaces()
}
@@ -38,8 +39,8 @@ class BackstackSecurityAndPrivacyNavigator(
backStack.pop()
}
override fun openManageAuthorizedSpaces() {
backStack.push(SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces)
override fun openManageAuthorizedSpaces(initialSelection: List<RoomId>) {
backStack.push(SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces(initialSelection))
}
override fun closeManageAuthorizedSpaces() {

View File

@@ -11,6 +11,7 @@ package io.element.android.features.securityandprivacy.impl.manageauthorizedspac
import io.element.android.libraries.matrix.api.core.RoomId
sealed interface ManageAuthorizedSpacesEvent {
data class SetData(val data: AuthorizedSpacesSelection) : ManageAuthorizedSpacesEvent
data object Done : ManageAuthorizedSpacesEvent
data class ToggleSpace(val roomId: RoomId) : ManageAuthorizedSpacesEvent
}

View File

@@ -9,6 +9,8 @@
package io.element.android.features.securityandprivacy.impl.manageauthorizedspaces
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
@@ -18,7 +20,12 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNavigator
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.appyx.launchMolecule
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.first
@ContributesNode(RoomScope::class)
@AssistedInject
@@ -27,12 +34,24 @@ class ManageAuthorizedSpacesNode(
@Assisted plugins: List<Plugin>,
presenterFactory: ManageAuthorizedSpacesPresenter.Factory,
) : Node(buildContext, plugins = plugins) {
data class Params(
val initialSelection: List<RoomId>
) : NodeInputs
private val navigator = plugins<SecurityAndPrivacyNavigator>().first()
private val presenter = presenterFactory.create(navigator)
private val stateFlow = launchMolecule { presenter.present() }
suspend fun waitForCompletion(data: AuthorizedSpacesSelection): ImmutableList<RoomId> {
stateFlow.value.eventSink(ManageAuthorizedSpacesEvent.SetData(data))
return stateFlow.first { it.isSelectionComplete }.selectedIds
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
val state by stateFlow.collectAsState()
ManageAuthorizedSpacesView(
state = state,
onBackClick = ::navigateUp,

View File

@@ -9,16 +9,21 @@
package io.element.android.features.securityandprivacy.impl.manageauthorizedspaces
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.securityandprivacy.impl.SecurityAndPrivacyNavigator
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.JoinedRoom
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
@AssistedInject
class ManageAuthorizedSpacesPresenter(
@@ -33,19 +38,33 @@ class ManageAuthorizedSpacesPresenter(
@Composable
override fun present(): ManageAuthorizedSpacesState {
val roomInfo by room.roomInfoFlow.collectAsState()
var currentSelection: ImmutableList<RoomId> by remember { mutableStateOf(persistentListOf()) }
var spacesData by remember { mutableStateOf(AuthorizedSpacesSelection()) }
var isSelectionComplete by remember { mutableStateOf(false) }
fun handleEvent(event: ManageAuthorizedSpacesEvent) {
when (event) {
ManageAuthorizedSpacesEvent.Done -> TODO()
is ManageAuthorizedSpacesEvent.ToggleSpace -> TODO()
ManageAuthorizedSpacesEvent.Done -> {
isSelectionComplete = true
}
is ManageAuthorizedSpacesEvent.ToggleSpace -> {
currentSelection = if (currentSelection.contains(event.roomId)) {
currentSelection.minus(event.roomId).toPersistentList()
} else {
currentSelection.plus(event.roomId).toPersistentList()
}
}
is ManageAuthorizedSpacesEvent.SetData -> {
spacesData = event.data
currentSelection = event.data.initialSelectedIds
}
}
}
return ManageAuthorizedSpacesState(
joinedSpaces = persistentListOf(),
unknownSpaceIds = persistentListOf(),
currentSelection = persistentListOf(),
initialSelection = persistentListOf(),
selection = spacesData,
selectedIds = currentSelection,
isSelectionComplete = isSelectionComplete,
eventSink = ::handleEvent,
)
}

View File

@@ -11,11 +11,17 @@ package io.element.android.features.securityandprivacy.impl.manageauthorizedspac
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
data class ManageAuthorizedSpacesState(
val joinedSpaces: ImmutableList<SpaceRoom>,
val unknownSpaceIds: ImmutableList<RoomId>,
val currentSelection: ImmutableList<RoomId>,
val initialSelection: ImmutableList<RoomId>,
val selection: AuthorizedSpacesSelection,
val selectedIds: ImmutableList<RoomId>,
val isSelectionComplete: Boolean,
val eventSink: (ManageAuthorizedSpacesEvent) -> Unit
)
data class AuthorizedSpacesSelection(
val joinedSpaces: ImmutableList<SpaceRoom> = persistentListOf(),
val unknownSpaceIds: ImmutableList<RoomId> = persistentListOf(),
val initialSelectedIds: ImmutableList<RoomId> = persistentListOf()
)

View File

@@ -20,11 +20,15 @@ open class ManageAuthorizedSpacesStateProvider : PreviewParameterProvider<Manage
get() = sequenceOf(
aManageAuthorizedSpacesState(),
aManageAuthorizedSpacesState(
unknownSpaceIds = listOf(aRoomId(99))
authorizedSpacesData = anAuthorizedSpacesData(
unknownSpaceIds = listOf(aRoomId(99))
)
),
aManageAuthorizedSpacesState(
currentSelection = listOf(aRoomId(1), aRoomId(3)),
initialSelection = listOf(aRoomId(1)),
authorizedSpacesData = anAuthorizedSpacesData(
initialSelection = listOf(aRoomId(1)),
),
),
)
}
@@ -45,17 +49,24 @@ private fun aSpaceRoomList(count: Int): List<SpaceRoom> {
}
}
private fun aManageAuthorizedSpacesState(
fun anAuthorizedSpacesData(
joinedSpaces: List<SpaceRoom> = aSpaceRoomList(5),
unknownSpaceIds: List<RoomId> = emptyList(),
currentSelection: List<RoomId> = emptyList(),
initialSelection: List<RoomId> = emptyList(),
eventSink: (ManageAuthorizedSpacesEvent) -> Unit = {},
) = ManageAuthorizedSpacesState(
) = AuthorizedSpacesSelection(
joinedSpaces = joinedSpaces.toImmutableList(),
unknownSpaceIds = unknownSpaceIds.toImmutableList(),
currentSelection = currentSelection.toImmutableList(),
initialSelection = initialSelection.toImmutableList(),
initialSelectedIds = initialSelection.toImmutableList(),
)
private fun aManageAuthorizedSpacesState(
authorizedSpacesData: AuthorizedSpacesSelection = anAuthorizedSpacesData(),
currentSelection: List<RoomId> = emptyList(),
eventSink: (ManageAuthorizedSpacesEvent) -> Unit = {},
) = ManageAuthorizedSpacesState(
selection = authorizedSpacesData,
selectedIds = currentSelection.toImmutableList(),
isSelectionComplete = false,
eventSink = eventSink,
)

View File

@@ -66,12 +66,12 @@ fun ManageAuthorizedSpacesView(
hasDivider = false,
)
}
items(items = state.joinedSpaces) { space ->
items(items = state.selection.joinedSpaces) { space ->
CheckableSpaceListItem(
headlineText = space.displayName,
supportingText = space.canonicalAlias?.value,
avatarData = space.getAvatarData(AvatarSize.SpaceMember),
checked = state.currentSelection.contains(space.roomId),
checked = state.selectedIds.contains(space.roomId),
onCheckedChange = { _ ->
state.eventSink(
ManageAuthorizedSpacesEvent.ToggleSpace(space.roomId)
@@ -79,19 +79,19 @@ fun ManageAuthorizedSpacesView(
}
)
}
if (state.unknownSpaceIds.isNotEmpty()) {
if (state.selection.unknownSpaceIds.isNotEmpty()) {
item {
ListSectionHeader(
title = stringResource(R.string.screen_manage_authorized_spaces_unknown_spaces_section_title),
hasDivider = true,
)
}
items(items = state.unknownSpaceIds) {
items(items = state.selection.unknownSpaceIds) {
CheckableSpaceListItem(
headlineText = stringResource(R.string.screen_manage_authorized_spaces_unknown_space),
supportingText = it.value,
avatarData = null,
checked = state.currentSelection.contains(it),
checked = state.selectedIds.contains(it),
onCheckedChange = { _ ->
state.eventSink(
ManageAuthorizedSpacesEvent.ToggleSpace(it)

View File

@@ -23,9 +23,12 @@ import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNavigator
import io.element.android.features.securityandprivacy.impl.manageauthorizedspaces.AuthorizedSpacesSelection
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
import io.element.android.libraries.architecture.appyx.launchMolecule
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.collections.immutable.ImmutableList
@ContributesNode(RoomScope::class)
@AssistedInject
@@ -43,6 +46,16 @@ class SecurityAndPrivacyNode(
activity.openUrlInChromeCustomTab(null, darkTheme, url)
}
fun getAuthorizedSpacesData(): AuthorizedSpacesSelection{
return stateFlow.value.getAuthorizedSpaceData()
}
fun onAuthorizedSpacesSelected(selectedSpaces: ImmutableList<RoomId>) {
stateFlow.value.eventSink(
SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.SpaceMember(selectedSpaces))
)
}
@Composable
override fun View(modifier: Modifier) {
val activity = requireNotNull(LocalActivity.current)
@@ -56,4 +69,5 @@ class SecurityAndPrivacyNode(
modifier = modifier
)
}
}

View File

@@ -35,6 +35,7 @@ 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.core.RoomId
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
@@ -130,9 +131,9 @@ class SecurityAndPrivacyPresenter(
value = (joinedParentSpaces + nonParentJoinedSpaces).toImmutableSet()
}
val spaceSelection by remember {
val spaceSelectionMode by remember {
derivedStateOf {
getSpaceSelection(selectableJoinedSpaces, savedSettings.roomAccess)
getSpaceSelectionMode(selectableJoinedSpaces, savedSettings.roomAccess)
}
}
@@ -191,9 +192,12 @@ class SecurityAndPrivacyPresenter(
SecurityAndPrivacyEvent.DismissExitConfirmation -> {
saveAction.value = AsyncAction.Uninitialized
}
SecurityAndPrivacyEvent.ManageAuthorizedSpaces -> navigator.openManageAuthorizedSpaces()
SecurityAndPrivacyEvent.ManageAuthorizedSpaces -> {
navigator.openManageAuthorizedSpaces(editedSettings.roomAccess.spaceIds())
}
SecurityAndPrivacyEvent.SelectSpaceMemberAccess -> handleSpaceMemberAccessSelection(
spaceSelection = spaceSelection,
spaceSelectionMode = spaceSelectionMode,
spaceIds = editedSettings.roomAccess.spaceIds(),
editedAccess = editedRoomAccess,
)
}
@@ -216,7 +220,7 @@ class SecurityAndPrivacyPresenter(
isSpace = roomInfo.isSpace,
isSpaceSettingsEnabled = isSpaceSettingsEnabled,
selectableJoinedSpaces = selectableJoinedSpaces,
spaceSelection = spaceSelection,
spaceSelectionMode = spaceSelectionMode,
eventSink = ::handleEvent,
)
@@ -241,42 +245,45 @@ class SecurityAndPrivacyPresenter(
}
private fun handleSpaceMemberAccessSelection(
spaceSelection: SpaceSelection,
spaceSelectionMode: SpaceSelectionMode,
spaceIds: List<RoomId>,
editedAccess: MutableState<SecurityAndPrivacyRoomAccess>,
) {
if (editedAccess.value is SecurityAndPrivacyRoomAccess.SpaceMember) {
return
}
when (spaceSelection) {
is SpaceSelection.None -> Unit
is SpaceSelection.Multiple -> navigator.openManageAuthorizedSpaces()
is SpaceSelection.Single -> {
when (spaceSelectionMode) {
is SpaceSelectionMode.None -> Unit
is SpaceSelectionMode.Multiple -> navigator.openManageAuthorizedSpaces(
initialSelection = spaceIds ,
)
is SpaceSelectionMode.Single -> {
val newRoomAccess = SecurityAndPrivacyRoomAccess.SpaceMember(
spaceIds = persistentListOf(spaceSelection.spaceId)
spaceIds = persistentListOf(spaceSelectionMode.spaceId)
)
editedAccess.value = newRoomAccess
}
}
}
private fun getSpaceSelection(
private fun getSpaceSelectionMode(
selectableJoinedSpaces: Set<SpaceRoom>,
savedAccess: SecurityAndPrivacyRoomAccess,
): SpaceSelection {
): SpaceSelectionMode {
val selectableSpacesCount = (selectableJoinedSpaces.map { it.roomId } + savedAccess.spaceIds()).toSet().size
return when {
selectableSpacesCount == 0 -> SpaceSelection.None
selectableSpacesCount > 1 -> SpaceSelection.Multiple
selectableSpacesCount == 0 -> SpaceSelectionMode.None
selectableSpacesCount > 1 -> SpaceSelectionMode.Multiple
else -> {
val joinedSpace = selectableJoinedSpaces.firstOrNull()
if (joinedSpace != null) {
SpaceSelection.Single(joinedSpace.roomId, joinedSpace)
SpaceSelectionMode.Single(joinedSpace.roomId, joinedSpace)
} else {
val spaceId = savedAccess.spaceIds().firstOrNull()
if (spaceId == null) {
SpaceSelection.None
SpaceSelectionMode.None
} else {
SpaceSelection.Single(spaceId, null)
SpaceSelectionMode.Single(spaceId, null)
}
}
}

View File

@@ -8,7 +8,11 @@
package io.element.android.features.securityandprivacy.impl.root
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyPermissions
import io.element.android.features.securityandprivacy.impl.R
import io.element.android.features.securityandprivacy.impl.manageauthorizedspaces.AuthorizedSpacesSelection
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.RoomId
@@ -25,16 +29,32 @@ data class SecurityAndPrivacyState(
val editedSettings: SecurityAndPrivacySettings,
val homeserverName: String,
val showEnableEncryptionConfirmation: Boolean,
val isKnockEnabled: Boolean,
val isSpaceSettingsEnabled: Boolean,
private val isKnockEnabled: Boolean,
private val isSpaceSettingsEnabled: Boolean,
val saveAction: AsyncAction<Unit>,
val isSpace: Boolean,
private val permissions: SecurityAndPrivacyPermissions,
private val selectableJoinedSpaces: ImmutableSet<SpaceRoom>,
private val spaceSelection: SpaceSelection,
private val spaceSelectionMode: SpaceSelectionMode,
val eventSink: (SecurityAndPrivacyEvent) -> Unit
) {
val isSpaceMemberSelectable = isSpaceSettingsEnabled && spaceSelectionMode != SpaceSelectionMode.None
// Show SpaceMember option in two cases:
// - the SpaceSettings FF is enabled
// - SpaceMember is the current saved value
val showSpaceMemberOption = savedSettings.roomAccess is SecurityAndPrivacyRoomAccess.SpaceMember || isSpaceMemberSelectable
val showManageSpaceAction = spaceSelectionMode is SpaceSelectionMode.Multiple && editedSettings.roomAccess is SecurityAndPrivacyRoomAccess.SpaceMember
val isAskToJoinSelectable = isKnockEnabled
// Show Ask to join option in two cases:
// - the Knock FF is enabled
// - AskToJoin is the current saved value
val showAskToJoinOption = savedSettings.roomAccess == SecurityAndPrivacyRoomAccess.AskToJoin || isAskToJoinSelectable
val canBeSaved = savedSettings != editedSettings
// Logic is in https://github.com/element-hq/element-meta/issues/3029
@@ -57,6 +77,32 @@ data class SecurityAndPrivacyState(
val showHistoryVisibilitySection = permissions.canChangeHistoryVisibility && !isSpace
val showEncryptionSection = permissions.canChangeEncryption && !isSpace
@Composable
fun spaceMemberDescription(): String {
return if (isSpaceMemberSelectable) {
when (spaceSelectionMode) {
is SpaceSelectionMode.Single -> {
val spaceName = spaceSelectionMode.spaceRoom?.displayName ?: spaceSelectionMode.spaceId.value
stringResource(R.string.screen_security_and_privacy_room_access_space_members_option_single_parent_description, spaceName)
}
is SpaceSelectionMode.None,
is SpaceSelectionMode.Multiple -> stringResource(R.string.screen_security_and_privacy_room_access_space_members_option_multiple_parents_description)
}
} else {
stringResource(R.string.screen_security_and_privacy_room_access_space_members_option_unavailable_description)
}
}
fun getAuthorizedSpaceData(): AuthorizedSpacesSelection {
return AuthorizedSpacesSelection(
joinedSpaces = selectableJoinedSpaces.toImmutableList(),
unknownSpaceIds = savedSettings.roomAccess.spaceIds().filter { spaceId ->
selectableJoinedSpaces.none { it.roomId == spaceId }
}.toImmutableList(),
initialSelectedIds = editedSettings.roomAccess.spaceIds().toImmutableList()
)
}
}
data class SecurityAndPrivacySettings(
@@ -85,10 +131,10 @@ enum class SecurityAndPrivacyHistoryVisibility {
}
}
sealed interface SpaceSelection {
data object None : SpaceSelection
data class Single(val spaceId: RoomId, val spaceRoom: SpaceRoom?) : SpaceSelection
data object Multiple : SpaceSelection
sealed interface SpaceSelectionMode {
data object None : SpaceSelectionMode
data class Single(val spaceId: RoomId, val spaceRoom: SpaceRoom?) : SpaceSelectionMode
data object Multiple : SpaceSelectionMode
}
sealed interface SecurityAndPrivacyRoomAccess {

View File

@@ -14,7 +14,6 @@ import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
open class SecurityAndPrivacyStateProvider : PreviewParameterProvider<SecurityAndPrivacyState> {
@@ -122,7 +121,7 @@ fun aSecurityAndPrivacyState(
isKnockEnabled: Boolean = true,
isSpace: Boolean = false,
selectableJoinedSpaces: Set<SpaceRoom> = emptySet(),
spaceSelection: SpaceSelection = SpaceSelection.None,
spaceSelectionMode: SpaceSelectionMode = SpaceSelectionMode.None,
isSpaceSettingsEnabled: Boolean = true,
eventSink: (SecurityAndPrivacyEvent) -> Unit = {}
) = SecurityAndPrivacyState(
@@ -135,7 +134,7 @@ fun aSecurityAndPrivacyState(
permissions = permissions,
isSpace = isSpace,
selectableJoinedSpaces = selectableJoinedSpaces.toImmutableSet(),
spaceSelection = SpaceSelection.None,
spaceSelectionMode = spaceSelectionMode,
isSpaceSettingsEnabled = isSpaceSettingsEnabled,
eventSink = eventSink,
)

View File

@@ -90,14 +90,8 @@ fun SecurityAndPrivacyView(
) {
if (state.showRoomAccessSection) {
RoomAccessSection(
state = state,
modifier = Modifier.padding(top = 24.dp),
edited = state.editedSettings.roomAccess,
saved = state.savedSettings.roomAccess,
isKnockEnabled = state.isKnockEnabled,
isSpaceSettingsEnabled = state.isSpaceSettingsEnabled,
onSelectOption = { state.eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(it)) },
onManageSpacesClick = { state.eventSink(SecurityAndPrivacyEvent.ManageAuthorizedSpaces) },
onSpaceMemberAccessClick = { state.eventSink(SecurityAndPrivacyEvent.SelectSpaceMemberAccess) }
)
}
if (state.showRoomVisibilitySections) {
@@ -211,15 +205,25 @@ private fun SecurityAndPrivacySection(
@Composable
private fun RoomAccessSection(
edited: SecurityAndPrivacyRoomAccess,
saved: SecurityAndPrivacyRoomAccess,
isKnockEnabled: Boolean,
isSpaceSettingsEnabled: Boolean,
onSelectOption: (SecurityAndPrivacyRoomAccess) -> Unit,
onSpaceMemberAccessClick: () -> Unit,
onManageSpacesClick: () -> Unit,
state: SecurityAndPrivacyState,
modifier: Modifier = Modifier,
) {
val edited = state.editedSettings.roomAccess
val saved = state.savedSettings.roomAccess
fun onSelectOption(option: SecurityAndPrivacyRoomAccess) {
state.eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(option))
}
fun onSpaceMemberAccessClick() {
state.eventSink(SecurityAndPrivacyEvent.SelectSpaceMemberAccess)
}
fun onManageSpacesClick() {
state.eventSink(SecurityAndPrivacyEvent.ManageAuthorizedSpaces)
}
SecurityAndPrivacySection(
title = stringResource(R.string.screen_security_and_privacy_room_access_section_header),
modifier = modifier,
@@ -231,31 +235,25 @@ private fun RoomAccessSection(
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Public())),
onClick = { onSelectOption(SecurityAndPrivacyRoomAccess.Anyone) },
)
// Show SpaceMember option in two cases:
// - the SpaceSettings FF is enabled
// - SpaceMember is the current saved value
if (saved is SecurityAndPrivacyRoomAccess.SpaceMember || isSpaceSettingsEnabled)
if (state.showSpaceMemberOption)
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_unavailable_description))
Text(text = state.spaceMemberDescription())
},
trailingContent = ListItemContent.RadioButton(selected = edited is SecurityAndPrivacyRoomAccess.SpaceMember),
trailingContent = ListItemContent.RadioButton(selected = state.editedSettings.roomAccess is SecurityAndPrivacyRoomAccess.SpaceMember),
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Space())),
onClick = onSpaceMemberAccessClick,
enabled = isSpaceSettingsEnabled,
onClick = ::onSpaceMemberAccessClick,
enabled = state.isSpaceMemberSelectable,
)
// Show Ask to join option in two cases:
// - the Knock FF is enabled
// - AskToJoin is the current saved value
if (saved == SecurityAndPrivacyRoomAccess.AskToJoin || isKnockEnabled) {
if (state.showAskToJoinOption) {
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) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserAdd())),
enabled = isKnockEnabled,
enabled = state.isAskToJoinSelectable,
)
}
ListItem(
@@ -265,11 +263,11 @@ private fun RoomAccessSection(
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Lock())),
onClick = { onSelectOption(SecurityAndPrivacyRoomAccess.InviteOnly) },
)
if (edited is SecurityAndPrivacyRoomAccess.SpaceMember) {
if (state.showManageSpaceAction) {
val footerText = stringWithLink(
textRes = R.string.screen_security_and_privacy_room_access_footer,
url = stringResource(R.string.screen_security_and_privacy_room_access_footer_manage_spaces_action),
onLinkClick = { onManageSpacesClick() },
onLinkClick = {onManageSpacesClick()},
)
Text(
text = footerText,