change(room permissions): ensure closing screen without permissions

This commit is contained in:
ganfra 2025-12-11 17:39:10 +01:00
parent 6e2863ded6
commit d06f3a0798
8 changed files with 88 additions and 30 deletions

View file

@ -8,6 +8,19 @@
package io.element.android.features.rolesandpermissions.api 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
}

View file

@ -17,7 +17,11 @@ import io.element.android.libraries.di.RoomScope
@ContributesBinding(RoomScope::class) @ContributesBinding(RoomScope::class)
class DefaultRolesAndPermissionsEntryPoint : RolesAndPermissionsEntryPoint { class DefaultRolesAndPermissionsEntryPoint : RolesAndPermissionsEntryPoint {
override fun createNode(parentNode: Node, buildContext: BuildContext): Node { override fun createNode(
return parentNode.createNode<RolesAndPermissionsFlowNode>(buildContext) parentNode: Node,
buildContext: BuildContext,
callback: RolesAndPermissionsEntryPoint.Callback,
): Node {
return parentNode.createNode<RolesAndPermissionsFlowNode>(buildContext, listOf(callback))
} }
} }

View file

@ -14,7 +14,10 @@ import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope import androidx.lifecycle.coroutineScope
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
@ -25,17 +28,24 @@ 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.rolesandpermissions.api.ChangeRoomMemberRolesListType 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.permissions.ChangeRoomPermissionsNode
import io.element.android.features.rolesandpermissions.impl.roles.ChangeRolesNode import io.element.android.features.rolesandpermissions.impl.roles.ChangeRolesNode
import io.element.android.features.rolesandpermissions.impl.root.RolesAndPermissionsNode import io.element.android.features.rolesandpermissions.impl.root.RolesAndPermissionsNode
import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode 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.architecture.createNode
import io.element.android.libraries.designsystem.components.async.AsyncIndicator 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.AsyncIndicatorHost
import io.element.android.libraries.designsystem.components.async.AsyncIndicatorState import io.element.android.libraries.designsystem.components.async.AsyncIndicatorState
import io.element.android.libraries.di.RoomScope 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 io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ -44,6 +54,7 @@ import kotlinx.parcelize.Parcelize
class RolesAndPermissionsFlowNode( class RolesAndPermissionsFlowNode(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val room: JoinedRoom,
) : BaseFlowNode<RolesAndPermissionsFlowNode.NavTarget>( ) : BaseFlowNode<RolesAndPermissionsFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
initialElement = NavTarget.Root, initialElement = NavTarget.Root,
@ -66,6 +77,7 @@ class RolesAndPermissionsFlowNode(
data object ChangeRoomPermissions : NavTarget data object ChangeRoomPermissions : NavTarget
} }
private val callback: RolesAndPermissionsEntryPoint.Callback = callback()
private val asyncIndicatorState = AsyncIndicatorState() private val asyncIndicatorState = AsyncIndicatorState()
override fun onBuilt() { override fun onBuilt() {
@ -76,6 +88,15 @@ class RolesAndPermissionsFlowNode(
onChangeComplete(changesSaved) 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) { private fun onChangeComplete(changesSaved: Boolean) {

View file

@ -11,7 +11,6 @@ package io.element.android.features.rolesandpermissions.impl.root
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin 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.architecture.callback
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.room.BaseRoom 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) @ContributesNode(RoomScope::class)
@AssistedInject @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 @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state = presenter.present() val state = presenter.present()

View file

@ -14,7 +14,7 @@ import io.element.android.features.rolesandpermissions.api.RolesAndPermissionsEn
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
class FakeRolesAndPermissionsEntryPoint : RolesAndPermissionsEntryPoint { class FakeRolesAndPermissionsEntryPoint : RolesAndPermissionsEntryPoint {
override fun createNode(parentNode: Node, buildContext: BuildContext): Node { override fun createNode(parentNode: Node, buildContext: BuildContext, callback: RolesAndPermissionsEntryPoint.Callback): Node {
lambdaError() lambdaError()
} }
} }

View file

@ -349,7 +349,16 @@ class RoomDetailsFlowNode(
} }
is NavTarget.AdminSettings -> { 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 -> { NavTarget.PinnedMessagesList -> {
val params = MessagesEntryPoint.Params( val params = MessagesEntryPoint.Params(

View file

@ -11,6 +11,9 @@ package io.element.android.features.securityandprivacy.impl
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier 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.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
@ -19,6 +22,7 @@ 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.api.SecurityAndPrivacyEntryPoint 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.editroomaddress.EditRoomAddressNode
import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyNode import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyNode
import io.element.android.libraries.architecture.BackstackView 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.callback
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.RoomScope 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 import kotlinx.parcelize.Parcelize
@ContributesNode(RoomScope::class) @ContributesNode(RoomScope::class)
@ -33,6 +43,7 @@ import kotlinx.parcelize.Parcelize
class SecurityAndPrivacyFlowNode( class SecurityAndPrivacyFlowNode(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val room: JoinedRoom,
) : BaseFlowNode<SecurityAndPrivacyFlowNode.NavTarget>( ) : BaseFlowNode<SecurityAndPrivacyFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
initialElement = NavTarget.SecurityAndPrivacy, initialElement = NavTarget.SecurityAndPrivacy,
@ -52,6 +63,24 @@ class SecurityAndPrivacyFlowNode(
private val callback: SecurityAndPrivacyEntryPoint.Callback = callback() private val callback: SecurityAndPrivacyEntryPoint.Callback = callback()
private val navigator = BackstackSecurityAndPrivacyNavigator(callback, backstack) 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 { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) { return when (navTarget) {
NavTarget.SecurityAndPrivacy -> { NavTarget.SecurityAndPrivacy -> {

View file

@ -113,9 +113,15 @@ class SpaceSettingsFlowNode(
) )
} }
is NavTarget.RolesAndPermissions -> { is NavTarget.RolesAndPermissions -> {
val callback = object : RolesAndPermissionsEntryPoint.Callback {
override fun onDone() {
backstack.pop()
}
}
rolesAndPermissionsEntryPoint.createNode( rolesAndPermissionsEntryPoint.createNode(
parentNode = this, parentNode = this,
buildContext = buildContext, buildContext = buildContext,
callback = callback,
) )
} }
NavTarget.EditDetails -> { NavTarget.EditDetails -> {