feature(security&privacy): working SpaceMember selection

This commit is contained in:
ganfra 2026-01-07 11:38:57 +01:00
parent 4e9d5c533f
commit 0d11c43a9a
9 changed files with 37 additions and 54 deletions

View file

@ -66,7 +66,7 @@ class SecurityAndPrivacyFlowNode(
data object EditRoomAddress : NavTarget
@Parcelize
data class ManageAuthorizedSpaces(val initialSelection: List<RoomId>) : NavTarget
data object ManageAuthorizedSpaces: NavTarget
}
private val callback: SecurityAndPrivacyEntryPoint.Callback = callback()
@ -95,7 +95,7 @@ class SecurityAndPrivacyFlowNode(
val authorizedSpacesData = securityAndPrivacyNode.getAuthorizedSpacesData()
val selectedSpaces = manageAuthorizedSpacesNode.waitForCompletion(authorizedSpacesData)
withContext(NonCancellable) {
backstack.pop()
navigator.closeManageAuthorizedSpaces()
securityAndPrivacyNode.onAuthorizedSpacesSelected(selectedSpaces)
}
}
@ -110,7 +110,7 @@ class SecurityAndPrivacyFlowNode(
NavTarget.EditRoomAddress -> {
createNode<EditRoomAddressNode>(buildContext, plugins = listOf(navigator))
}
is NavTarget.ManageAuthorizedSpaces -> {
NavTarget.ManageAuthorizedSpaces -> {
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.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(initialSelection: List<RoomId>)
fun openManageAuthorizedSpaces()
fun closeManageAuthorizedSpaces()
}
@ -39,8 +38,8 @@ class BackstackSecurityAndPrivacyNavigator(
backStack.pop()
}
override fun openManageAuthorizedSpaces(initialSelection: List<RoomId>) {
backStack.push(SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces(initialSelection))
override fun openManageAuthorizedSpaces() {
backStack.push(SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces)
}
override fun closeManageAuthorizedSpaces() {

View file

@ -20,7 +20,6 @@ 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
@ -32,16 +31,10 @@ import kotlinx.coroutines.flow.first
class ManageAuthorizedSpacesNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
presenterFactory: ManageAuthorizedSpacesPresenter.Factory,
presenter: ManageAuthorizedSpacesPresenter,
) : 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> {
@ -54,7 +47,7 @@ class ManageAuthorizedSpacesNode(
val state by stateFlow.collectAsState()
ManageAuthorizedSpacesView(
state = state,
onBackClick = ::navigateUp,
onBackClick = { navigator.closeManageAuthorizedSpaces() },
modifier = modifier
)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -42,8 +42,8 @@ data class SecurityAndPrivacyState(
val isSpaceMemberSelectable = isSpaceSettingsEnabled && spaceSelectionMode != SpaceSelectionMode.None
// Show SpaceMember option in two cases:
// - the SpaceSettings FF is enabled
// - 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 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(
joinedSpaces = selectableJoinedSpaces.toImmutableList(),
unknownSpaceIds = savedSettings.roomAccess.spaceIds().filter { spaceId ->
selectableJoinedSpaces.none { it.roomId == spaceId }
}.toImmutableList(),
initialSelectedIds = editedSettings.roomAccess.spaceIds().toImmutableList()
initialSelectedIds = when (editedSettings.roomAccess) {
is SecurityAndPrivacyRoomAccess.SpaceMember -> editedSettings.roomAccess.spaceIds
else -> savedSettings.roomAccess.spaceIds()
}
)
}
}