misc(power level) : use new api

This commit is contained in:
ganfra 2025-12-08 22:23:07 +01:00
parent 44535243ef
commit d654280e30
29 changed files with 312 additions and 334 deletions

View file

@ -130,57 +130,6 @@ interface BaseRoom : Closeable {
*/
suspend fun forget(): Result<Unit>
/**
* Returns `true` if the user with the provided [userId] can invite other users to the room.
*/
suspend fun canUserInvite(userId: UserId): Result<Boolean>
/**
* Returns `true` if the user with the provided [userId] can kick other users from the room.
*/
suspend fun canUserKick(userId: UserId): Result<Boolean>
/**
* Returns `true` if the user with the provided [userId] can ban other users from the room.
*/
suspend fun canUserBan(userId: UserId): Result<Boolean>
/**
* Returns `true` if the user with the provided [userId] can redact their own messages.
*/
suspend fun canUserRedactOwn(userId: UserId): Result<Boolean>
/**
* Returns `true` if the user with the provided [userId] can redact messages from other users.
*/
suspend fun canUserRedactOther(userId: UserId): Result<Boolean>
/**
* Returns `true` if the user with the provided [userId] can send state events.
*/
suspend fun canUserSendState(userId: UserId, type: StateEventType): Result<Boolean>
/**
* Returns `true` if the user with the provided [userId] can send messages.
*/
suspend fun canUserSendMessage(userId: UserId, type: MessageEventType): Result<Boolean>
/**
* Returns `true` if the user with the provided [userId] can trigger an `@room` notification.
*/
suspend fun canUserTriggerRoomNotification(userId: UserId): Result<Boolean>
/**
* Returns `true` if the user with the provided [userId] can pin or unpin messages.
*/
suspend fun canUserPinUnpin(userId: UserId): Result<Boolean>
/**
* Returns `true` if the user with the provided [userId] can join or starts calls.
*/
suspend fun canUserJoinCall(userId: UserId): Result<Boolean> =
canUserSendState(userId, StateEventType.CALL_MEMBER)
/**
* Sets the room as favorite or not, based on the [isFavorite] parameter.
*/

View file

@ -7,9 +7,18 @@
package io.element.android.libraries.matrix.api.room.powerlevels
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.StateEventType
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import timber.log.Timber
/**
* Provides information about the permissions of users in a room.
@ -120,23 +129,38 @@ interface RoomPermissions : AutoCloseable {
fun canUserTriggerRoomNotification(userId: UserId): Boolean
}
fun RoomPermissions.canEditRoomDetails(): Boolean {
return canOwnUserSendState(StateEventType.ROOM_NAME) ||
canOwnUserSendState(StateEventType.ROOM_TOPIC) ||
canOwnUserSendState(StateEventType.ROOM_AVATAR)
}
fun RoomPermissions.canManageKnockRequests(): Boolean {
return canOwnUserInvite() || canOwnUserBan() || canOwnUserKick()
}
fun RoomPermissions.canEditSecurityAndPrivacy(): Boolean {
return canOwnUserSendState(StateEventType.ROOM_JOIN_RULES) ||
canOwnUserSendState(StateEventType.ROOM_HISTORY_VISIBILITY) ||
canOwnUserSendState(StateEventType.ROOM_CANONICAL_ALIAS) ||
canOwnUserSendState(StateEventType.ROOM_ENCRYPTION)
}
fun RoomPermissions.canEditRolesAndPermissions(): Boolean {
return canOwnUserSendState(StateEventType.ROOM_POWER_LEVELS)
}
fun RoomPermissions.canCall(): Boolean {
return canOwnUserSendState(StateEventType.CALL_MEMBER)
}
fun <T> Result<RoomPermissions>.use(default: T, block: (RoomPermissions) -> T): T {
return fold(
onSuccess = { perms ->
perms.use(block)
},
onFailure = {
default
}
)
}
fun <T> BaseRoom.permissionsFlow(default: T, block: (RoomPermissions) -> T): Flow<T> {
return roomInfoFlow
.map { info -> info.roomPowerLevels }
.distinctUntilChanged()
.map {
roomPermissions().use(default, block)
}
}
@Composable
fun <T> BaseRoom.permissionsAsState(default: T, block: (RoomPermissions) -> T): State<T> {
return remember(this, default, block) {
Timber.d("Computing permissionsAsState for room $roomId with default=$default")
permissionsFlow(default, block)
}.collectAsState(default)
}

View file

@ -8,11 +8,6 @@
package io.element.android.libraries.matrix.api.room.powerlevels
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.StateEventType
data class RoomPowerLevelsValues(
val ban: Long,
val invite: Long,
@ -24,50 +19,3 @@ data class RoomPowerLevelsValues(
val roomTopic: Long,
val spaceChild: Long,
)
/**
* Shortcut for calling [BaseRoom.canUserInvite] with our own user.
*/
suspend fun BaseRoom.canInvite(): Result<Boolean> = canUserInvite(sessionId)
/**
* Shortcut for calling [BaseRoom.canUserKick] with our own user.
*/
suspend fun BaseRoom.canKick(): Result<Boolean> = canUserKick(sessionId)
/**
* Shortcut for calling [BaseRoom.canUserBan] with our own user.
*/
suspend fun BaseRoom.canBan(): Result<Boolean> = canUserBan(sessionId)
/**
* Shortcut for calling [BaseRoom.canUserSendState] with our own user.
*/
suspend fun BaseRoom.canSendState(type: StateEventType): Result<Boolean> = canUserSendState(sessionId, type)
/**
* Shortcut for calling [BaseRoom.canUserSendMessage] with our own user.
*/
suspend fun BaseRoom.canSendMessage(type: MessageEventType): Result<Boolean> = canUserSendMessage(sessionId, type)
/**
* Shortcut for calling [BaseRoom.canUserRedactOwn] with our own user.
*/
suspend fun BaseRoom.canRedactOwn(): Result<Boolean> = canUserRedactOwn(sessionId)
/**
* Shortcut for calling [BaseRoom.canRedactOther] with our own user.
*/
suspend fun BaseRoom.canRedactOther(): Result<Boolean> = canUserRedactOther(sessionId)
/**
* Shortcut for checking if current user can handle knock requests.
*/
suspend fun BaseRoom.canHandleKnockRequests(): Result<Boolean> = runCatchingExceptions {
canInvite().getOrThrow() || canBan().getOrThrow() || canKick().getOrThrow()
}
/**
* Shortcut for calling [BaseRoom.canUserPinUnpin] with our own user.
*/
suspend fun BaseRoom.canPinUnpin(): Result<Boolean> = canUserPinUnpin(sessionId)

View file

@ -30,8 +30,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarM
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory
import io.element.android.libraries.mediaviewer.impl.datasource.MediaGalleryDataSource
@ -39,8 +38,10 @@ import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetSta
import io.element.android.libraries.mediaviewer.impl.local.LocalMediaActions
import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems
import io.element.android.libraries.mediaviewer.impl.model.MediaItem
import io.element.android.libraries.mediaviewer.impl.model.MediaPermissions
import io.element.android.libraries.mediaviewer.impl.model.eventId
import io.element.android.libraries.mediaviewer.impl.model.mediaInfo
import io.element.android.libraries.mediaviewer.impl.model.mediaPermissions
import io.element.android.libraries.mediaviewer.impl.model.mediaSource
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.launch
@ -80,6 +81,10 @@ class MediaGalleryPresenter(
mediaGalleryDataSource.start()
}
val permissions by room.permissionsAsState(MediaPermissions.DEFAULT) { perms ->
perms.mediaPermissions()
}
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
localMediaActions.Configure()
@ -119,8 +124,8 @@ class MediaGalleryPresenter(
eventId = event.mediaItem.eventId(),
canDelete = when (event.mediaItem.mediaInfo().senderId) {
null -> false
room.sessionId -> room.canRedactOwn().getOrElse { false } && event.mediaItem.eventId() != null
else -> room.canRedactOther().getOrElse { false } && event.mediaItem.eventId() != null
room.sessionId -> permissions.canRedactOwn && event.mediaItem.eventId() != null
else -> permissions.canRedactOther && event.mediaItem.eventId() != null
},
mediaInfo = event.mediaItem.mediaInfo(),
thumbnailSource = when (event.mediaItem) {
@ -202,6 +207,7 @@ class MediaGalleryPresenter(
CommonStrings.error_unknown
}
}
}
private fun GroupedMediaItems?.find(eventId: EventId?): MediaItem.Event? {

View file

@ -0,0 +1,29 @@
/*
* 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.libraries.mediaviewer.impl.model
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions
data class MediaPermissions(
val canRedactOwn: Boolean,
val canRedactOther: Boolean,
) {
companion object {
val DEFAULT = MediaPermissions(
canRedactOwn = false,
canRedactOther = false,
)
}
}
fun RoomPermissions.mediaPermissions(): MediaPermissions {
return MediaPermissions(
canRedactOwn = canOwnUserRedactOwn(),
canRedactOther = canOwnUserRedactOther(),
)
}

View file

@ -32,8 +32,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
@ -41,6 +40,8 @@ import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.impl.R
import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
import io.element.android.libraries.mediaviewer.impl.local.LocalMediaActions
import io.element.android.libraries.mediaviewer.impl.model.MediaPermissions
import io.element.android.libraries.mediaviewer.impl.model.mediaPermissions
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CoroutineScope
@ -81,6 +82,9 @@ class MediaViewerPresenter(
NoMoreItemsBackwardSnackBarDisplayer(currentIndex, data)
NoMoreItemsForwardSnackBarDisplayer(currentIndex, data)
val permissions by room.permissionsAsState(MediaPermissions.DEFAULT) { perms ->
perms.mediaPermissions()
}
var mediaBottomSheetState by remember { mutableStateOf<MediaBottomSheetState>(MediaBottomSheetState.Hidden) }
DisposableEffect(Unit) {
@ -131,8 +135,8 @@ class MediaViewerPresenter(
eventId = event.data.eventId,
canDelete = when (event.data.mediaInfo.senderId) {
null -> false
room.sessionId -> room.canRedactOwn().getOrElse { false } && event.data.eventId != null
else -> room.canRedactOther().getOrElse { false } && event.data.eventId != null
room.sessionId -> permissions.canRedactOwn && event.data.eventId != null
else -> permissions.canRedactOther && event.data.eventId != null
},
mediaInfo = event.data.mediaInfo,
thumbnailSource = event.data.thumbnailSource,