feature(security&privacy): working SpaceMember selection

This commit is contained in:
ganfra 2026-01-07 11:38:57 +01:00
parent 1930877a81
commit 75ab791629
9 changed files with 37 additions and 54 deletions

View file

@ -66,7 +66,7 @@ class SecurityAndPrivacyFlowNode(
data object EditRoomAddress : NavTarget data object EditRoomAddress : NavTarget
@Parcelize @Parcelize
data class ManageAuthorizedSpaces(val initialSelection: List<RoomId>) : NavTarget data object ManageAuthorizedSpaces: NavTarget
} }
private val callback: SecurityAndPrivacyEntryPoint.Callback = callback() private val callback: SecurityAndPrivacyEntryPoint.Callback = callback()
@ -95,7 +95,7 @@ class SecurityAndPrivacyFlowNode(
val authorizedSpacesData = securityAndPrivacyNode.getAuthorizedSpacesData() val authorizedSpacesData = securityAndPrivacyNode.getAuthorizedSpacesData()
val selectedSpaces = manageAuthorizedSpacesNode.waitForCompletion(authorizedSpacesData) val selectedSpaces = manageAuthorizedSpacesNode.waitForCompletion(authorizedSpacesData)
withContext(NonCancellable) { withContext(NonCancellable) {
backstack.pop() navigator.closeManageAuthorizedSpaces()
securityAndPrivacyNode.onAuthorizedSpacesSelected(selectedSpaces) securityAndPrivacyNode.onAuthorizedSpacesSelected(selectedSpaces)
} }
} }
@ -110,7 +110,7 @@ class SecurityAndPrivacyFlowNode(
NavTarget.EditRoomAddress -> { NavTarget.EditRoomAddress -> {
createNode<EditRoomAddressNode>(buildContext, plugins = listOf(navigator)) createNode<EditRoomAddressNode>(buildContext, plugins = listOf(navigator))
} }
is NavTarget.ManageAuthorizedSpaces -> { NavTarget.ManageAuthorizedSpaces -> {
createNode<ManageAuthorizedSpacesNode>(buildContext, plugins = listOf(navigator)) createNode<ManageAuthorizedSpacesNode>(buildContext, plugins = listOf(navigator))
} }
} }

View file

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

View file

@ -20,7 +20,6 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNavigator 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.architecture.appyx.launchMolecule
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@ -32,16 +31,10 @@ import kotlinx.coroutines.flow.first
class ManageAuthorizedSpacesNode( class ManageAuthorizedSpacesNode(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
presenterFactory: ManageAuthorizedSpacesPresenter.Factory, presenter: ManageAuthorizedSpacesPresenter,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
data class Params(
val initialSelection: List<RoomId>
) : NodeInputs
private val navigator = plugins<SecurityAndPrivacyNavigator>().first() private val navigator = plugins<SecurityAndPrivacyNavigator>().first()
private val presenter = presenterFactory.create(navigator)
private val stateFlow = launchMolecule { presenter.present() } private val stateFlow = launchMolecule { presenter.present() }
suspend fun waitForCompletion(data: AuthorizedSpacesSelection): ImmutableList<RoomId> { suspend fun waitForCompletion(data: AuthorizedSpacesSelection): ImmutableList<RoomId> {
@ -54,7 +47,7 @@ class ManageAuthorizedSpacesNode(
val state by stateFlow.collectAsState() val state by stateFlow.collectAsState()
ManageAuthorizedSpacesView( ManageAuthorizedSpacesView(
state = state, state = state,
onBackClick = ::navigateUp, onBackClick = { navigator.closeManageAuthorizedSpaces() },
modifier = modifier modifier = modifier
) )
} }

View file

@ -13,57 +13,42 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Inject
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.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.core.RoomId
import io.element.android.libraries.matrix.api.room.JoinedRoom
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
@AssistedInject @Inject
class ManageAuthorizedSpacesPresenter( class ManageAuthorizedSpacesPresenter() : Presenter<ManageAuthorizedSpacesState> {
@Assisted private val navigator: SecurityAndPrivacyNavigator,
private val client: MatrixClient,
private val room: JoinedRoom,
) : Presenter<ManageAuthorizedSpacesState> {
@AssistedFactory
interface Factory {
fun create(navigator: SecurityAndPrivacyNavigator): ManageAuthorizedSpacesPresenter
}
@Composable @Composable
override fun present(): ManageAuthorizedSpacesState { override fun present(): ManageAuthorizedSpacesState {
var currentSelection: ImmutableList<RoomId> by remember { mutableStateOf(persistentListOf()) } var selectedIds: ImmutableList<RoomId> by remember { mutableStateOf(persistentListOf()) }
var spacesData by remember { mutableStateOf(AuthorizedSpacesSelection()) } var spacesSelection by remember { mutableStateOf(AuthorizedSpacesSelection()) }
var isSelectionComplete by remember { mutableStateOf(false) } var isSelectionComplete by remember { mutableStateOf(false) }
fun handleEvent(event: ManageAuthorizedSpacesEvent) { fun handleEvent(event: ManageAuthorizedSpacesEvent) {
when (event) { when (event) {
ManageAuthorizedSpacesEvent.Done -> { ManageAuthorizedSpacesEvent.Done ->isSelectionComplete = true
isSelectionComplete = true
}
is ManageAuthorizedSpacesEvent.ToggleSpace -> { is ManageAuthorizedSpacesEvent.ToggleSpace -> {
currentSelection = if (currentSelection.contains(event.roomId)) { selectedIds = if (selectedIds.contains(event.roomId)) {
currentSelection.minus(event.roomId).toPersistentList() selectedIds.minus(event.roomId).toPersistentList()
} else { } else {
currentSelection.plus(event.roomId).toPersistentList() selectedIds.plus(event.roomId).toPersistentList()
} }
} }
is ManageAuthorizedSpacesEvent.SetData -> { is ManageAuthorizedSpacesEvent.SetData -> {
spacesData = event.data spacesSelection = event.data
currentSelection = event.data.initialSelectedIds selectedIds = event.data.initialSelectedIds
} }
} }
} }
return ManageAuthorizedSpacesState( return ManageAuthorizedSpacesState(
selection = spacesData, selection = spacesSelection,
selectedIds = currentSelection, selectedIds = selectedIds,
isSelectionComplete = isSelectionComplete, isSelectionComplete = isSelectionComplete,
eventSink = ::handleEvent, eventSink = ::handleEvent,
) )

View file

@ -18,7 +18,9 @@ data class ManageAuthorizedSpacesState(
val selectedIds: ImmutableList<RoomId>, val selectedIds: ImmutableList<RoomId>,
val isSelectionComplete: Boolean, val isSelectionComplete: Boolean,
val eventSink: (ManageAuthorizedSpacesEvent) -> Unit val eventSink: (ManageAuthorizedSpacesEvent) -> Unit
) ) {
val isDoneButtonEnabled = selectedIds.isNotEmpty()
}
data class AuthorizedSpacesSelection( data class AuthorizedSpacesSelection(
val joinedSpaces: ImmutableList<SpaceRoom> = persistentListOf(), val joinedSpaces: ImmutableList<SpaceRoom> = persistentListOf(),

View file

@ -53,6 +53,7 @@ fun ManageAuthorizedSpacesView(
onDoneClick = { onDoneClick = {
state.eventSink(ManageAuthorizedSpacesEvent.Done) state.eventSink(ManageAuthorizedSpacesEvent.Done)
}, },
isDoneButtonEnabled = state.isDoneButtonEnabled
) )
} }
) { padding -> ) { padding ->
@ -160,6 +161,7 @@ private fun CheckableSpaceListItem(
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun ManageAuthorizedSpacesTopBar( private fun ManageAuthorizedSpacesTopBar(
isDoneButtonEnabled: Boolean,
onBackClick: () -> Unit, onBackClick: () -> Unit,
onDoneClick: () -> Unit, onDoneClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -170,6 +172,7 @@ private fun ManageAuthorizedSpacesTopBar(
navigationIcon = { BackButton(onClick = onBackClick) }, navigationIcon = { BackButton(onClick = onBackClick) },
actions = { actions = {
TextButton( TextButton(
enabled = isDoneButtonEnabled,
text = stringResource(CommonStrings.action_done), text = stringResource(CommonStrings.action_done),
onClick = onDoneClick, onClick = onDoneClick,
) )

View file

@ -47,7 +47,7 @@ class SecurityAndPrivacyNode(
} }
fun getAuthorizedSpacesData(): AuthorizedSpacesSelection{ fun getAuthorizedSpacesData(): AuthorizedSpacesSelection{
return stateFlow.value.getAuthorizedSpaceData() return stateFlow.value.getAuthorizedSpacesSelection()
} }
fun onAuthorizedSpacesSelected(selectedSpaces: ImmutableList<RoomId>) { fun onAuthorizedSpacesSelected(selectedSpaces: ImmutableList<RoomId>) {

View file

@ -118,7 +118,7 @@ class SecurityAndPrivacyPresenter(
address = savedSettings.address, address = savedSettings.address,
) )
val selectableJoinedSpaces by produceState(persistentSetOf()) { val selectableJoinedSpaces by produceState(initialValue = persistentSetOf(), key1 = savedSettings.roomAccess.spaceIds()) {
val joinedParentSpaces = matrixClient val joinedParentSpaces = matrixClient
.spaceService .spaceService
.joinedParents(room.roomId) .joinedParents(room.roomId)
@ -193,7 +193,7 @@ class SecurityAndPrivacyPresenter(
saveAction.value = AsyncAction.Uninitialized saveAction.value = AsyncAction.Uninitialized
} }
SecurityAndPrivacyEvent.ManageAuthorizedSpaces -> { SecurityAndPrivacyEvent.ManageAuthorizedSpaces -> {
navigator.openManageAuthorizedSpaces(editedSettings.roomAccess.spaceIds()) navigator.openManageAuthorizedSpaces()
} }
SecurityAndPrivacyEvent.SelectSpaceMemberAccess -> handleSpaceMemberAccessSelection( SecurityAndPrivacyEvent.SelectSpaceMemberAccess -> handleSpaceMemberAccessSelection(
spaceSelectionMode = spaceSelectionMode, spaceSelectionMode = spaceSelectionMode,
@ -254,9 +254,7 @@ class SecurityAndPrivacyPresenter(
} }
when (spaceSelectionMode) { when (spaceSelectionMode) {
is SpaceSelectionMode.None -> Unit is SpaceSelectionMode.None -> Unit
is SpaceSelectionMode.Multiple -> navigator.openManageAuthorizedSpaces( is SpaceSelectionMode.Multiple -> navigator.openManageAuthorizedSpaces()
initialSelection = spaceIds ,
)
is SpaceSelectionMode.Single -> { is SpaceSelectionMode.Single -> {
val newRoomAccess = SecurityAndPrivacyRoomAccess.SpaceMember( val newRoomAccess = SecurityAndPrivacyRoomAccess.SpaceMember(
spaceIds = persistentListOf(spaceSelectionMode.spaceId) spaceIds = persistentListOf(spaceSelectionMode.spaceId)

View file

@ -42,8 +42,8 @@ data class SecurityAndPrivacyState(
val isSpaceMemberSelectable = isSpaceSettingsEnabled && spaceSelectionMode != SpaceSelectionMode.None val isSpaceMemberSelectable = isSpaceSettingsEnabled && spaceSelectionMode != SpaceSelectionMode.None
// Show SpaceMember option in two cases: // Show SpaceMember option in two cases:
// - the SpaceSettings FF is enabled
// - SpaceMember is the current saved value // - SpaceMember is the current saved value
// - SpaceMember option is selectable (ie. the FF is enabled and there is at least one space to select)
val showSpaceMemberOption = savedSettings.roomAccess is SecurityAndPrivacyRoomAccess.SpaceMember || isSpaceMemberSelectable val showSpaceMemberOption = savedSettings.roomAccess is SecurityAndPrivacyRoomAccess.SpaceMember || isSpaceMemberSelectable
val showManageSpaceAction = spaceSelectionMode is SpaceSelectionMode.Multiple && editedSettings.roomAccess is SecurityAndPrivacyRoomAccess.SpaceMember val showManageSpaceAction = spaceSelectionMode is SpaceSelectionMode.Multiple && editedSettings.roomAccess is SecurityAndPrivacyRoomAccess.SpaceMember
@ -94,13 +94,16 @@ data class SecurityAndPrivacyState(
} }
} }
fun getAuthorizedSpaceData(): AuthorizedSpacesSelection { fun getAuthorizedSpacesSelection(): AuthorizedSpacesSelection {
return AuthorizedSpacesSelection( return AuthorizedSpacesSelection(
joinedSpaces = selectableJoinedSpaces.toImmutableList(), joinedSpaces = selectableJoinedSpaces.toImmutableList(),
unknownSpaceIds = savedSettings.roomAccess.spaceIds().filter { spaceId -> unknownSpaceIds = savedSettings.roomAccess.spaceIds().filter { spaceId ->
selectableJoinedSpaces.none { it.roomId == spaceId } selectableJoinedSpaces.none { it.roomId == spaceId }
}.toImmutableList(), }.toImmutableList(),
initialSelectedIds = editedSettings.roomAccess.spaceIds().toImmutableList() initialSelectedIds = when (editedSettings.roomAccess) {
is SecurityAndPrivacyRoomAccess.SpaceMember -> editedSettings.roomAccess.spaceIds
else -> savedSettings.roomAccess.spaceIds()
}
) )
} }
} }