From 79de4514b8362acff6be7dd37648fe8d4835efba Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 11 Dec 2025 17:39:10 +0100 Subject: [PATCH] change(room permissions): ensure closing screen without permissions --- .../api/RolesAndPermissionsEntryPoint.kt | 17 +++++++++-- .../DefaultRolesAndPermissionsEntryPoint.kt | 8 +++-- .../impl/RolesAndPermissionsFlowNode.kt | 21 ++++++++++++++ .../impl/root/RolesAndPermissionsNode.kt | 24 --------------- .../test/FakeRolesAndPermissionsEntryPoint.kt | 2 +- .../roomdetails/impl/RoomDetailsFlowNode.kt | 11 ++++++- .../impl/SecurityAndPrivacyFlowNode.kt | 29 +++++++++++++++++++ .../impl/settings/SpaceSettingsFlowNode.kt | 6 ++++ 8 files changed, 88 insertions(+), 30 deletions(-) diff --git a/features/rolesandpermissions/api/src/main/kotlin/io/element/android/features/rolesandpermissions/api/RolesAndPermissionsEntryPoint.kt b/features/rolesandpermissions/api/src/main/kotlin/io/element/android/features/rolesandpermissions/api/RolesAndPermissionsEntryPoint.kt index 1e9fe6a1c4..bd3cea0439 100644 --- a/features/rolesandpermissions/api/src/main/kotlin/io/element/android/features/rolesandpermissions/api/RolesAndPermissionsEntryPoint.kt +++ b/features/rolesandpermissions/api/src/main/kotlin/io/element/android/features/rolesandpermissions/api/RolesAndPermissionsEntryPoint.kt @@ -8,6 +8,19 @@ package io.element.android.features.rolesandpermissions.api -import io.element.android.libraries.architecture.SimpleFeatureEntryPoint +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import io.element.android.libraries.architecture.FeatureEntryPoint -fun interface RolesAndPermissionsEntryPoint : SimpleFeatureEntryPoint +fun interface RolesAndPermissionsEntryPoint : FeatureEntryPoint { + interface Callback : Plugin { + fun onDone() + } + + fun createNode( + parentNode: Node, + buildContext: BuildContext, + callback: Callback, + ): Node +} diff --git a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/DefaultRolesAndPermissionsEntryPoint.kt b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/DefaultRolesAndPermissionsEntryPoint.kt index 2f281a596a..5f27365857 100644 --- a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/DefaultRolesAndPermissionsEntryPoint.kt +++ b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/DefaultRolesAndPermissionsEntryPoint.kt @@ -17,7 +17,11 @@ import io.element.android.libraries.di.RoomScope @ContributesBinding(RoomScope::class) class DefaultRolesAndPermissionsEntryPoint : RolesAndPermissionsEntryPoint { - override fun createNode(parentNode: Node, buildContext: BuildContext): Node { - return parentNode.createNode(buildContext) + override fun createNode( + parentNode: Node, + buildContext: BuildContext, + callback: RolesAndPermissionsEntryPoint.Callback, + ): Node { + return parentNode.createNode(buildContext, listOf(callback)) } } diff --git a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/RolesAndPermissionsFlowNode.kt b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/RolesAndPermissionsFlowNode.kt index 5966a4f0ed..4bd88071c3 100644 --- a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/RolesAndPermissionsFlowNode.kt +++ b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/RolesAndPermissionsFlowNode.kt @@ -14,7 +14,10 @@ import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +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 @@ -25,17 +28,24 @@ import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode import io.element.android.features.rolesandpermissions.api.ChangeRoomMemberRolesListType +import io.element.android.features.rolesandpermissions.api.RolesAndPermissionsEntryPoint import io.element.android.features.rolesandpermissions.impl.permissions.ChangeRoomPermissionsNode import io.element.android.features.rolesandpermissions.impl.roles.ChangeRolesNode import io.element.android.features.rolesandpermissions.impl.root.RolesAndPermissionsNode import io.element.android.libraries.architecture.BackstackView 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.designsystem.components.async.AsyncIndicator import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost import io.element.android.libraries.designsystem.components.async.AsyncIndicatorState import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.room.JoinedRoom +import io.element.android.libraries.matrix.api.room.powerlevels.canEditRolesAndPermissions +import io.element.android.libraries.matrix.api.room.powerlevels.permissionsFlow import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize @@ -44,6 +54,7 @@ import kotlinx.parcelize.Parcelize class RolesAndPermissionsFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, + private val room: JoinedRoom, ) : BaseFlowNode( backstack = BackStack( initialElement = NavTarget.Root, @@ -66,6 +77,7 @@ class RolesAndPermissionsFlowNode( data object ChangeRoomPermissions : NavTarget } + private val callback: RolesAndPermissionsEntryPoint.Callback = callback() private val asyncIndicatorState = AsyncIndicatorState() override fun onBuilt() { @@ -76,6 +88,15 @@ class RolesAndPermissionsFlowNode( onChangeComplete(changesSaved) } } + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + room.permissionsFlow(false) { perms -> perms.canEditRolesAndPermissions() } + .filter { canEdit -> !canEdit } + .first() + // If the user can no longer edit roles and permissions, exit the flow + callback.onDone() + } + } } private fun onChangeComplete(changesSaved: Boolean) { diff --git a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsNode.kt b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsNode.kt index 4469eb8f37..d6efa661bc 100644 --- a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsNode.kt +++ b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsNode.kt @@ -11,7 +11,6 @@ package io.element.android.features.rolesandpermissions.impl.root import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier -import androidx.lifecycle.lifecycleScope import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin @@ -21,13 +20,6 @@ import io.element.android.annotations.ContributesNode import io.element.android.libraries.architecture.callback import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.room.BaseRoom -import io.element.android.libraries.matrix.api.room.RoomMember -import io.element.android.libraries.matrix.ui.model.roleOf -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.take -import kotlinx.coroutines.launch @ContributesNode(RoomScope::class) @AssistedInject @@ -54,22 +46,6 @@ class RolesAndPermissionsNode( } } - override fun onBuilt() { - super.onBuilt() - - // If the user is not an admin anymore, exit this section since they won't have permissions to use it - lifecycleScope.launch { - room.roomInfoFlow - .filter { info -> - val role = info.roleOf(room.sessionId) - role != RoomMember.Role.Admin && role !is RoomMember.Role.Owner - } - .take(1) - .onEach { navigateUp() } - .collect() - } - } - @Composable override fun View(modifier: Modifier) { val state = presenter.present() diff --git a/features/rolesandpermissions/test/src/main/kotlin/io/element/android/features/changeroommemberroles/test/FakeRolesAndPermissionsEntryPoint.kt b/features/rolesandpermissions/test/src/main/kotlin/io/element/android/features/changeroommemberroles/test/FakeRolesAndPermissionsEntryPoint.kt index 01f2787778..62cf3285f3 100644 --- a/features/rolesandpermissions/test/src/main/kotlin/io/element/android/features/changeroommemberroles/test/FakeRolesAndPermissionsEntryPoint.kt +++ b/features/rolesandpermissions/test/src/main/kotlin/io/element/android/features/changeroommemberroles/test/FakeRolesAndPermissionsEntryPoint.kt @@ -14,7 +14,7 @@ import io.element.android.features.rolesandpermissions.api.RolesAndPermissionsEn import io.element.android.tests.testutils.lambda.lambdaError class FakeRolesAndPermissionsEntryPoint : RolesAndPermissionsEntryPoint { - override fun createNode(parentNode: Node, buildContext: BuildContext): Node { + override fun createNode(parentNode: Node, buildContext: BuildContext, callback: RolesAndPermissionsEntryPoint.Callback): Node { lambdaError() } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt index 9ea060ef6c..03adbded1b 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt @@ -349,7 +349,16 @@ class RoomDetailsFlowNode( } is NavTarget.AdminSettings -> { - rolesAndPermissionsEntryPoint.createNode(this, buildContext) + val callback = object : RolesAndPermissionsEntryPoint.Callback { + override fun onDone() { + backstack.pop() + } + } + rolesAndPermissionsEntryPoint.createNode( + parentNode = this, + buildContext = buildContext, + callback = callback, + ) } NavTarget.PinnedMessagesList -> { val params = MessagesEntryPoint.Params( 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 13c5b454a2..3d62e7b5d7 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 @@ -11,6 +11,9 @@ package io.element.android.features.securityandprivacy.impl import android.os.Parcelable import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.lifecycle.Lifecycle +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 @@ -19,6 +22,7 @@ import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyEntryPoint +import io.element.android.features.securityandprivacy.api.securityAndPrivacyPermissions import io.element.android.features.securityandprivacy.impl.editroomaddress.EditRoomAddressNode import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyNode import io.element.android.libraries.architecture.BackstackView @@ -26,6 +30,12 @@ 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.room.JoinedRoom +import io.element.android.libraries.matrix.api.room.powerlevels.use +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize @ContributesNode(RoomScope::class) @@ -33,6 +43,7 @@ import kotlinx.parcelize.Parcelize class SecurityAndPrivacyFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, + private val room: JoinedRoom, ) : BaseFlowNode( backstack = BackStack( initialElement = NavTarget.SecurityAndPrivacy, @@ -52,6 +63,24 @@ class SecurityAndPrivacyFlowNode( private val callback: SecurityAndPrivacyEntryPoint.Callback = callback() private val navigator = BackstackSecurityAndPrivacyNavigator(callback, backstack) + override fun onBuilt() { + super.onBuilt() + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + room.roomInfoFlow + .map { roomInfo -> + room.roomPermissions().use(false) { perms -> + perms.securityAndPrivacyPermissions().hasAny(roomInfo.isSpace, roomInfo.joinRule) + } + } + .filter { canEdit -> !canEdit } + .first() + // If the user can no longer edit security and privacy, exit the flow + callback.onDone() + } + } + } + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { NavTarget.SecurityAndPrivacy -> { diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsFlowNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsFlowNode.kt index f7ccba6d93..b5f0495bcb 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsFlowNode.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsFlowNode.kt @@ -113,9 +113,15 @@ class SpaceSettingsFlowNode( ) } is NavTarget.RolesAndPermissions -> { + val callback = object : RolesAndPermissionsEntryPoint.Callback { + override fun onDone() { + backstack.pop() + } + } rolesAndPermissionsEntryPoint.createNode( parentNode = this, buildContext = buildContext, + callback = callback, ) } NavTarget.EditDetails -> {