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 79c0c68ad3..f7bdde7aa0 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 @@ -19,6 +19,7 @@ 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.activeElement import com.bumble.appyx.navmodel.backstack.operation.pop import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject @@ -66,7 +67,7 @@ class SecurityAndPrivacyFlowNode( data object EditRoomAddress : NavTarget @Parcelize - data object ManageAuthorizedSpaces: NavTarget + data class ManageAuthorizedSpaces(val forKnockRestricted: Boolean = false) : NavTarget } private val callback: SecurityAndPrivacyEntryPoint.Callback = callback() @@ -94,9 +95,10 @@ class SecurityAndPrivacyFlowNode( commonLifecycle.coroutineScope.launch { val authorizedSpacesData = securityAndPrivacyNode.getAuthorizedSpacesData() val selectedSpaces = manageAuthorizedSpacesNode.waitForCompletion(authorizedSpacesData) + val forKnock = (backstack.activeElement as? NavTarget.ManageAuthorizedSpaces)?.forKnockRestricted ?: false withContext(NonCancellable) { navigator.closeManageAuthorizedSpaces() - securityAndPrivacyNode.onAuthorizedSpacesSelected(selectedSpaces) + securityAndPrivacyNode.onAuthorizedSpacesSelected(selectedSpaces, forKnock = forKnock) } } } @@ -110,7 +112,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..9690c915d5 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 @@ -18,7 +18,7 @@ interface SecurityAndPrivacyNavigator : Plugin { fun onDone() fun openEditRoomAddress() fun closeEditRoomAddress() - fun openManageAuthorizedSpaces() + fun openManageAuthorizedSpaces(forKnockRestricted: Boolean = false) fun closeManageAuthorizedSpaces() } @@ -38,8 +38,8 @@ class BackstackSecurityAndPrivacyNavigator( backStack.pop() } - override fun openManageAuthorizedSpaces() { - backStack.push(SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces) + override fun openManageAuthorizedSpaces(forKnockRestricted: Boolean) { + backStack.push(SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces(forKnockRestricted)) } override fun closeManageAuthorizedSpaces() { diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyEvent.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyEvent.kt index 0c56a834de..25045a72c1 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyEvent.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyEvent.kt @@ -17,6 +17,8 @@ sealed interface SecurityAndPrivacyEvent { data class ChangeRoomAccess(val roomAccess: SecurityAndPrivacyRoomAccess) : SecurityAndPrivacyEvent // Special case for "Space Members" data object SelectSpaceMemberAccess : SecurityAndPrivacyEvent + // Special case for "Ask to join with Space Members" + data object SelectAskToJoinWithSpaceMembersAccess : SecurityAndPrivacyEvent data object ToggleEncryptionState : SecurityAndPrivacyEvent data object CancelEnableEncryption : SecurityAndPrivacyEvent data object ConfirmEnableEncryption : SecurityAndPrivacyEvent 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 8c41992e7a..7d3c115be5 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 @@ -50,10 +50,13 @@ class SecurityAndPrivacyNode( return stateFlow.value.getAuthorizedSpacesSelection() } - fun onAuthorizedSpacesSelected(selectedSpaces: ImmutableList) { - stateFlow.value.eventSink( - SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.SpaceMember(selectedSpaces)) - ) + fun onAuthorizedSpacesSelected(selectedSpaces: ImmutableList, forKnock: Boolean) { + val roomAccess = if (forKnock) { + SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember(selectedSpaces) + } else { + SecurityAndPrivacyRoomAccess.SpaceMember(selectedSpaces) + } + stateFlow.value.eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(roomAccess)) } @Composable 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 9e2bd6c375..a27884a1ab 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 @@ -200,6 +200,10 @@ class SecurityAndPrivacyPresenter( spaceIds = editedSettings.roomAccess.spaceIds(), editedAccess = editedRoomAccess, ) + SecurityAndPrivacyEvent.SelectAskToJoinWithSpaceMembersAccess -> handleAskToJoinWithSpaceMembersAccessSelection( + spaceSelectionMode = spaceSelectionMode, + editedAccess = editedRoomAccess, + ) } } @@ -254,7 +258,7 @@ class SecurityAndPrivacyPresenter( } when (spaceSelectionMode) { is SpaceSelectionMode.None -> Unit - is SpaceSelectionMode.Multiple -> navigator.openManageAuthorizedSpaces() + is SpaceSelectionMode.Multiple -> navigator.openManageAuthorizedSpaces(forKnockRestricted = false) is SpaceSelectionMode.Single -> { val newRoomAccess = SecurityAndPrivacyRoomAccess.SpaceMember( spaceIds = persistentListOf(spaceSelectionMode.spaceId) @@ -264,6 +268,25 @@ class SecurityAndPrivacyPresenter( } } + private fun handleAskToJoinWithSpaceMembersAccessSelection( + spaceSelectionMode: SpaceSelectionMode, + editedAccess: MutableState, + ) { + if (editedAccess.value is SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember) { + return + } + when (spaceSelectionMode) { + is SpaceSelectionMode.None -> Unit + is SpaceSelectionMode.Multiple -> navigator.openManageAuthorizedSpaces(forKnockRestricted = true) + is SpaceSelectionMode.Single -> { + val newRoomAccess = SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember( + spaceIds = persistentListOf(spaceSelectionMode.spaceId) + ) + editedAccess.value = newRoomAccess + } + } + } + private fun getSpaceSelectionMode( selectableJoinedSpaces: Set, savedAccess: SecurityAndPrivacyRoomAccess, @@ -328,6 +351,7 @@ class SecurityAndPrivacyPresenter( // the room should be automatically made invisible (private) in the room directory. val editedIsVisibleInRoomDirectory = when (editedSettings.roomAccess) { SecurityAndPrivacyRoomAccess.AskToJoin, + is SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember, SecurityAndPrivacyRoomAccess.Anyone -> editedSettings.isVisibleInRoomDirectory.dataOrNull() else -> false } @@ -365,7 +389,13 @@ class SecurityAndPrivacyPresenter( private fun JoinRule?.map(): SecurityAndPrivacyRoomAccess { return when (this) { JoinRule.Public -> SecurityAndPrivacyRoomAccess.Anyone - JoinRule.Knock, is JoinRule.KnockRestricted -> SecurityAndPrivacyRoomAccess.AskToJoin + JoinRule.Knock -> SecurityAndPrivacyRoomAccess.AskToJoin + is JoinRule.KnockRestricted -> SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember( + spaceIds = this.rules + .filterIsInstance() + .map { it.roomId } + .toImmutableList() + ) is JoinRule.Restricted -> SecurityAndPrivacyRoomAccess.SpaceMember( spaceIds = this.rules .filterIsInstance() @@ -388,6 +418,9 @@ private fun SecurityAndPrivacyRoomAccess.map(): JoinRule? { is SecurityAndPrivacyRoomAccess.SpaceMember -> JoinRule.Restricted( rules = this.spaceIds.map { AllowRule.RoomMembership(it) }.toImmutableList() ) + is SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember -> JoinRule.KnockRestricted( + rules = this.spaceIds.map { AllowRule.RoomMembership(it) }.toImmutableList() + ) } } 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 d74ecb13d9..9d47e57846 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 @@ -46,14 +46,25 @@ data class SecurityAndPrivacyState( // - 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 + val showManageSpaceFooter = spaceSelectionMode is SpaceSelectionMode.Multiple && + (editedSettings.roomAccess is SecurityAndPrivacyRoomAccess.SpaceMember || + editedSettings.roomAccess is SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember) 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 isAskToJoinWithSpaceMembersSelectable = isAskToJoinSelectable && isSpaceMemberSelectable + + // Show Ask to join option only when: + // - AskToJoin is the current saved value (legacy), OR + // - Knock FF enabled BUT (SpaceSettings FF disabled OR no spaces available) + val showAskToJoinOption = savedSettings.roomAccess == SecurityAndPrivacyRoomAccess.AskToJoin || + (isAskToJoinSelectable && !isAskToJoinWithSpaceMembersSelectable) + + // Show AskToJoinWithSpaceMember option when: + // - It's the current saved value, OR + // - Both FFs enabled AND spaces available + val showAskToJoinWithSpaceMemberOption = savedSettings.roomAccess is SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember || + isAskToJoinWithSpaceMembersSelectable val canBeSaved = savedSettings != editedSettings @@ -94,6 +105,22 @@ data class SecurityAndPrivacyState( } } + @Composable + fun askToJoinWithSpaceMembersDescription(): String { + return if (isAskToJoinWithSpaceMembersSelectable) { + when (spaceSelectionMode) { + is SpaceSelectionMode.Single -> { + val spaceName = spaceSelectionMode.spaceRoom?.displayName ?: spaceSelectionMode.spaceId.value + stringResource(R.string.screen_security_and_privacy_ask_to_join_single_space_members_option_description, spaceName) + } + is SpaceSelectionMode.None, + is SpaceSelectionMode.Multiple -> stringResource(R.string.screen_security_and_privacy_ask_to_join_multiple_spaces_members_option_description) + } + } else { + stringResource(R.string.screen_security_and_privacy_ask_to_join_option_description) + } + } + fun getAuthorizedSpacesSelection(): AuthorizedSpacesSelection { return AuthorizedSpacesSelection( joinedSpaces = selectableJoinedSpaces.toImmutableList(), @@ -102,6 +129,7 @@ data class SecurityAndPrivacyState( }.toImmutableList(), initialSelectedIds = when (editedSettings.roomAccess) { is SecurityAndPrivacyRoomAccess.SpaceMember -> editedSettings.roomAccess.spaceIds + is SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember -> editedSettings.roomAccess.spaceIds else -> savedSettings.roomAccess.spaceIds() } ) @@ -145,17 +173,19 @@ sealed interface SecurityAndPrivacyRoomAccess { data object AskToJoin : SecurityAndPrivacyRoomAccess data object Anyone : SecurityAndPrivacyRoomAccess data class SpaceMember(val spaceIds: ImmutableList) : SecurityAndPrivacyRoomAccess + data class AskToJoinWithSpaceMember(val spaceIds: ImmutableList) : SecurityAndPrivacyRoomAccess fun canConfigureRoomVisibility(): Boolean { return when (this) { InviteOnly, is SpaceMember -> false - AskToJoin, Anyone -> true + AskToJoin, Anyone, is AskToJoinWithSpaceMember -> true } } fun spaceIds(): ImmutableList { return when (this) { is SpaceMember -> spaceIds + is AskToJoinWithSpaceMember -> spaceIds else -> persistentListOf() } } 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 61218e8897..d253021143 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 @@ -69,6 +69,19 @@ private fun commonSecurityAndPrivacyStates(isSpace: Boolean): Sequence Unit = { lambdaError() }, private val openEditRoomAddressLambda: () -> Unit = { lambdaError() }, private val closeEditRoomAddressLambda: () -> Unit = { lambdaError() }, + private val openManageAuthorizedSpacesLambda: (Boolean) -> Unit = { lambdaError() }, + private val closeManageAuthorizedSpacesLambda: () -> Unit = { lambdaError() }, ) : SecurityAndPrivacyNavigator { override fun onDone() { onDoneLambda() @@ -26,4 +28,12 @@ class FakeSecurityAndPrivacyNavigator( override fun closeEditRoomAddress() { closeEditRoomAddressLambda() } + + override fun openManageAuthorizedSpaces(forKnockRestricted: Boolean) { + openManageAuthorizedSpacesLambda(forKnockRestricted) + } + + override fun closeManageAuthorizedSpaces() { + closeManageAuthorizedSpacesLambda() + } }