diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt index 844c4f1a70..d29a7ffd20 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNode.kt @@ -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) : 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(buildContext, plugins = listOf(navigator)) } - NavTarget.ManageAuthorizedSpaces -> { + is NavTarget.ManageAuthorizedSpaces -> { createNode(buildContext, plugins = listOf(navigator)) } } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyNavigator.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyNavigator.kt index 274bf0b823..da6ca379e8 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyNavigator.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyNavigator.kt @@ -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) fun closeManageAuthorizedSpaces() } @@ -38,8 +39,8 @@ class BackstackSecurityAndPrivacyNavigator( backStack.pop() } - override fun openManageAuthorizedSpaces() { - backStack.push(SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces) + override fun openManageAuthorizedSpaces(initialSelection: List) { + backStack.push(SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces(initialSelection)) } override fun closeManageAuthorizedSpaces() { diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesEvent.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesEvent.kt index 0515878ade..47abe2fbce 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesEvent.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesEvent.kt @@ -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 } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt index dfb7d6c833..5608c0ce16 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesNode.kt @@ -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, presenterFactory: ManageAuthorizedSpacesPresenter.Factory, ) : Node(buildContext, plugins = plugins) { + + data class Params( + val initialSelection: List + ) : NodeInputs + private val navigator = plugins().first() private val presenter = presenterFactory.create(navigator) + private val stateFlow = launchMolecule { presenter.present() } + + suspend fun waitForCompletion(data: AuthorizedSpacesSelection): ImmutableList { + 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, diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt index c815bb91b7..09869cdf09 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt @@ -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 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, ) } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt index 688ef6e6e9..7564fabae9 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt @@ -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, - val unknownSpaceIds: ImmutableList, - val currentSelection: ImmutableList, - val initialSelection: ImmutableList, + val selection: AuthorizedSpacesSelection, + val selectedIds: ImmutableList, + val isSelectionComplete: Boolean, val eventSink: (ManageAuthorizedSpacesEvent) -> Unit ) + +data class AuthorizedSpacesSelection( + val joinedSpaces: ImmutableList = persistentListOf(), + val unknownSpaceIds: ImmutableList = persistentListOf(), + val initialSelectedIds: ImmutableList = persistentListOf() +) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt index d91e16f3e6..e232d02022 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt @@ -20,11 +20,15 @@ open class ManageAuthorizedSpacesStateProvider : PreviewParameterProvider { } } -private fun aManageAuthorizedSpacesState( +fun anAuthorizedSpacesData( joinedSpaces: List = aSpaceRoomList(5), unknownSpaceIds: List = emptyList(), - currentSelection: List = emptyList(), initialSelection: List = 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 = emptyList(), + eventSink: (ManageAuthorizedSpacesEvent) -> Unit = {}, +) = ManageAuthorizedSpacesState( + selection = authorizedSpacesData, + selectedIds = currentSelection.toImmutableList(), + isSelectionComplete = false, eventSink = eventSink, ) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt index 513ecc980a..1c4ff7bc77 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt @@ -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) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyNode.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyNode.kt index d5fb72e72e..1e5e6e0943 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyNode.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyNode.kt @@ -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) { + 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 ) } + } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt index e025914801..bc7688dab3 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt @@ -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, editedAccess: MutableState, ) { 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, 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) } } } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt index bcbee0203d..d14d83b16b 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt @@ -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, val isSpace: Boolean, private val permissions: SecurityAndPrivacyPermissions, private val selectableJoinedSpaces: ImmutableSet, - 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 { diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt index 312043324e..61218e8897 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt @@ -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 { @@ -122,7 +121,7 @@ fun aSecurityAndPrivacyState( isKnockEnabled: Boolean = true, isSpace: Boolean = false, selectableJoinedSpaces: Set = 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, ) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt index 374a4e3911..aedea1879c 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt @@ -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,