Merge pull request #5908 from element-hq/feature/fga/space_settings_iteration

Change : space settings iteration
This commit is contained in:
ganfra 2025-12-16 22:41:03 +01:00 committed by GitHub
commit f29b0e399a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
78 changed files with 428 additions and 205 deletions

View file

@ -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
}

View file

@ -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<RolesAndPermissionsFlowNode>(buildContext)
override fun createNode(
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.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<Plugin>,
private val room: JoinedRoom,
) : BaseFlowNode<RolesAndPermissionsFlowNode.NavTarget>(
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) {

View file

@ -34,8 +34,8 @@ internal fun AnalyticsService.trackPermissionChangeAnalytics(initial: RoomPowerL
if (updated.kick != initial?.kick) {
capture(RoomModeration(RoomModeration.Action.ChangePermissionsKickMembers, analyticsMemberRoleForPowerLevel(updated.kick)))
}
if (updated.sendEvents != initial?.sendEvents) {
capture(RoomModeration(RoomModeration.Action.ChangePermissionsSendMessages, analyticsMemberRoleForPowerLevel(updated.sendEvents)))
if (updated.eventsDefault != initial?.eventsDefault) {
capture(RoomModeration(RoomModeration.Action.ChangePermissionsSendMessages, analyticsMemberRoleForPowerLevel(updated.eventsDefault)))
}
if (updated.redactEvents != initial?.redactEvents) {
capture(RoomModeration(RoomModeration.Action.ChangePermissionsRedactMessages, analyticsMemberRoleForPowerLevel(updated.redactEvents)))

View file

@ -36,8 +36,7 @@ class ChangeRoomPermissionsPresenter(
) : Presenter<ChangeRoomPermissionsState> {
companion object {
private fun itemsForSection(section: RoomPermissionsSection) = when (section) {
RoomPermissionsSection.SpaceDetails,
RoomPermissionsSection.RoomDetails -> persistentListOf(
RoomPermissionsSection.EditDetails -> persistentListOf(
RoomPermissionType.ROOM_NAME,
RoomPermissionType.ROOM_AVATAR,
RoomPermissionType.ROOM_TOPIC,
@ -46,19 +45,23 @@ class ChangeRoomPermissionsPresenter(
RoomPermissionType.SEND_EVENTS,
RoomPermissionType.REDACT_EVENTS,
)
RoomPermissionsSection.MembershipModeration -> persistentListOf(
RoomPermissionsSection.ManageMembers -> persistentListOf(
RoomPermissionType.INVITE,
RoomPermissionType.KICK,
RoomPermissionType.BAN,
)
RoomPermissionsSection.ManageSpace -> persistentListOf(
RoomPermissionType.SPACE_MANAGE_ROOMS,
RoomPermissionType.CHANGE_SETTINGS,
)
}
private fun RoomPermissionsSection.shouldShow(isSpace: Boolean): Boolean {
return when (this) {
RoomPermissionsSection.RoomDetails -> !isSpace
RoomPermissionsSection.MembershipModeration -> true
RoomPermissionsSection.EditDetails -> true
RoomPermissionsSection.ManageMembers -> true
RoomPermissionsSection.MessagesAndContent -> !isSpace
RoomPermissionsSection.SpaceDetails -> isSpace
RoomPermissionsSection.ManageSpace -> isSpace
}
}
@ -99,11 +102,13 @@ class ChangeRoomPermissionsPresenter(
RoomPermissionType.BAN -> currentPermissions?.copy(ban = powerLevel)
RoomPermissionType.INVITE -> currentPermissions?.copy(invite = powerLevel)
RoomPermissionType.KICK -> currentPermissions?.copy(kick = powerLevel)
RoomPermissionType.SEND_EVENTS -> currentPermissions?.copy(sendEvents = powerLevel)
RoomPermissionType.SEND_EVENTS -> currentPermissions?.copy(eventsDefault = powerLevel)
RoomPermissionType.REDACT_EVENTS -> currentPermissions?.copy(redactEvents = powerLevel)
RoomPermissionType.ROOM_NAME -> currentPermissions?.copy(roomName = powerLevel)
RoomPermissionType.ROOM_AVATAR -> currentPermissions?.copy(roomAvatar = powerLevel)
RoomPermissionType.ROOM_TOPIC -> currentPermissions?.copy(roomTopic = powerLevel)
RoomPermissionType.SPACE_MANAGE_ROOMS -> currentPermissions?.copy(spaceChild = powerLevel)
RoomPermissionType.CHANGE_SETTINGS -> currentPermissions?.copy(stateDefault = powerLevel)
}
}
is ChangeRoomPermissionsEvent.Save -> coroutineScope.save()

View file

@ -32,11 +32,13 @@ data class ChangeRoomPermissionsState(
RoomPermissionType.BAN -> RoomMember.Role.forPowerLevel(currentPermissions.ban)
RoomPermissionType.INVITE -> RoomMember.Role.forPowerLevel(currentPermissions.invite)
RoomPermissionType.KICK -> RoomMember.Role.forPowerLevel(currentPermissions.kick)
RoomPermissionType.SEND_EVENTS -> RoomMember.Role.forPowerLevel(currentPermissions.sendEvents)
RoomPermissionType.SEND_EVENTS -> RoomMember.Role.forPowerLevel(currentPermissions.eventsDefault)
RoomPermissionType.REDACT_EVENTS -> RoomMember.Role.forPowerLevel(currentPermissions.redactEvents)
RoomPermissionType.ROOM_NAME -> RoomMember.Role.forPowerLevel(currentPermissions.roomName)
RoomPermissionType.ROOM_AVATAR -> RoomMember.Role.forPowerLevel(currentPermissions.roomAvatar)
RoomPermissionType.ROOM_TOPIC -> RoomMember.Role.forPowerLevel(currentPermissions.roomTopic)
RoomPermissionType.SPACE_MANAGE_ROOMS -> RoomMember.Role.forPowerLevel(currentPermissions.spaceChild)
RoomPermissionType.CHANGE_SETTINGS -> RoomMember.Role.forPowerLevel(currentPermissions.stateDefault)
}
return when (role) {
is RoomMember.Role.Owner,
@ -48,10 +50,10 @@ data class ChangeRoomPermissionsState(
}
enum class RoomPermissionsSection {
SpaceDetails,
RoomDetails,
ManageMembers,
EditDetails,
MessagesAndContent,
MembershipModeration,
ManageSpace
}
enum class SelectableRole : DropdownOption {
@ -80,5 +82,7 @@ enum class RoomPermissionType {
REDACT_EVENTS,
ROOM_NAME,
ROOM_AVATAR,
ROOM_TOPIC
ROOM_TOPIC,
SPACE_MANAGE_ROOMS,
CHANGE_SETTINGS,
}

View file

@ -26,6 +26,7 @@ class ChangeRoomPermissionsStateProvider : PreviewParameterProvider<ChangeRoomPe
saveAction = AsyncAction.Failure(IllegalStateException("Failed to save changes"))
),
aChangeRoomPermissionsState(hasChanges = true, saveAction = AsyncAction.ConfirmingCancellation),
aChangeRoomPermissionsState(itemsBySection = ChangeRoomPermissionsPresenter.buildItems(isSpace = true)),
)
}
@ -51,12 +52,13 @@ private fun previewPermissions(): RoomPowerLevelsValues {
ban = RoomMember.Role.User.powerLevel,
// MessagesAndContent section
redactEvents = RoomMember.Role.Moderator.powerLevel,
sendEvents = RoomMember.Role.Admin.powerLevel,
eventsDefault = RoomMember.Role.Admin.powerLevel,
// RoomDetails section
roomName = RoomMember.Role.Admin.powerLevel,
roomAvatar = RoomMember.Role.Moderator.powerLevel,
roomTopic = RoomMember.Role.User.powerLevel,
// SpaceManagement section
spaceChild = RoomMember.Role.Moderator.powerLevel,
stateDefault = RoomMember.Role.Moderator.powerLevel,
)
}

View file

@ -110,10 +110,10 @@ fun ChangeRoomPermissionsView(
@Composable
private fun titleForSection(section: RoomPermissionsSection): String = when (section) {
RoomPermissionsSection.SpaceDetails -> stringResource(R.string.screen_room_roles_and_permissions_space_details)
RoomPermissionsSection.RoomDetails -> stringResource(R.string.screen_room_roles_and_permissions_room_details)
RoomPermissionsSection.MessagesAndContent -> stringResource(R.string.screen_room_roles_and_permissions_messages_and_content)
RoomPermissionsSection.MembershipModeration -> stringResource(R.string.screen_room_roles_and_permissions_member_moderation)
RoomPermissionsSection.EditDetails -> stringResource(R.string.screen_room_change_permissions_room_details)
RoomPermissionsSection.MessagesAndContent -> stringResource(R.string.screen_room_change_permissions_messages_and_content)
RoomPermissionsSection.ManageMembers -> stringResource(R.string.screen_room_change_permissions_member_moderation)
RoomPermissionsSection.ManageSpace -> stringResource(R.string.screen_room_change_permissions_manage_space)
}
@Composable
@ -126,6 +126,8 @@ private fun titleForType(type: RoomPermissionType): String = when (type) {
RoomPermissionType.ROOM_NAME -> stringResource(R.string.screen_room_change_permissions_room_name)
RoomPermissionType.ROOM_AVATAR -> stringResource(R.string.screen_room_change_permissions_room_avatar)
RoomPermissionType.ROOM_TOPIC -> stringResource(R.string.screen_room_change_permissions_room_topic)
RoomPermissionType.SPACE_MANAGE_ROOMS -> stringResource(R.string.screen_room_change_permissions_manage_space_rooms)
RoomPermissionType.CHANGE_SETTINGS -> stringResource(R.string.screen_room_change_permissions_change_settings)
}
@PreviewsDayNight

View file

@ -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
@ -20,14 +19,6 @@ import dev.zacsweers.metro.AssistedInject
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
@ -35,7 +26,6 @@ class RolesAndPermissionsNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: RolesAndPermissionsPresenter,
private val room: BaseRoom,
) : Node(buildContext, plugins = plugins), RolesAndPermissionsNavigator {
interface Callback : Plugin, RolesAndPermissionsNavigator {
override fun openAdminList()
@ -54,22 +44,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()

View file

@ -2,9 +2,12 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_room_change_permissions_administrators">"Správce"</string>
<string name="screen_room_change_permissions_ban_people">"Vykázat lidi"</string>
<string name="screen_room_change_permissions_change_settings">"Změnit nastavení"</string>
<string name="screen_room_change_permissions_delete_messages">"Odstranit zprávy"</string>
<string name="screen_room_change_permissions_everyone">"Člen"</string>
<string name="screen_room_change_permissions_invite_people">"Pozvat přátele"</string>
<string name="screen_room_change_permissions_manage_space">"Správa prostoru"</string>
<string name="screen_room_change_permissions_manage_space_rooms">"Spravovat místnosti"</string>
<string name="screen_room_change_permissions_member_moderation">"Spravovat členy"</string>
<string name="screen_room_change_permissions_messages_and_content">"Zprávy a obsah"</string>
<string name="screen_room_change_permissions_moderators">"Moderátor"</string>
@ -14,6 +17,7 @@
<string name="screen_room_change_permissions_room_name">"Změnit název místnosti"</string>
<string name="screen_room_change_permissions_room_topic">"Změnit téma místnosti"</string>
<string name="screen_room_change_permissions_send_messages">"Odeslat zprávy"</string>
<string name="screen_room_change_permissions_title">"Oprávnění"</string>
<string name="screen_room_change_role_administrators_title">"Upravit správce"</string>
<string name="screen_room_change_role_confirm_add_admin_description">"Tuto akci nebudete moci vrátit zpět. Upravujete oprávnění uživatele, tak aby měl stejnou úroveň jako vy."</string>
<string name="screen_room_change_role_confirm_add_admin_title">"Přidat správce?"</string>

View file

@ -2,9 +2,12 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_room_change_permissions_administrators">"Peakasutajad"</string>
<string name="screen_room_change_permissions_ban_people">"Suhtluskeelu seadmine"</string>
<string name="screen_room_change_permissions_change_settings">"Muuda seadistusi"</string>
<string name="screen_room_change_permissions_delete_messages">"Eemalda sõnumid"</string>
<string name="screen_room_change_permissions_everyone">"Liikmed"</string>
<string name="screen_room_change_permissions_invite_people">"Osalejate kutsumine"</string>
<string name="screen_room_change_permissions_manage_space">"Halda kogukonda"</string>
<string name="screen_room_change_permissions_manage_space_rooms">"Halda jututuba"</string>
<string name="screen_room_change_permissions_member_moderation">"Liikmete haldus"</string>
<string name="screen_room_change_permissions_messages_and_content">"Sõnumid ja sisu"</string>
<string name="screen_room_change_permissions_moderators">"Moderaatorid"</string>
@ -14,6 +17,7 @@
<string name="screen_room_change_permissions_room_name">"Jututoa nime muutmine"</string>
<string name="screen_room_change_permissions_room_topic">"Jututoa teema muutmine"</string>
<string name="screen_room_change_permissions_send_messages">"Sõnumite saatmine"</string>
<string name="screen_room_change_permissions_title">"Õigused"</string>
<string name="screen_room_change_role_administrators_title">"Muuda peakasutajaid"</string>
<string name="screen_room_change_role_confirm_add_admin_description">"Kuna sa annad teisele kasutajale sinu õigustega võrreldes samad õigused, siis sa ei saa seda muudatust hiljem tagasi pöörata."</string>
<string name="screen_room_change_role_confirm_add_admin_title">"Lisame peakasutaja?"</string>

View file

@ -2,9 +2,12 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_room_change_permissions_administrators">"Ylläpitäjä"</string>
<string name="screen_room_change_permissions_ban_people">"Porttikieltojen antaminen"</string>
<string name="screen_room_change_permissions_change_settings">"Asetusten muuttaminen"</string>
<string name="screen_room_change_permissions_delete_messages">"Viestien poistaminen"</string>
<string name="screen_room_change_permissions_everyone">"Jäsen"</string>
<string name="screen_room_change_permissions_invite_people">"Ihmisten kutsuminen ja liittymispyyntöjen hyväksyminen"</string>
<string name="screen_room_change_permissions_manage_space">"Tilan hallitseminen"</string>
<string name="screen_room_change_permissions_manage_space_rooms">"Huoneiden hallitseminen"</string>
<string name="screen_room_change_permissions_member_moderation">"Jäsenien hallinta"</string>
<string name="screen_room_change_permissions_messages_and_content">"Viestit ja sisältö"</string>
<string name="screen_room_change_permissions_moderators">"Valvoja"</string>
@ -14,6 +17,7 @@
<string name="screen_room_change_permissions_room_name">"Huoneen nimen vaihtaminen"</string>
<string name="screen_room_change_permissions_room_topic">"Huoneen aiheen vaihtaminen"</string>
<string name="screen_room_change_permissions_send_messages">"Viestien lähettäminen"</string>
<string name="screen_room_change_permissions_title">"Oikeudet"</string>
<string name="screen_room_change_role_administrators_title">"Muokkaa ylläpitäjiä"</string>
<string name="screen_room_change_role_confirm_add_admin_description">"Et voi peruuttaa tätä toimenpidettä. Ylennät käyttäjän samalle oikeustasolle kuin sinä."</string>
<string name="screen_room_change_role_confirm_add_admin_title">"Lisätäänkö ylläpitäjä?"</string>

View file

@ -2,9 +2,12 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_room_change_permissions_administrators">"Administrateurs"</string>
<string name="screen_room_change_permissions_ban_people">"Bannir des participants"</string>
<string name="screen_room_change_permissions_change_settings">"Changer les paramètres"</string>
<string name="screen_room_change_permissions_delete_messages">"Supprimer des messages"</string>
<string name="screen_room_change_permissions_everyone">"Membre"</string>
<string name="screen_room_change_permissions_invite_people">"Inviter des personnes"</string>
<string name="screen_room_change_permissions_manage_space">"Gérer lespace"</string>
<string name="screen_room_change_permissions_manage_space_rooms">"Gérer les salons"</string>
<string name="screen_room_change_permissions_member_moderation">"Gérer les membres"</string>
<string name="screen_room_change_permissions_messages_and_content">"Messages et contenus"</string>
<string name="screen_room_change_permissions_moderators">"Modérateurs"</string>
@ -14,6 +17,7 @@
<string name="screen_room_change_permissions_room_name">"Changer le nom du salon"</string>
<string name="screen_room_change_permissions_room_topic">"Changer le sujet du salon"</string>
<string name="screen_room_change_permissions_send_messages">"Envoyer des messages"</string>
<string name="screen_room_change_permissions_title">"Autorisations"</string>
<string name="screen_room_change_role_administrators_title">"Modifier les administrateurs"</string>
<string name="screen_room_change_role_confirm_add_admin_description">"Vous ne pourrez pas annuler cette action. Vous êtes en train de promouvoir lutilisateur pour quil ait le même niveau que vous."</string>
<string name="screen_room_change_role_confirm_add_admin_title">"Ajouter un administrateur ?"</string>

View file

@ -2,9 +2,12 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_room_change_permissions_administrators">"Adminisztrátor"</string>
<string name="screen_room_change_permissions_ban_people">"Emberek kitiltása"</string>
<string name="screen_room_change_permissions_change_settings">"Beállítások módosítása"</string>
<string name="screen_room_change_permissions_delete_messages">"Üzenetek eltávolítása"</string>
<string name="screen_room_change_permissions_everyone">"Tag"</string>
<string name="screen_room_change_permissions_invite_people">"Emberek meghívása"</string>
<string name="screen_room_change_permissions_manage_space">"Tér kezelése"</string>
<string name="screen_room_change_permissions_manage_space_rooms">"Szobák kezelése"</string>
<string name="screen_room_change_permissions_member_moderation">"Tagok kezelése"</string>
<string name="screen_room_change_permissions_messages_and_content">"Üzenetek és tartalom"</string>
<string name="screen_room_change_permissions_moderators">"Moderátor"</string>
@ -14,6 +17,7 @@
<string name="screen_room_change_permissions_room_name">"Szoba nevének módosítása"</string>
<string name="screen_room_change_permissions_room_topic">"Szoba témájának módosítása"</string>
<string name="screen_room_change_permissions_send_messages">"Üzenetek küldése"</string>
<string name="screen_room_change_permissions_title">"Jogosultságok"</string>
<string name="screen_room_change_role_administrators_title">"Adminisztrátorok szerkesztése"</string>
<string name="screen_room_change_role_confirm_add_admin_description">"Ezt a műveletet nem fogja tudja visszavonni. Ugyanarra a szintre lépteti elő a felhasználót, mint amellyel Ön is rendelkezik."</string>
<string name="screen_room_change_role_confirm_add_admin_title">"Adminisztrátor hozzáadása?"</string>

View file

@ -2,9 +2,12 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_room_change_permissions_administrators">"Amministratore"</string>
<string name="screen_room_change_permissions_ban_people">"Escludi membri"</string>
<string name="screen_room_change_permissions_change_settings">"Modifica impostazioni"</string>
<string name="screen_room_change_permissions_delete_messages">"Rimuovi messaggi"</string>
<string name="screen_room_change_permissions_everyone">"Membro"</string>
<string name="screen_room_change_permissions_invite_people">"Invita persone"</string>
<string name="screen_room_change_permissions_manage_space">"Gestire lo spazio"</string>
<string name="screen_room_change_permissions_manage_space_rooms">"Gestisci le stanze"</string>
<string name="screen_room_change_permissions_member_moderation">"Gestisci membri"</string>
<string name="screen_room_change_permissions_messages_and_content">"Messaggi e contenuti"</string>
<string name="screen_room_change_permissions_moderators">"Moderatore"</string>
@ -14,6 +17,7 @@
<string name="screen_room_change_permissions_room_name">"Cambia il nome della stanza"</string>
<string name="screen_room_change_permissions_room_topic">"Cambiare l\'argomento della stanza"</string>
<string name="screen_room_change_permissions_send_messages">"Inviare messaggi"</string>
<string name="screen_room_change_permissions_title">"Autorizzazioni"</string>
<string name="screen_room_change_role_administrators_title">"Modifica amministratori"</string>
<string name="screen_room_change_role_confirm_add_admin_description">"Non potrai annullare questa azione. Stai promuovendo l\'utente al tuo stesso livello di potere."</string>
<string name="screen_room_change_role_confirm_add_admin_title">"Aggiungi amministratore?"</string>

View file

@ -2,9 +2,12 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_room_change_permissions_administrators">"Administradores"</string>
<string name="screen_room_change_permissions_ban_people">"Banir pessoas"</string>
<string name="screen_room_change_permissions_change_settings">"Alterar configurações"</string>
<string name="screen_room_change_permissions_delete_messages">"Remover mensagens"</string>
<string name="screen_room_change_permissions_everyone">"Membro"</string>
<string name="screen_room_change_permissions_invite_people">"Convidar pessoas"</string>
<string name="screen_room_change_permissions_manage_space">"Gerenciar espaço"</string>
<string name="screen_room_change_permissions_manage_space_rooms">"Gerenciar salas"</string>
<string name="screen_room_change_permissions_member_moderation">"Gerenciar membros"</string>
<string name="screen_room_change_permissions_messages_and_content">"Mensagens e conteúdo"</string>
<string name="screen_room_change_permissions_moderators">"Moderador"</string>
@ -14,6 +17,7 @@
<string name="screen_room_change_permissions_room_name">"Alterar nome da sala"</string>
<string name="screen_room_change_permissions_room_topic">"Alterar tópico da sala"</string>
<string name="screen_room_change_permissions_send_messages">"Enviar mensagens"</string>
<string name="screen_room_change_permissions_title">"Permissões"</string>
<string name="screen_room_change_role_administrators_title">"Editar administradores"</string>
<string name="screen_room_change_role_confirm_add_admin_description">"Você não poderá desfazer essa ação. Você está promovendo o usuário a ter o mesmo nível de poder que você."</string>
<string name="screen_room_change_role_confirm_add_admin_title">"Adicionar administrador?"</string>

View file

@ -2,9 +2,12 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_room_change_permissions_administrators">"Только администраторы"</string>
<string name="screen_room_change_permissions_ban_people">"Блокировать людей могут"</string>
<string name="screen_room_change_permissions_change_settings">"Изменить настройки"</string>
<string name="screen_room_change_permissions_delete_messages">"Удалить сообщения"</string>
<string name="screen_room_change_permissions_everyone">"Участник"</string>
<string name="screen_room_change_permissions_invite_people">"Пригласить людей"</string>
<string name="screen_room_change_permissions_manage_space">"Управление пространством"</string>
<string name="screen_room_change_permissions_manage_space_rooms">"Управление комнатами"</string>
<string name="screen_room_change_permissions_member_moderation">"Список участников"</string>
<string name="screen_room_change_permissions_messages_and_content">"Сообщения и содержание"</string>
<string name="screen_room_change_permissions_moderators">"Модератор"</string>
@ -14,6 +17,7 @@
<string name="screen_room_change_permissions_room_name">"Менять название комнаты могут"</string>
<string name="screen_room_change_permissions_room_topic">"Менять тему комнаты могут"</string>
<string name="screen_room_change_permissions_send_messages">"Отправлять сообщения могут"</string>
<string name="screen_room_change_permissions_title">"Разрешения"</string>
<string name="screen_room_change_role_administrators_title">"Редактировать роль администраторов"</string>
<string name="screen_room_change_role_confirm_add_admin_description">"Вы не сможете отменить это действие. Вы устанавливаете уровень пользователю соответствующий вашему."</string>
<string name="screen_room_change_role_confirm_add_admin_title">"Добавить администратора?"</string>

View file

@ -2,9 +2,12 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_room_change_permissions_administrators">"管理員"</string>
<string name="screen_room_change_permissions_ban_people">"管理黑名單"</string>
<string name="screen_room_change_permissions_change_settings">"變更設定"</string>
<string name="screen_room_change_permissions_delete_messages">"移除訊息"</string>
<string name="screen_room_change_permissions_everyone">"成員"</string>
<string name="screen_room_change_permissions_invite_people">"邀請夥伴"</string>
<string name="screen_room_change_permissions_manage_space">"管理空間"</string>
<string name="screen_room_change_permissions_manage_space_rooms">"管理聊天室"</string>
<string name="screen_room_change_permissions_member_moderation">"管理成員"</string>
<string name="screen_room_change_permissions_messages_and_content">"訊息與內容"</string>
<string name="screen_room_change_permissions_moderators">"版主"</string>
@ -14,6 +17,7 @@
<string name="screen_room_change_permissions_room_name">"變更聊天室名稱"</string>
<string name="screen_room_change_permissions_room_topic">"變更聊天室主題"</string>
<string name="screen_room_change_permissions_send_messages">"傳送訊息"</string>
<string name="screen_room_change_permissions_title">"權限"</string>
<string name="screen_room_change_role_administrators_title">"編輯管理員"</string>
<string name="screen_room_change_role_confirm_add_admin_description">"您將無法復原此動作。您正將使用者提昇至與您相同的權力等級。"</string>
<string name="screen_room_change_role_confirm_add_admin_title">"要新增管理員嗎?"</string>

View file

@ -2,9 +2,12 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_room_change_permissions_administrators">"Admin"</string>
<string name="screen_room_change_permissions_ban_people">"Ban people"</string>
<string name="screen_room_change_permissions_change_settings">"Change settings"</string>
<string name="screen_room_change_permissions_delete_messages">"Remove messages"</string>
<string name="screen_room_change_permissions_everyone">"Member"</string>
<string name="screen_room_change_permissions_invite_people">"Invite people"</string>
<string name="screen_room_change_permissions_manage_space">"Manage space"</string>
<string name="screen_room_change_permissions_manage_space_rooms">"Manage rooms"</string>
<string name="screen_room_change_permissions_member_moderation">"Manage members"</string>
<string name="screen_room_change_permissions_messages_and_content">"Messages and content"</string>
<string name="screen_room_change_permissions_moderators">"Moderator"</string>
@ -14,6 +17,7 @@
<string name="screen_room_change_permissions_room_name">"Change name"</string>
<string name="screen_room_change_permissions_room_topic">"Change topic"</string>
<string name="screen_room_change_permissions_send_messages">"Send messages"</string>
<string name="screen_room_change_permissions_title">"Permissions"</string>
<string name="screen_room_change_role_administrators_title">"Edit Admins"</string>
<string name="screen_room_change_role_confirm_add_admin_description">"You will not be able to undo this action. You are promoting the user to have the same power level as you."</string>
<string name="screen_room_change_role_confirm_add_admin_title">"Add Admin?"</string>

View file

@ -53,7 +53,7 @@ class ChangeRoomPermissionsPresenterTest {
presenter.present()
}.test {
val itemsBySection = awaitUpdatedItem().itemsBySection
assertThat(itemsBySection[RoomPermissionsSection.RoomDetails]).containsExactly(
assertThat(itemsBySection[RoomPermissionsSection.EditDetails]).containsExactly(
RoomPermissionType.ROOM_NAME,
RoomPermissionType.ROOM_AVATAR,
RoomPermissionType.ROOM_TOPIC,
@ -62,7 +62,7 @@ class ChangeRoomPermissionsPresenterTest {
RoomPermissionType.SEND_EVENTS,
RoomPermissionType.REDACT_EVENTS,
)
assertThat(itemsBySection[RoomPermissionsSection.MembershipModeration]).containsExactly(
assertThat(itemsBySection[RoomPermissionsSection.ManageMembers]).containsExactly(
RoomPermissionType.INVITE,
RoomPermissionType.KICK,
RoomPermissionType.BAN,
@ -77,13 +77,13 @@ class ChangeRoomPermissionsPresenterTest {
presenter.present()
}.test {
val state = awaitUpdatedItem()
assertThat(state.currentPermissions?.roomName).isEqualTo(Admin.powerLevel)
assertThat(state.currentPermissions?.roomName).isEqualTo(Moderator.powerLevel)
assertThat(state.hasChanges).isFalse()
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, SelectableRole.Moderator))
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, SelectableRole.Admin))
awaitItem().run {
assertThat(currentPermissions?.roomName).isEqualTo(Moderator.powerLevel)
assertThat(currentPermissions?.roomName).isEqualTo(Admin.powerLevel)
assertThat(hasChanges).isTrue()
}
}
@ -115,8 +115,9 @@ class ChangeRoomPermissionsPresenterTest {
invite = Moderator.powerLevel,
kick = Moderator.powerLevel,
ban = Moderator.powerLevel,
stateDefault = Moderator.powerLevel,
redactEvents = Moderator.powerLevel,
sendEvents = Moderator.powerLevel,
eventsDefault = Moderator.powerLevel,
roomName = Moderator.powerLevel,
roomAvatar = Moderator.powerLevel,
roomTopic = Moderator.powerLevel,
@ -141,14 +142,14 @@ class ChangeRoomPermissionsPresenterTest {
presenter.present()
}.test {
val state = awaitUpdatedItem()
assertThat(state.currentPermissions?.roomName).isEqualTo(Admin.powerLevel)
assertThat(state.currentPermissions?.roomName).isEqualTo(Moderator.powerLevel)
assertThat(state.hasChanges).isFalse()
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, SelectableRole.Moderator))
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_AVATAR, SelectableRole.Moderator))
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_TOPIC, SelectableRole.Moderator))
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.SEND_EVENTS, SelectableRole.Moderator))
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.REDACT_EVENTS, SelectableRole.Everyone))
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, SelectableRole.Admin))
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_AVATAR, SelectableRole.Admin))
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_TOPIC, SelectableRole.Admin))
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.SEND_EVENTS, SelectableRole.Admin))
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.REDACT_EVENTS, SelectableRole.Admin))
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.KICK, SelectableRole.Admin))
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.BAN, SelectableRole.Admin))
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.INVITE, SelectableRole.Admin))
@ -160,16 +161,16 @@ class ChangeRoomPermissionsPresenterTest {
assertThat(awaitItem().saveAction).isEqualTo(AsyncAction.Loading)
assertThat(awaitItem().hasChanges).isFalse()
awaitItem().run {
assertThat(currentPermissions?.roomName).isEqualTo(Moderator.powerLevel)
assertThat(currentPermissions?.roomName).isEqualTo(Admin.powerLevel)
assertThat(saveAction).isEqualTo(AsyncAction.Success(true))
}
assertThat(analyticsService.capturedEvents).containsExactlyElementsIn(
listOf(
RoomModeration(RoomModeration.Action.ChangePermissionsRoomName, RoomModeration.Role.Moderator),
RoomModeration(RoomModeration.Action.ChangePermissionsRoomAvatar, RoomModeration.Role.Moderator),
RoomModeration(RoomModeration.Action.ChangePermissionsRoomTopic, RoomModeration.Role.Moderator),
RoomModeration(RoomModeration.Action.ChangePermissionsSendMessages, RoomModeration.Role.Moderator),
RoomModeration(RoomModeration.Action.ChangePermissionsRedactMessages, RoomModeration.Role.User),
RoomModeration(RoomModeration.Action.ChangePermissionsRoomName, RoomModeration.Role.Administrator),
RoomModeration(RoomModeration.Action.ChangePermissionsRoomAvatar, RoomModeration.Role.Administrator),
RoomModeration(RoomModeration.Action.ChangePermissionsRoomTopic, RoomModeration.Role.Administrator),
RoomModeration(RoomModeration.Action.ChangePermissionsSendMessages, RoomModeration.Role.Administrator),
RoomModeration(RoomModeration.Action.ChangePermissionsRedactMessages, RoomModeration.Role.Administrator),
RoomModeration(RoomModeration.Action.ChangePermissionsKickMembers, RoomModeration.Role.Administrator),
RoomModeration(RoomModeration.Action.ChangePermissionsBanMembers, RoomModeration.Role.Administrator),
RoomModeration(RoomModeration.Action.ChangePermissionsInviteUsers, RoomModeration.Role.Administrator),
@ -206,17 +207,17 @@ class ChangeRoomPermissionsPresenterTest {
presenter.present()
}.test {
val state = awaitUpdatedItem()
assertThat(state.currentPermissions?.roomName).isEqualTo(Admin.powerLevel)
assertThat(state.currentPermissions?.roomName).isEqualTo(Moderator.powerLevel)
assertThat(state.hasChanges).isFalse()
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, SelectableRole.Moderator))
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, SelectableRole.Admin))
assertThat(awaitItem().hasChanges).isTrue()
state.eventSink(ChangeRoomPermissionsEvent.Save)
assertThat(awaitItem().saveAction).isEqualTo(AsyncAction.Loading)
awaitItem().run {
assertThat(currentPermissions?.roomName).isEqualTo(Moderator.powerLevel)
assertThat(currentPermissions?.roomName).isEqualTo(Admin.powerLevel)
// Couldn't save the changes, so they're still pending
assertThat(hasChanges).isTrue()
assertThat(saveAction).isInstanceOf(AsyncAction.Failure::class.java)
@ -224,7 +225,7 @@ class ChangeRoomPermissionsPresenterTest {
state.eventSink(ChangeRoomPermissionsEvent.ResetPendingActions)
awaitItem().run {
assertThat(currentPermissions?.roomName).isEqualTo(Moderator.powerLevel)
assertThat(currentPermissions?.roomName).isEqualTo(Admin.powerLevel)
assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized)
assertThat(hasChanges).isTrue()
}
@ -238,7 +239,7 @@ class ChangeRoomPermissionsPresenterTest {
presenter.present()
}.test {
val state = awaitUpdatedItem()
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, SelectableRole.Moderator))
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, SelectableRole.Admin))
assertThat(awaitItem().hasChanges).isTrue()
state.eventSink(ChangeRoomPermissionsEvent.Exit)

View file

@ -104,7 +104,7 @@ class ChangeRoomPermissionsViewTest {
state = aChangeRoomPermissionsState(
itemsBySection = persistentMapOf(
// Makes sure there is only one item to click on
RoomPermissionsSection.RoomDetails to persistentListOf(RoomPermissionType.ROOM_NAME)
RoomPermissionsSection.EditDetails to persistentListOf(RoomPermissionType.ROOM_NAME)
),
eventSink = recorder,
)

View file

@ -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()
}
}

View file

@ -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(

View file

@ -19,6 +19,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Inject
import im.vector.app.features.analytics.plan.Interaction
import io.element.android.features.knockrequests.api.KnockRequestPermissions
import io.element.android.features.knockrequests.api.knockRequestPermissions
import io.element.android.features.leaveroom.api.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomState
@ -26,6 +27,7 @@ import io.element.android.features.roomcall.api.RoomCallState
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
import io.element.android.features.roomdetailsedit.api.RoomDetailsEditPermissions
import io.element.android.features.roomdetailsedit.api.roomDetailsEditPermissions
import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyPermissions
import io.element.android.features.securityandprivacy.api.securityAndPrivacyPermissions
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
import io.element.android.libraries.architecture.Presenter
@ -119,7 +121,10 @@ class RoomDetailsPresenter(
room.knockRequestsFlow.collect { value = it.size }
}
val canShowKnockRequests by remember {
derivedStateOf { isKnockRequestsEnabled && permissions.canManageKnockRequests && joinRule == JoinRule.Knock }
derivedStateOf { isKnockRequestsEnabled && permissions.knockRequestsPermissions.hasAny && joinRule == JoinRule.Knock }
}
val canShowSecurityAndPrivacy by remember {
derivedStateOf { !isDm && permissions.securityAndPrivacyPermissions.hasAny(isSpace = false, joinRule = joinRule) }
}
val isDeveloperModeEnabled by remember {
appPreferencesStore.isDeveloperModeEnabledFlow()
@ -186,7 +191,7 @@ class RoomDetailsPresenter(
snackbarMessage = snackbarMessage,
canShowKnockRequests = canShowKnockRequests,
knockRequestsCount = knockRequestsCount,
canShowSecurityAndPrivacy = !isDm && permissions.canEditSecurityAndPrivacy,
canShowSecurityAndPrivacy = canShowSecurityAndPrivacy,
hasMemberVerificationViolations = hasMemberVerificationViolations,
canReportRoom = canReportRoom,
isTombstoned = roomInfo.successorRoom != null,
@ -221,9 +226,9 @@ class RoomDetailsPresenter(
private data class Permissions(
val canInvite: Boolean = false,
val editDetailsPermissions: RoomDetailsEditPermissions = RoomDetailsEditPermissions.DEFAULT,
val canManageKnockRequests: Boolean = false,
val knockRequestsPermissions: KnockRequestPermissions = KnockRequestPermissions.DEFAULT,
val securityAndPrivacyPermissions: SecurityAndPrivacyPermissions = SecurityAndPrivacyPermissions.DEFAULT,
val canEditRolesAndPermissions: Boolean = false,
val canEditSecurityAndPrivacy: Boolean = false,
)
@Composable
@ -232,9 +237,9 @@ class RoomDetailsPresenter(
Permissions(
canInvite = perms.canOwnUserInvite(),
editDetailsPermissions = perms.roomDetailsEditPermissions(),
canManageKnockRequests = perms.knockRequestPermissions().hasAny,
knockRequestsPermissions = perms.knockRequestPermissions(),
canEditRolesAndPermissions = perms.canEditRolesAndPermissions(),
canEditSecurityAndPrivacy = perms.securityAndPrivacyPermissions().hasAny,
securityAndPrivacyPermissions = perms.securityAndPrivacyPermissions(),
)
}
}

View file

@ -110,6 +110,7 @@ fun RoomDetailsEditView(
} else {
AvatarType.Room()
},
enabled = state.canChangeAvatar,
onAvatarClick = ::onAvatarClick,
modifier = Modifier.fillMaxWidth(),
)

View file

@ -9,6 +9,7 @@
package io.element.android.features.securityandprivacy.api
import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions
data class SecurityAndPrivacyPermissions(
@ -17,10 +18,19 @@ data class SecurityAndPrivacyPermissions(
val canChangeEncryption: Boolean,
val canChangeRoomVisibility: Boolean,
) {
val hasAny = canChangeRoomAccess ||
canChangeHistoryVisibility ||
canChangeEncryption ||
canChangeRoomVisibility
fun hasAny(isSpace: Boolean, joinRule: JoinRule?): Boolean {
val canChangeRoomVisibility = when (joinRule) {
is JoinRule.Public,
is JoinRule.Knock,
is JoinRule.KnockRestricted -> canChangeRoomVisibility
else -> false
}
return if (isSpace) {
canChangeRoomAccess || canChangeRoomVisibility
} else {
canChangeRoomAccess || canChangeRoomVisibility || canChangeHistoryVisibility || canChangeEncryption
}
}
companion object {
val DEFAULT = SecurityAndPrivacyPermissions(

View file

@ -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<Plugin>,
private val room: JoinedRoom,
) : BaseFlowNode<SecurityAndPrivacyFlowNode.NavTarget>(
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 -> {

View file

@ -182,9 +182,20 @@ class SecurityAndPrivacyPresenter(
eventSink = ::handleEvent,
)
// If the history visibility is not available for the current access, use the fallback.
LaunchedEffect(state.availableHistoryVisibilities) {
if (editedSettings.historyVisibility !in state.availableHistoryVisibilities) {
// Revert changes that the user is not allowed to make anymore
LaunchedEffect(permissions, state.editedSettings.roomAccess) {
if (!state.showRoomAccessSection) {
editedRoomAccess = savedSettings.roomAccess
}
if (!state.showEncryptionSection) {
editedIsEncrypted = savedSettings.isEncrypted
}
if (!state.showRoomVisibilitySections) {
editedVisibleInRoomDirectory = savedSettings.isVisibleInRoomDirectory
}
if (!state.showHistoryVisibilitySection) {
editedHistoryVisibility = savedSettings.historyVisibility
} else if (editedSettings.historyVisibility !in state.availableHistoryVisibilities) {
editedHistoryVisibility = editedSettings.historyVisibility.fallback()
}
}

View file

@ -244,7 +244,7 @@ class SecurityAndPrivacyPresenterTest {
navigator = navigator,
)
presenter.test {
skipItems(2)
skipItems(1)
with(awaitItem()) {
assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly)
eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone))
@ -312,7 +312,7 @@ class SecurityAndPrivacyPresenterTest {
)
val presenter = createSecurityAndPrivacyPresenter(room = room)
presenter.test {
skipItems(2)
skipItems(1)
with(awaitItem()) {
assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly)
eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone))

View file

@ -42,6 +42,7 @@ dependencies {
implementation(projects.libraries.previewutils)
implementation(projects.features.securityandprivacy.api)
implementation(projects.features.rolesandpermissions.api)
implementation(projects.features.roomdetailsedit.api)
api(projects.features.space.api)
testCommonDependencies(libs, true)

View file

@ -65,7 +65,7 @@ class SpaceFlowNode(
data object Root : NavTarget
@Parcelize
data object Settings : NavTarget
data class Settings(val initialTarget: SpaceSettingsFlowNode.NavTarget = SpaceSettingsFlowNode.NavTarget.Root) : NavTarget
@Parcelize
data object Leave : NavTarget
@ -89,7 +89,7 @@ class SpaceFlowNode(
}
override fun navigateToRolesAndPermissions() {
// TODO
backstack.push(NavTarget.Settings(SpaceSettingsFlowNode.NavTarget.RolesAndPermissions))
}
}
createNode<LeaveSpaceNode>(buildContext, listOf(callback))
@ -101,7 +101,7 @@ class SpaceFlowNode(
}
override fun navigateToSpaceSettings() {
backstack.push(NavTarget.Settings)
backstack.push(NavTarget.Settings())
}
override fun navigateToRoomMemberList() {
@ -114,8 +114,10 @@ class SpaceFlowNode(
}
createNode<SpaceNode>(buildContext, listOf(callback))
}
NavTarget.Settings -> {
is NavTarget.Settings -> {
val callback = object : SpaceSettingsFlowNode.Callback {
override fun initialTarget() = navTarget.initialTarget
override fun navigateToSpaceMembers() {
callback.navigateToRoomMemberList()
}

View file

@ -132,8 +132,7 @@ fun LeaveSpaceView(
state.eventSink(LeaveSpaceEvents.LeaveSpace)
},
onCancel = onCancel,
// TODO enable when navigation is ready
showRolesAndPermissionsButton = false, // state.isLastAdmin,
showRolesAndPermissionsButton = state.isLastAdmin,
onRolesAndPermissionsClick = onRolesAndPermissionsClick,
)
}

View file

@ -11,17 +11,20 @@ package io.element.android.features.space.impl.root
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject
import im.vector.app.features.analytics.plan.JoinedRoom
import im.vector.app.features.analytics.plan.JoinedRoom.Trigger
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.api.toInviteData
import io.element.android.features.space.impl.settings.SpaceSettingsPermissions
import io.element.android.features.space.impl.settings.spaceSettingsPermissions
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.mapState
@ -31,8 +34,10 @@ import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.join.JoinRoom
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar
@ -50,6 +55,7 @@ import kotlin.jvm.optionals.getOrNull
@Inject
class SpacePresenter(
private val spaceRoomList: SpaceRoomList,
private val room: BaseRoom,
private val client: MatrixClient,
private val seenInvitesStore: SeenInvitesStore,
private val joinRoom: JoinRoom,
@ -82,10 +88,17 @@ class SpacePresenter(
}
}.collectAsState()
val permissions by room.permissionsAsState(SpaceSettingsPermissions.DEFAULT) { perms ->
perms.spaceSettingsPermissions()
}
val isSpaceSettingsEnabled by remember {
featureFlagService.isFeatureEnabledFlow(FeatureFlags.SpaceSettings)
}.collectAsState(false)
val roomInfo by room.roomInfoFlow.collectAsState()
val canAccessSpaceSettings by remember {
derivedStateOf { isSpaceSettingsEnabled && permissions.hasAny(roomInfo.joinRule) }
}
val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState()
val (joinActions, setJoinActions) = remember { mutableStateOf(emptyMap<RoomId, AsyncAction<Unit>>()) }
@ -136,7 +149,7 @@ class SpacePresenter(
joinActions = joinActions.toImmutableMap(),
acceptDeclineInviteState = acceptDeclineInviteState,
topicViewerState = topicViewerState,
canAccessSpaceSettings = isSpaceSettingsEnabled,
canAccessSpaceSettings = canAccessSpaceSettings,
eventSink = ::handleEvent,
)
}
@ -150,7 +163,7 @@ class SpacePresenter(
joinRoom.invoke(
roomIdOrAlias = spaceRoom.roomId.toRoomIdOrAlias(),
serverNames = spaceRoom.via,
trigger = JoinedRoom.Trigger.SpaceHierarchy,
trigger = Trigger.SpaceHierarchy,
).onFailure {
setJoinActions(joinActions + mapOf(spaceRoom.roomId to AsyncAction.Failure(it)))
}

View file

@ -20,6 +20,7 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.rolesandpermissions.api.RolesAndPermissionsEntryPoint
import io.element.android.features.roomdetailsedit.api.RoomDetailsEditEntryPoint
import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyEntryPoint
import io.element.android.features.space.impl.di.SpaceFlowScope
import io.element.android.libraries.architecture.BackstackView
@ -35,15 +36,17 @@ class SpaceSettingsFlowNode(
@Assisted plugins: List<Plugin>,
private val securityAndPrivacyEntryPoint: SecurityAndPrivacyEntryPoint,
private val rolesAndPermissionsEntryPoint: RolesAndPermissionsEntryPoint,
private val roomDetailsEditEntryPoint: RoomDetailsEditEntryPoint
) : BaseFlowNode<SpaceSettingsFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Root,
initialElement = initialElement(plugins),
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
plugins = plugins,
) {
interface Callback : Plugin {
fun initialTarget(): NavTarget = NavTarget.Root
fun navigateToSpaceMembers()
fun startLeaveSpaceFlow()
fun closeSettings()
@ -53,6 +56,9 @@ class SpaceSettingsFlowNode(
@Parcelize
data object Root : NavTarget
@Parcelize
data object EditDetails : NavTarget
@Parcelize
data object SecurityAndPrivacy : NavTarget
@ -71,7 +77,7 @@ class SpaceSettingsFlowNode(
}
override fun navigateToEditDetails() {
// TODO
backstack.push(NavTarget.EditDetails)
}
override fun navigateToSpaceMembers() {
@ -108,9 +114,21 @@ 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 -> {
roomDetailsEditEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
)
}
}
@ -121,3 +139,7 @@ class SpaceSettingsFlowNode(
BackstackView(modifier)
}
}
fun initialElement(plugins: List<Plugin>): SpaceSettingsFlowNode.NavTarget {
return plugins.callback<SpaceSettingsFlowNode.Callback>().initialTarget()
}

View file

@ -0,0 +1,44 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.space.impl.settings
import io.element.android.features.roomdetailsedit.api.RoomDetailsEditPermissions
import io.element.android.features.roomdetailsedit.api.roomDetailsEditPermissions
import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyPermissions
import io.element.android.features.securityandprivacy.api.securityAndPrivacyPermissions
import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions
import io.element.android.libraries.matrix.api.room.powerlevels.canEditRolesAndPermissions
data class SpaceSettingsPermissions(
val editDetailsPermissions: RoomDetailsEditPermissions,
val canEditRolesAndPermissions: Boolean,
val securityAndPrivacyPermissions: SecurityAndPrivacyPermissions,
) {
fun hasAny(joinRule: JoinRule?): Boolean {
return editDetailsPermissions.hasAny ||
canEditRolesAndPermissions ||
securityAndPrivacyPermissions.hasAny(isSpace = true, joinRule = joinRule)
}
companion object {
val DEFAULT = SpaceSettingsPermissions(
editDetailsPermissions = RoomDetailsEditPermissions.DEFAULT,
canEditRolesAndPermissions = false,
securityAndPrivacyPermissions = SecurityAndPrivacyPermissions.DEFAULT,
)
}
}
fun RoomPermissions.spaceSettingsPermissions(): SpaceSettingsPermissions {
return SpaceSettingsPermissions(
editDetailsPermissions = roomDetailsEditPermissions(),
canEditRolesAndPermissions = canEditRolesAndPermissions(),
securityAndPrivacyPermissions = securityAndPrivacyPermissions(),
)
}

View file

@ -10,11 +10,13 @@ package io.element.android.features.space.impl.settings
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import dev.zacsweers.metro.Inject
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.ui.room.isOwnUserAdmin
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
@Inject
class SpaceSettingsPresenter(
@ -23,15 +25,22 @@ class SpaceSettingsPresenter(
@Composable
override fun present(): SpaceSettingsState {
val roomInfo by room.roomInfoFlow.collectAsState()
val isUserAdmin = room.isOwnUserAdmin()
val permissions by room.permissionsAsState(SpaceSettingsPermissions.DEFAULT) { perms ->
perms.spaceSettingsPermissions()
}
val showSecurityAndPrivacy by remember {
derivedStateOf { permissions.securityAndPrivacyPermissions.hasAny(isSpace = false, joinRule = roomInfo.joinRule) }
}
return SpaceSettingsState(
roomId = room.roomId,
name = roomInfo.name.orEmpty(),
canonicalAlias = roomInfo.canonicalAlias,
avatarUrl = roomInfo.avatarUrl,
memberCount = roomInfo.activeMembersCount,
showRolesAndPermissions = isUserAdmin,
showSecurityAndPrivacy = isUserAdmin,
canEditDetails = permissions.editDetailsPermissions.hasAny,
showRolesAndPermissions = permissions.canEditRolesAndPermissions,
showSecurityAndPrivacy = showSecurityAndPrivacy,
eventSink = {},
)
}

View file

@ -17,6 +17,7 @@ data class SpaceSettingsState(
val canonicalAlias: RoomAlias?,
val avatarUrl: String?,
val memberCount: Long,
val canEditDetails: Boolean,
val showRolesAndPermissions: Boolean,
val showSecurityAndPrivacy: Boolean,
val eventSink: (SpaceSettingsEvents) -> Unit

View file

@ -30,6 +30,7 @@ fun aSpaceSettingsState(
memberCount: Long = 100,
showRolesAndPermissions: Boolean = false,
showSecurityAndPrivacy: Boolean = false,
canEditDetails: Boolean = false,
eventSink: (SpaceSettingsEvents) -> Unit = {},
) = SpaceSettingsState(
roomId = roomId,
@ -37,6 +38,7 @@ fun aSpaceSettingsState(
canonicalAlias = alias,
avatarUrl = avatarUrl,
memberCount = memberCount,
canEditDetails = canEditDetails,
showRolesAndPermissions = showRolesAndPermissions,
showSecurityAndPrivacy = showSecurityAndPrivacy,
eventSink = eventSink,

View file

@ -73,6 +73,7 @@ fun SpaceSettingsView(
name = state.name,
avatarUrl = state.avatarUrl,
canonicalAlias = state.canonicalAlias?.value,
canEditDetails = state.canEditDetails,
onSpaceInfoClick = onSpaceInfoClick,
)
Section(isVisible = state.showSecurityAndPrivacy, content = {
@ -101,12 +102,13 @@ private fun SpaceInfoSection(
name: String,
avatarUrl: String?,
canonicalAlias: String?,
canEditDetails: Boolean,
onSpaceInfoClick: () -> Unit,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onSpaceInfoClick)
.clickable(enabled = canEditDetails, onClick = onSpaceInfoClick)
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {

View file

@ -24,6 +24,7 @@ import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.join.JoinRoom
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
@ -31,7 +32,9 @@ import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions
import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList
import io.element.android.libraries.previewutils.room.aSpaceRoom
import io.element.android.tests.testutils.EventsRecorder
@ -71,8 +74,25 @@ class SpacePresenterTest {
}
@Test
fun `present - canAccessSpaceSettings when space settings ff is enabled`() = runTest {
fun `present - canAccessSpaceSettings false when space settings ff is enabled but no permissions`() = runTest {
val presenter = createSpacePresenter(spaceSettingsEnabled = true)
presenter.test {
val state = awaitItem()
assertThat(state.canAccessSpaceSettings).isFalse()
}
}
@Test
fun `present - canAccessSpaceSettings true when space settings ff is enabled and has permissions`() = runTest {
val room = FakeBaseRoom(
roomPermissions = FakeRoomPermissions(
canSendState = { true }
)
)
val presenter = createSpacePresenter(
room = room,
spaceSettingsEnabled = true,
)
presenter.test {
skipItems(1)
val state = awaitItem()
@ -335,7 +355,10 @@ class SpacePresenterTest {
private fun TestScope.createSpacePresenter(
client: MatrixClient = FakeMatrixClient(),
spaceRoomList: SpaceRoomList = FakeSpaceRoomList(),
room: BaseRoom = FakeBaseRoom(),
spaceRoomList: SpaceRoomList = FakeSpaceRoomList(
paginateResult = { Result.success(Unit) }
),
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
joinRoom: JoinRoom = FakeJoinRoom(
lambda = { _, _, _ -> Result.success(Unit) },
@ -345,6 +368,7 @@ class SpacePresenterTest {
): SpacePresenter {
return SpacePresenter(
client = client,
room = room,
spaceRoomList = spaceRoomList,
seenInvitesStore = seenInvitesStore,
joinRoom = joinRoom,