misc(power level) : use new api

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

View file

@ -0,0 +1,34 @@
/*
* 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.knockrequests.api
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions
data class KnockRequestPermissions(
val canAccept: Boolean,
val canDecline: Boolean,
val canBan: Boolean,
) {
val hasAny = canAccept || canDecline || canBan
companion object {
val DEFAULT = KnockRequestPermissions(
canAccept = false,
canDecline = false,
canBan = false,
)
}
}
fun RoomPermissions.knockRequestPermissions(): KnockRequestPermissions {
return KnockRequestPermissions(
canAccept = canOwnUserInvite(),
canDecline = canOwnUserKick(),
canBan = canOwnUserBan(),
)
}

View file

@ -49,7 +49,7 @@ class KnockRequestsBannerPresenter(
val shouldShowBanner by remember { val shouldShowBanner by remember {
derivedStateOf { derivedStateOf {
permissions.canHandle && knockRequests.isNotEmpty() permissions.hasAny && knockRequests.isNotEmpty()
} }
} }

View file

@ -1,33 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector 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.knockrequests.impl.data
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.powerlevels.canBan
import io.element.android.libraries.matrix.api.room.powerlevels.canInvite
import io.element.android.libraries.matrix.api.room.powerlevels.canKick
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
data class KnockRequestPermissions(
val canAccept: Boolean,
val canDecline: Boolean,
val canBan: Boolean,
) {
val canHandle = canAccept || canDecline || canBan
}
fun JoinedRoom.knockRequestPermissionsFlow(): Flow<KnockRequestPermissions> {
return syncUpdateFlow.map {
val canAccept = canInvite().getOrDefault(false)
val canDecline = canKick().getOrDefault(false)
val canBan = canBan().getOrDefault(false)
KnockRequestPermissions(canAccept, canDecline, canBan)
}
}

View file

@ -12,10 +12,13 @@ import dev.zacsweers.metro.BindingContainer
import dev.zacsweers.metro.ContributesTo import dev.zacsweers.metro.ContributesTo
import dev.zacsweers.metro.Provides import dev.zacsweers.metro.Provides
import dev.zacsweers.metro.SingleIn import dev.zacsweers.metro.SingleIn
import io.element.android.features.knockrequests.api.KnockRequestPermissions
import io.element.android.features.knockrequests.api.knockRequestPermissions
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsFlow
@BindingContainer @BindingContainer
@ContributesTo(RoomScope::class) @ContributesTo(RoomScope::class)
@ -25,7 +28,9 @@ object KnockRequestsModule {
fun knockRequestsService(room: JoinedRoom, featureFlagService: FeatureFlagService): KnockRequestsService { fun knockRequestsService(room: JoinedRoom, featureFlagService: FeatureFlagService): KnockRequestsService {
return KnockRequestsService( return KnockRequestsService(
knockRequestsFlow = room.knockRequestsFlow, knockRequestsFlow = room.knockRequestsFlow,
permissionsFlow = room.knockRequestPermissionsFlow(), permissionsFlow = room.permissionsFlow(KnockRequestPermissions.DEFAULT) { perms ->
perms.knockRequestPermissions()
},
isKnockFeatureEnabledFlow = featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock), isKnockFeatureEnabledFlow = featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock),
coroutineScope = room.roomCoroutineScope coroutineScope = room.roomCoroutineScope
) )

View file

@ -8,6 +8,7 @@
package io.element.android.features.knockrequests.impl.data package io.element.android.features.knockrequests.impl.data
import io.element.android.features.knockrequests.api.KnockRequestPermissions
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.knock.KnockRequest import io.element.android.libraries.matrix.api.room.knock.KnockRequest

View file

@ -9,7 +9,7 @@
package io.element.android.features.knockrequests.impl.list package io.element.android.features.knockrequests.impl.list
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.features.knockrequests.impl.data.KnockRequestPermissions import io.element.android.features.knockrequests.api.KnockRequestPermissions
import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData

View file

@ -9,7 +9,7 @@
package io.element.android.features.knockrequests.impl.list package io.element.android.features.knockrequests.impl.list
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.knockrequests.impl.data.KnockRequestPermissions import io.element.android.features.knockrequests.api.KnockRequestPermissions
import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable
import io.element.android.features.knockrequests.impl.data.aKnockRequestPresentable import io.element.android.features.knockrequests.impl.data.aKnockRequestPresentable
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction

View file

@ -9,7 +9,7 @@
package io.element.android.features.knockrequests.impl.banner package io.element.android.features.knockrequests.impl.banner
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.knockrequests.impl.data.KnockRequestPermissions import io.element.android.features.knockrequests.api.KnockRequestPermissions
import io.element.android.features.knockrequests.impl.data.KnockRequestsService import io.element.android.features.knockrequests.impl.data.KnockRequestsService
import io.element.android.libraries.matrix.api.room.knock.KnockRequest import io.element.android.libraries.matrix.api.room.knock.KnockRequest
import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID

View file

@ -11,7 +11,7 @@
package io.element.android.features.knockrequests.impl.list package io.element.android.features.knockrequests.impl.list
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.knockrequests.impl.data.KnockRequestPermissions import io.element.android.features.knockrequests.api.KnockRequestPermissions
import io.element.android.features.knockrequests.impl.data.KnockRequestsService import io.element.android.features.knockrequests.impl.data.KnockRequestsService
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData

View file

@ -12,12 +12,10 @@ import android.os.Build
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
@ -76,14 +74,10 @@ import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.RoomInfo import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.RoomMembersState import io.element.android.libraries.matrix.api.room.RoomMembersState
import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.powerlevels.canPinUnpin import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
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.canSendMessage
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
import io.element.android.libraries.matrix.ui.messages.reply.map import io.element.android.libraries.matrix.ui.messages.reply.map
import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getAvatarData
@ -170,7 +164,9 @@ class MessagesPresenter(
val roomCallState = roomCallStatePresenter.present() val roomCallState = roomCallStatePresenter.present()
val roomMemberModerationState = roomMemberModerationPresenter.present() val roomMemberModerationState = roomMemberModerationPresenter.present()
val userEventPermissions by userEventPermissions(roomInfo) val userEventPermissions by room.permissionsAsState(UserEventPermissions.DEFAULT) { perms ->
perms.userEventPermissions()
}
val roomAvatar by remember { val roomAvatar by remember {
derivedStateOf { roomInfo.avatarData() } derivedStateOf { roomInfo.avatarData() }
@ -301,24 +297,6 @@ class MessagesPresenter(
) )
} }
@Composable
private fun userEventPermissions(roomInfo: RoomInfo): State<UserEventPermissions> {
val key = if (roomInfo.privilegedCreatorRole && roomInfo.creators.contains(room.sessionId)) {
Long.MAX_VALUE
} else {
roomInfo.roomPowerLevels?.hashCode() ?: 0L
}
return produceState(UserEventPermissions.DEFAULT, key1 = key) {
value = UserEventPermissions(
canSendMessage = room.canSendMessage(type = MessageEventType.RoomMessage).getOrElse { true },
canSendReaction = room.canSendMessage(type = MessageEventType.Reaction).getOrElse { true },
canRedactOwn = room.canRedactOwn().getOrElse { false },
canRedactOther = room.canRedactOther().getOrElse { false },
canPinUnpin = room.canPinUnpin().getOrElse { false },
)
}
}
private fun RoomInfo.avatarData(): AvatarData { private fun RoomInfo.avatarData(): AvatarData {
return AvatarData( return AvatarData(
id = id.value, id = id.value,

View file

@ -8,6 +8,9 @@
package io.element.android.features.messages.impl package io.element.android.features.messages.impl
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions
/** /**
* Represents the permissions a user has in a room. * Represents the permissions a user has in a room.
* It's dependent of the user's power level in the room. * It's dependent of the user's power level in the room.
@ -29,3 +32,13 @@ data class UserEventPermissions(
) )
} }
} }
fun RoomPermissions.userEventPermissions(): UserEventPermissions {
return UserEventPermissions(
canRedactOwn = canOwnUserRedactOwn(),
canRedactOther = canOwnUserRedactOther(),
canSendMessage = canOwnUserSendMessage(MessageEventType.RoomMessage),
canSendReaction = canOwnUserSendMessage(MessageEventType.Reaction),
canPinUnpin = canOwnUserPinUnpin()
)
}

View file

@ -55,6 +55,7 @@ import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
import io.element.android.libraries.matrix.api.room.draft.ComposerDraftType import io.element.android.libraries.matrix.api.room.draft.ComposerDraftType
import io.element.android.libraries.matrix.api.room.getDirectRoomMember import io.element.android.libraries.matrix.api.room.getDirectRoomMember
import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.powerlevels.use
import io.element.android.libraries.matrix.api.timeline.TimelineException import io.element.android.libraries.matrix.api.timeline.TimelineException
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
@ -396,7 +397,9 @@ class MessageComposerPresenter(
val currentUserId = room.sessionId val currentUserId = room.sessionId
suspend fun canSendRoomMention(): Boolean { suspend fun canSendRoomMention(): Boolean {
val userCanSendAtRoom = room.canUserTriggerRoomNotification(currentUserId).getOrDefault(false) val userCanSendAtRoom = room.roomPermissions().use(false){ perms ->
perms.canOwnUserTriggerRoomNotification()
}
return !room.isDm() && userCanSendAtRoom return !room.isDm() && userCanSendAtRoom
} }

View file

@ -10,11 +10,10 @@ package io.element.android.features.messages.impl.pinned.list
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@ -35,6 +34,7 @@ import io.element.android.features.messages.impl.timeline.factories.TimelineItem
import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
import io.element.android.features.messages.impl.typing.TypingNotificationState import io.element.android.features.messages.impl.typing.TypingNotificationState
import io.element.android.features.messages.impl.userEventPermissions
import io.element.android.features.roomcall.api.aStandByCallState import io.element.android.features.roomcall.api.aStandByCallState
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
@ -44,11 +44,9 @@ import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.powerlevels.canPinUnpin import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.matrix.ui.room.isDmAsState
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
@ -97,31 +95,33 @@ class PinnedMessagesListPresenter(
@Composable @Composable
override fun present(): PinnedMessagesListState { override fun present(): PinnedMessagesListState {
htmlConverterProvider.Update() htmlConverterProvider.Update()
val isDm by room.isDmAsState() val roomInfo by room.roomInfoFlow.collectAsState()
val timelineRoomInfo by remember {
val timelineRoomInfo = remember(isDm) { derivedStateOf {
TimelineRoomInfo( TimelineRoomInfo(
isDm = isDm, isDm = roomInfo.isDm,
name = room.info().name, name = roomInfo.name,
// We don't need to compute those values // We don't need to compute those values
userHasPermissionToSendMessage = false, userHasPermissionToSendMessage = false,
userHasPermissionToSendReaction = false, userHasPermissionToSendReaction = false,
// We do not care about the call state here. // We do not care about the call state here.
roomCallState = aStandByCallState(), roomCallState = aStandByCallState(),
// don't compute this value or the pin icon will be shown // don't compute this value or the pin icon will be shown
pinnedEventIds = persistentListOf(), pinnedEventIds = persistentListOf(),
typingNotificationState = TypingNotificationState( typingNotificationState = TypingNotificationState(
renderTypingNotifications = false, renderTypingNotifications = false,
typingMembers = persistentListOf(), typingMembers = persistentListOf(),
reserveSpace = false, reserveSpace = false,
), ),
predecessorRoom = room.predecessorRoom(), predecessorRoom = room.predecessorRoom(),
) )
}
} }
val timelineProtectionState = timelineProtectionPresenter.present() val timelineProtectionState = timelineProtectionPresenter.present()
val linkState = linkPresenter.present() val linkState = linkPresenter.present()
val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val userEventPermissions by room.permissionsAsState(UserEventPermissions.DEFAULT) { perms ->
val userEventPermissions by userEventPermissions(syncUpdateFlow.value) perms.userEventPermissions()
}
val displayThreadSummaries by featureFlagService.isFeatureEnabledFlow(FeatureFlags.Threads).collectAsState(false) val displayThreadSummaries by featureFlagService.isFeatureEnabledFlow(FeatureFlags.Threads).collectAsState(false)
@ -192,19 +192,6 @@ class PinnedMessagesListPresenter(
} }
} }
@Composable
private fun userEventPermissions(updateKey: Long): State<UserEventPermissions> {
return produceState(UserEventPermissions.DEFAULT, key1 = updateKey) {
value = UserEventPermissions(
canSendMessage = false,
canSendReaction = false,
canRedactOwn = room.canRedactOwn().getOrElse { false },
canRedactOther = room.canRedactOther().getOrElse { false },
canPinUnpin = room.canPinUnpin().getOrElse { false },
)
}
}
@Composable @Composable
private fun PinnedMessagesListEffect(onItemsChange: (AsyncData<ImmutableList<TimelineItem>>) -> Unit) { private fun PinnedMessagesListEffect(onItemsChange: (AsyncData<ImmutableList<TimelineItem>>) -> Unit) {
val updatedOnItemsChange by rememberUpdatedState(onItemsChange) val updatedOnItemsChange by rememberUpdatedState(onItemsChange)

View file

@ -24,6 +24,7 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.features.messages.impl.MessagesNavigator import io.element.android.features.messages.impl.MessagesNavigator
import io.element.android.features.messages.impl.UserEventPermissions
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureEvents import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureEvents
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureState import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureState
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
@ -32,6 +33,7 @@ import io.element.android.features.messages.impl.timeline.model.NewEventState
import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemTypingNotificationModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemTypingNotificationModel
import io.element.android.features.messages.impl.typing.TypingNotificationState import io.element.android.features.messages.impl.typing.TypingNotificationState
import io.element.android.features.messages.impl.userEventPermissions
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
import io.element.android.features.poll.api.actions.EndPollAction import io.element.android.features.poll.api.actions.EndPollAction
import io.element.android.features.poll.api.actions.SendPollResponseAction import io.element.android.features.poll.api.actions.SendPollResponseAction
@ -46,14 +48,13 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.core.UniqueId
import io.element.android.libraries.matrix.api.core.asEventId import io.element.android.libraries.matrix.api.core.asEventId
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.DisplayFirstTimelineItems import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.DisplayFirstTimelineItems
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.NotificationTapOpensTimeline import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.NotificationTapOpensTimeline
@ -128,11 +129,6 @@ class TimelinePresenter(
val roomInfo by room.roomInfoFlow.collectAsState() val roomInfo by room.roomInfoFlow.collectAsState()
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.RoomMessage, updateKey = syncUpdateFlow.value)
val userHasPermissionToSendReaction by room.canSendMessageAsState(type = MessageEventType.Reaction, updateKey = syncUpdateFlow.value)
val prevMostRecentItemId = rememberSaveable { mutableStateOf<UniqueId?>(null) } val prevMostRecentItemId = rememberSaveable { mutableStateOf<UniqueId?>(null) }
val newEventState = remember { mutableStateOf(NewEventState.None) } val newEventState = remember { mutableStateOf(NewEventState.None) }
@ -285,13 +281,16 @@ class TimelinePresenter(
val typingNotificationState = typingNotificationPresenter.present() val typingNotificationState = typingNotificationPresenter.present()
val roomCallState = roomCallStatePresenter.present() val roomCallState = roomCallStatePresenter.present()
val userEventPermissions by room.permissionsAsState(UserEventPermissions.DEFAULT) { perms ->
perms.userEventPermissions()
}
val timelineRoomInfo by remember(typingNotificationState, roomCallState, roomInfo) { val timelineRoomInfo by remember(typingNotificationState, roomCallState, roomInfo) {
derivedStateOf { derivedStateOf {
TimelineRoomInfo( TimelineRoomInfo(
name = roomInfo.name, name = roomInfo.name,
isDm = roomInfo.isDm.orFalse(), isDm = roomInfo.isDm,
userHasPermissionToSendMessage = userHasPermissionToSendMessage, userHasPermissionToSendMessage = userEventPermissions.canSendMessage,
userHasPermissionToSendReaction = userHasPermissionToSendReaction, userHasPermissionToSendReaction = userEventPermissions.canSendReaction,
roomCallState = roomCallState, roomCallState = roomCallState,
pinnedEventIds = roomInfo.pinnedEventIds, pinnedEventIds = roomInfo.pinnedEventIds,
typingNotificationState = typingNotificationState, typingNotificationState = typingNotificationState,

View file

@ -21,7 +21,8 @@ import io.element.android.features.enterprise.api.SessionEnterpriseService
import io.element.android.features.roomcall.api.RoomCallState import io.element.android.features.roomcall.api.RoomCallState
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.ui.room.canCall import io.element.android.libraries.matrix.api.room.powerlevels.canCall
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
@Inject @Inject
class RoomCallStatePresenter( class RoomCallStatePresenter(
@ -35,8 +36,7 @@ class RoomCallStatePresenter(
value = sessionEnterpriseService.isElementCallAvailable() value = sessionEnterpriseService.isElementCallAvailable()
} }
val roomInfo by room.roomInfoFlow.collectAsState() val roomInfo by room.roomInfoFlow.collectAsState()
val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val canJoinCall by room.permissionsAsState(false) { perms -> perms.canCall() }
val canJoinCall by room.canCall(updateKey = syncUpdateFlow.value)
val isUserInTheCall by remember { val isUserInTheCall by remember {
derivedStateOf { derivedStateOf {
room.sessionId in roomInfo.activeRoomCallParticipants room.sessionId in roomInfo.activeRoomCallParticipants

View file

@ -10,6 +10,7 @@ package io.element.android.features.roomdetails.impl
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -18,11 +19,14 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import im.vector.app.features.analytics.plan.Interaction import im.vector.app.features.analytics.plan.Interaction
import io.element.android.features.knockrequests.api.knockRequestPermissions
import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.leaveroom.api.LeaveRoomState
import io.element.android.features.roomcall.api.RoomCallState import io.element.android.features.roomcall.api.RoomCallState
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
import io.element.android.features.securityandprivacy.api.securityAndPrivacyPermissionsAsState 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.libraries.androidutils.clipboard.ClipboardHelper import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
@ -36,17 +40,13 @@ import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembersState import io.element.android.libraries.matrix.api.room.isDm
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.join.JoinRule
import io.element.android.libraries.matrix.api.room.powerlevels.canInvite import io.element.android.libraries.matrix.api.room.powerlevels.canEditRolesAndPermissions
import io.element.android.libraries.matrix.api.room.powerlevels.canSendState import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
import io.element.android.libraries.matrix.api.room.roomNotificationSettings import io.element.android.libraries.matrix.api.room.roomNotificationSettings
import io.element.android.libraries.matrix.ui.room.canHandleKnockRequestsAsState
import io.element.android.libraries.matrix.ui.room.getCurrentRoomMember import io.element.android.libraries.matrix.ui.room.getCurrentRoomMember
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
import io.element.android.libraries.matrix.ui.room.isDmAsState
import io.element.android.libraries.matrix.ui.room.isOwnUserAdmin
import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange
import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@ -77,8 +77,6 @@ class RoomDetailsPresenter(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val leaveRoomState = leaveRoomPresenter.present() val leaveRoomState = leaveRoomPresenter.present()
val roomInfo by room.roomInfoFlow.collectAsState() val roomInfo by room.roomInfoFlow.collectAsState()
val isUserAdmin = room.isOwnUserAdmin()
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
val roomAvatar by remember { derivedStateOf { roomInfo.avatarUrl } } val roomAvatar by remember { derivedStateOf { roomInfo.avatarUrl } }
val roomName by remember { derivedStateOf { roomInfo.name?.trim().orEmpty() } } val roomName by remember { derivedStateOf { roomInfo.name?.trim().orEmpty() } }
@ -93,15 +91,11 @@ class RoomDetailsPresenter(
observeNotificationSettings() observeNotificationSettings()
} }
val isDm = roomInfo.isDm
val membersState by room.membersStateFlow.collectAsState() val membersState by room.membersStateFlow.collectAsState()
val canInvite by getCanInvite(membersState) val permissions by getPermissions()
val canonicalAlias by remember { derivedStateOf { roomInfo.canonicalAlias } } val canonicalAlias by remember { derivedStateOf { roomInfo.canonicalAlias } }
val isEncrypted by remember { derivedStateOf { roomInfo.isEncrypted == true } } val isEncrypted by remember { derivedStateOf { roomInfo.isEncrypted == true } }
val isDm by room.isDmAsState()
val canEditName by getCanSendState(membersState, StateEventType.ROOM_NAME)
val canEditAvatar by getCanSendState(membersState, StateEventType.ROOM_AVATAR)
val canEditTopic by getCanSendState(membersState, StateEventType.ROOM_TOPIC)
val dmMember by room.getDirectRoomMember(membersState) val dmMember by room.getDirectRoomMember(membersState)
val currentMember by room.getCurrentRoomMember(membersState) val currentMember by room.getCurrentRoomMember(membersState)
val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMember) val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMember)
@ -109,16 +103,15 @@ class RoomDetailsPresenter(
val roomCallState = roomCallStatePresenter.present() val roomCallState = roomCallStatePresenter.present()
val joinedMemberCount by remember { derivedStateOf { roomInfo.joinedMembersCount } } val joinedMemberCount by remember { derivedStateOf { roomInfo.joinedMembersCount } }
val topicState = remember(canEditTopic, roomTopic, roomType) { val topicState = remember(permissions.editDetailsPermissions.canEditTopic, roomTopic, roomType) {
val topic = roomTopic val topic = roomTopic
when { when {
!topic.isNullOrBlank() -> RoomTopicState.ExistingTopic(topic) !topic.isNullOrBlank() -> RoomTopicState.ExistingTopic(topic)
canEditTopic && roomType is RoomDetailsType.Room -> RoomTopicState.CanAddTopic permissions.editDetailsPermissions.canEditTopic && roomType is RoomDetailsType.Room -> RoomTopicState.CanAddTopic
else -> RoomTopicState.Hidden else -> RoomTopicState.Hidden
} }
} }
val canHandleKnockRequests by room.canHandleKnockRequestsAsState(syncUpdateFlow.value)
val isKnockRequestsEnabled by remember { val isKnockRequestsEnabled by remember {
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock) featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock)
}.collectAsState(false) }.collectAsState(false)
@ -126,7 +119,7 @@ class RoomDetailsPresenter(
room.knockRequestsFlow.collect { value = it.size } room.knockRequestsFlow.collect { value = it.size }
} }
val canShowKnockRequests by remember { val canShowKnockRequests by remember {
derivedStateOf { isKnockRequestsEnabled && canHandleKnockRequests && joinRule == JoinRule.Knock } derivedStateOf { isKnockRequestsEnabled && permissions.canManageKnockRequests && joinRule == JoinRule.Knock }
} }
val isDeveloperModeEnabled by remember { val isDeveloperModeEnabled by remember {
appPreferencesStore.isDeveloperModeEnabledFlow() appPreferencesStore.isDeveloperModeEnabledFlow()
@ -162,13 +155,6 @@ class RoomDetailsPresenter(
val roomMemberDetailsState = roomMemberDetailsPresenter?.present() val roomMemberDetailsState = roomMemberDetailsPresenter?.present()
val securityAndPrivacyPermissions = room.securityAndPrivacyPermissionsAsState(syncUpdateFlow.value)
val canShowSecurityAndPrivacy by remember {
derivedStateOf {
roomType is RoomDetailsType.Room && securityAndPrivacyPermissions.value.hasAny
}
}
val hasMemberVerificationViolations by produceState(false) { val hasMemberVerificationViolations by produceState(false) {
room.roomMemberIdentityStateChange(waitForEncryption = true) room.roomMemberIdentityStateChange(waitForEncryption = true)
.onEach { identities -> value = identities.any { it.identityState == IdentityState.VerificationViolation } } .onEach { identities -> value = identities.any { it.identityState == IdentityState.VerificationViolation } }
@ -185,22 +171,22 @@ class RoomDetailsPresenter(
roomTopic = topicState, roomTopic = topicState,
memberCount = joinedMemberCount, memberCount = joinedMemberCount,
isEncrypted = isEncrypted, isEncrypted = isEncrypted,
canInvite = canInvite, canInvite = permissions.canInvite,
canEdit = (canEditAvatar || canEditName || canEditTopic) && roomType == RoomDetailsType.Room, canEdit = roomType == RoomDetailsType.Room && permissions.editDetailsPermissions.hasAny,
roomCallState = roomCallState, roomCallState = roomCallState,
roomType = roomType, roomType = roomType,
roomMemberDetailsState = roomMemberDetailsState, roomMemberDetailsState = roomMemberDetailsState,
leaveRoomState = leaveRoomState, leaveRoomState = leaveRoomState,
roomNotificationSettings = roomNotificationSettingsState.roomNotificationSettings(), roomNotificationSettings = roomNotificationSettingsState.roomNotificationSettings(),
isFavorite = isFavorite, isFavorite = isFavorite,
displayRolesAndPermissionsSettings = !isDm && isUserAdmin, displayRolesAndPermissionsSettings = !isDm && permissions.canEditRolesAndPermissions,
isPublic = joinRule == JoinRule.Public, isPublic = joinRule == JoinRule.Public,
heroes = roomInfo.heroes.toImmutableList(), heroes = roomInfo.heroes.toImmutableList(),
pinnedMessagesCount = pinnedMessagesCount, pinnedMessagesCount = pinnedMessagesCount,
snackbarMessage = snackbarMessage, snackbarMessage = snackbarMessage,
canShowKnockRequests = canShowKnockRequests, canShowKnockRequests = canShowKnockRequests,
knockRequestsCount = knockRequestsCount, knockRequestsCount = knockRequestsCount,
canShowSecurityAndPrivacy = canShowSecurityAndPrivacy, canShowSecurityAndPrivacy = !isDm && permissions.canEditSecurityAndPrivacy,
hasMemberVerificationViolations = hasMemberVerificationViolations, hasMemberVerificationViolations = hasMemberVerificationViolations,
canReportRoom = canReportRoom, canReportRoom = canReportRoom,
isTombstoned = roomInfo.successorRoom != null, isTombstoned = roomInfo.successorRoom != null,
@ -232,14 +218,25 @@ class RoomDetailsPresenter(
} }
} }
@Composable private data class Permissions(
private fun getCanInvite(membersState: RoomMembersState) = produceState(false, membersState) { val canInvite: Boolean = false,
value = room.canInvite().getOrElse { false } val editDetailsPermissions: RoomDetailsEditPermissions = RoomDetailsEditPermissions.DEFAULT,
} val canManageKnockRequests: Boolean = false,
val canEditRolesAndPermissions: Boolean = false,
val canEditSecurityAndPrivacy: Boolean = false,
)
@Composable @Composable
private fun getCanSendState(membersState: RoomMembersState, type: StateEventType) = produceState(false, membersState) { private fun getPermissions(): State<Permissions> {
value = room.canSendState(type).getOrElse { false } return room.permissionsAsState(Permissions()) { perms ->
Permissions(
canInvite = perms.canOwnUserInvite(),
editDetailsPermissions = perms.roomDetailsEditPermissions(),
canManageKnockRequests = perms.knockRequestPermissions().hasAny,
canEditRolesAndPermissions = perms.canEditRolesAndPermissions(),
canEditSecurityAndPrivacy = perms.securityAndPrivacyPermissions().hasAny,
)
}
} }
private fun CoroutineScope.observeNotificationSettings() { private fun CoroutineScope.observeNotificationSettings() {

View file

@ -32,10 +32,10 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembersState import io.element.android.libraries.matrix.api.room.RoomMembersState
import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.matrix.api.room.toMatrixUser import io.element.android.libraries.matrix.api.room.toMatrixUser
import io.element.android.libraries.matrix.ui.room.PowerLevelRoomMemberComparator import io.element.android.libraries.matrix.ui.room.PowerLevelRoomMemberComparator
import io.element.android.libraries.matrix.ui.room.canInviteAsState
import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange
import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.persistentMapOf
@ -58,8 +58,7 @@ class RoomMemberListPresenter(
override fun present(): RoomMemberListState { override fun present(): RoomMemberListState {
var searchQuery by rememberSaveable { mutableStateOf("") } var searchQuery by rememberSaveable { mutableStateOf("") }
val membersState by room.membersStateFlow.collectAsState() val membersState by room.membersStateFlow.collectAsState()
val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val canInvite by room.permissionsAsState(false) { perms -> perms.canOwnUserInvite() }
val canInvite by room.canInviteAsState(syncUpdateFlow.value)
val roomModerationState = roomMembersModerationPresenter.present() val roomModerationState = roomMembersModerationPresenter.present()
val roomMemberIdentityStates by produceState(persistentMapOf()) { val roomMemberIdentityStates by produceState(persistentMapOf()) {

View file

@ -0,0 +1,37 @@
/*
* 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.roomdetailsedit.api
import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions
data class RoomDetailsEditPermissions(
val canEditName: Boolean,
val canEditTopic: Boolean,
val canEditAvatar: Boolean,
){
val hasAny = canEditName ||
canEditTopic ||
canEditAvatar
companion object {
val DEFAULT = RoomDetailsEditPermissions(
canEditName = false,
canEditTopic = false,
canEditAvatar = false,
)
}
}
fun RoomPermissions.roomDetailsEditPermissions(): RoomDetailsEditPermissions {
return RoomDetailsEditPermissions(
canEditName = canOwnUserSendState(StateEventType.ROOM_NAME),
canEditTopic = canOwnUserSendState(StateEventType.ROOM_TOPIC),
canEditAvatar = canOwnUserSendState(StateEventType.ROOM_AVATAR),
)
}

View file

@ -23,6 +23,8 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.core.net.toUri import androidx.core.net.toUri
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import io.element.android.features.roomdetailsedit.api.RoomDetailsEditPermissions
import io.element.android.features.roomdetailsedit.api.roomDetailsEditPermissions
import io.element.android.libraries.androidutils.file.TemporaryUriDeleter import io.element.android.libraries.androidutils.file.TemporaryUriDeleter
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
@ -30,8 +32,7 @@ import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
import io.element.android.libraries.matrix.api.room.powerlevels.canSendState
import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.libraries.mediapickers.api.PickerProvider import io.element.android.libraries.mediapickers.api.PickerProvider
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
@ -93,14 +94,8 @@ class RoomDetailsEditPresenter(
} }
} }
var canChangeName by remember { mutableStateOf(false) } val permissions by room.permissionsAsState(RoomDetailsEditPermissions.DEFAULT){perms ->
var canChangeTopic by remember { mutableStateOf(false) } perms.roomDetailsEditPermissions()
var canChangeAvatar by remember { mutableStateOf(false) }
LaunchedEffect(roomSyncUpdateFlow.value) {
canChangeName = room.canSendState(StateEventType.ROOM_NAME).getOrElse { false }
canChangeTopic = room.canSendState(StateEventType.ROOM_TOPIC).getOrElse { false }
canChangeAvatar = room.canSendState(StateEventType.ROOM_AVATAR).getOrElse { false }
} }
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker( val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(
@ -181,11 +176,11 @@ class RoomDetailsEditPresenter(
return RoomDetailsEditState( return RoomDetailsEditState(
roomId = room.roomId, roomId = room.roomId,
roomRawName = roomRawNameEdited, roomRawName = roomRawNameEdited,
canChangeName = canChangeName, canChangeName = permissions.canEditName,
roomTopic = roomTopicEdited, roomTopic = roomTopicEdited,
canChangeTopic = canChangeTopic, canChangeTopic = permissions.canEditTopic,
roomAvatarUrl = roomAvatarUriEdited, roomAvatarUrl = roomAvatarUriEdited,
canChangeAvatar = canChangeAvatar, canChangeAvatar = permissions.canEditAvatar,
avatarActions = avatarActions, avatarActions = avatarActions,
saveButtonEnabled = saveButtonEnabled, saveButtonEnabled = saveButtonEnabled,
saveAction = saveAction.value, saveAction = saveAction.value,

View file

@ -30,10 +30,9 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.room.canBanAsState
import io.element.android.libraries.matrix.ui.room.canKickAsState
import io.element.android.libraries.matrix.ui.room.userPowerLevelAsState import io.element.android.libraries.matrix.ui.room.userPowerLevelAsState
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -56,8 +55,12 @@ class RoomMemberModerationPresenter(
override fun present(): RoomMemberModerationState { override fun present(): RoomMemberModerationState {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
val canBan = room.canBanAsState(syncUpdateFlow.value) val permissions by room.permissionsAsState(Permissions()) { perms ->
val canKick = room.canKickAsState(syncUpdateFlow.value) Permissions(
canKick = perms.canOwnUserKick(),
canBan = perms.canOwnUserBan(),
)
}
val currentUserMemberPowerLevel = room.userPowerLevelAsState(syncUpdateFlow.value) val currentUserMemberPowerLevel = room.userPowerLevelAsState(syncUpdateFlow.value)
val kickUserAsyncAction = val kickUserAsyncAction =
@ -80,8 +83,7 @@ class RoomMemberModerationPresenter(
} }
moderationActions.value = computeModerationActions( moderationActions.value = computeModerationActions(
member = member, member = member,
canKick = canKick.value, permissions = permissions,
canBan = canBan.value,
currentUserMemberPowerLevel = currentUserMemberPowerLevel.value, currentUserMemberPowerLevel = currentUserMemberPowerLevel.value,
) )
} }
@ -134,8 +136,8 @@ class RoomMemberModerationPresenter(
} }
return InternalRoomMemberModerationState( return InternalRoomMemberModerationState(
canKick = canKick.value, canKick = permissions.canKick,
canBan = canBan.value, canBan = permissions.canBan,
selectedUser = selectedUser, selectedUser = selectedUser,
actions = moderationActions.value, actions = moderationActions.value,
kickUserAsyncAction = kickUserAsyncAction.value, kickUserAsyncAction = kickUserAsyncAction.value,
@ -147,8 +149,7 @@ class RoomMemberModerationPresenter(
private fun computeModerationActions( private fun computeModerationActions(
member: RoomMember?, member: RoomMember?,
canKick: Boolean, permissions: Permissions,
canBan: Boolean,
currentUserMemberPowerLevel: Long, currentUserMemberPowerLevel: Long,
): ImmutableList<ModerationActionState> { ): ImmutableList<ModerationActionState> {
return buildList { return buildList {
@ -158,11 +159,11 @@ class RoomMemberModerationPresenter(
val canModerateThisUser = currentUserMemberPowerLevel > targetMemberPowerLevel val canModerateThisUser = currentUserMemberPowerLevel > targetMemberPowerLevel
// Assume the member is joined when it's unknown // Assume the member is joined when it's unknown
val membership = member?.membership ?: RoomMembershipState.JOIN val membership = member?.membership ?: RoomMembershipState.JOIN
if (canKick) { if (permissions.canKick) {
val isKickEnabled = canModerateThisUser && membership.isActive() val isKickEnabled = canModerateThisUser && membership.isActive()
add(ModerationActionState(action = ModerationAction.KickUser, isEnabled = isKickEnabled)) add(ModerationActionState(action = ModerationAction.KickUser, isEnabled = isKickEnabled))
} }
if (canBan) { if (permissions.canBan) {
if (membership == RoomMembershipState.BAN) { if (membership == RoomMembershipState.BAN) {
add(ModerationActionState(action = ModerationAction.UnbanUser, isEnabled = canModerateThisUser)) add(ModerationActionState(action = ModerationAction.UnbanUser, isEnabled = canModerateThisUser))
} else { } else {
@ -208,6 +209,11 @@ class RoomMemberModerationPresenter(
) )
} }
private data class Permissions(
val canKick: Boolean = false,
val canBan: Boolean = false,
)
private fun <T> CoroutineScope.runActionAndWaitForMembershipChange( private fun <T> CoroutineScope.runActionAndWaitForMembershipChange(
action: MutableState<AsyncAction<T>>, action: MutableState<AsyncAction<T>>,
block: suspend () -> Result<T> block: suspend () -> Result<T>

View file

@ -8,13 +8,8 @@
package io.element.android.features.securityandprivacy.api package io.element.android.features.securityandprivacy.api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.produceState
import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyPermissions.Companion.DEFAULT
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.powerlevels.canSendState import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions
data class SecurityAndPrivacyPermissions( data class SecurityAndPrivacyPermissions(
val canChangeRoomAccess: Boolean, val canChangeRoomAccess: Boolean,
@ -37,14 +32,11 @@ data class SecurityAndPrivacyPermissions(
} }
} }
@Composable fun RoomPermissions.securityAndPrivacyPermissions(): SecurityAndPrivacyPermissions {
fun BaseRoom.securityAndPrivacyPermissionsAsState(updateKey: Long): State<SecurityAndPrivacyPermissions> { return SecurityAndPrivacyPermissions(
return produceState(DEFAULT, key1 = updateKey) { canChangeRoomAccess = canOwnUserSendState(StateEventType.ROOM_JOIN_RULES),
value = SecurityAndPrivacyPermissions( canChangeHistoryVisibility = canOwnUserSendState(StateEventType.ROOM_HISTORY_VISIBILITY),
canChangeRoomAccess = canSendState(type = StateEventType.ROOM_JOIN_RULES).getOrElse { false }, canChangeEncryption = canOwnUserSendState(StateEventType.ROOM_ENCRYPTION),
canChangeHistoryVisibility = canSendState(type = StateEventType.ROOM_HISTORY_VISIBILITY).getOrElse { false }, canChangeRoomVisibility = canOwnUserSendState(StateEventType.ROOM_CANONICAL_ALIAS),
canChangeEncryption = canSendState(type = StateEventType.ROOM_ENCRYPTION).getOrElse { false }, )
canChangeRoomVisibility = canSendState(type = StateEventType.ROOM_CANONICAL_ALIAS).getOrElse { false },
)
}
} }

View file

@ -21,7 +21,8 @@ import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.features.securityandprivacy.api.securityAndPrivacyPermissionsAsState import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyPermissions
import io.element.android.features.securityandprivacy.api.securityAndPrivacyPermissions
import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNavigator import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNavigator
import io.element.android.features.securityandprivacy.impl.editroomaddress.matchesServer import io.element.android.features.securityandprivacy.impl.editroomaddress.matchesServer
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
@ -37,6 +38,7 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.RoomInfo import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
@ -106,7 +108,9 @@ class SecurityAndPrivacyPresenter(
) )
var showEnableEncryptionConfirmation by remember(savedSettings.isEncrypted) { mutableStateOf(false) } var showEnableEncryptionConfirmation by remember(savedSettings.isEncrypted) { mutableStateOf(false) }
val permissions by room.securityAndPrivacyPermissionsAsState(syncUpdateFlow.value) val permissions by room.permissionsAsState(SecurityAndPrivacyPermissions.DEFAULT) { perms ->
perms.securityAndPrivacyPermissions()
}
fun handleEvent(event: SecurityAndPrivacyEvent) { fun handleEvent(event: SecurityAndPrivacyEvent) {
when (event) { when (event) {

View file

@ -34,6 +34,8 @@ import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.matrix.api.MatrixClient 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.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.powerlevels.canCall
import io.element.android.libraries.matrix.api.room.powerlevels.use
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
@ -56,7 +58,7 @@ class UserProfilePresenter(
@Composable @Composable
private fun getDmRoomId(): State<RoomId?> { private fun getDmRoomId(): State<RoomId?> {
return produceState<RoomId?>(initialValue = null) { return produceState(initialValue = null) {
value = client.findDM(userId).getOrNull() value = client.findDM(userId).getOrNull()
} }
} }
@ -66,7 +68,6 @@ class UserProfilePresenter(
val isElementCallAvailable by produceState(initialValue = false, roomId) { val isElementCallAvailable by produceState(initialValue = false, roomId) {
value = sessionEnterpriseService.isElementCallAvailable() value = sessionEnterpriseService.isElementCallAvailable()
} }
return produceState(initialValue = false, isElementCallAvailable, roomId) { return produceState(initialValue = false, isElementCallAvailable, roomId) {
value = when { value = when {
isElementCallAvailable.not() -> false isElementCallAvailable.not() -> false
@ -75,7 +76,7 @@ class UserProfilePresenter(
roomId roomId
?.let { client.getRoom(it) } ?.let { client.getRoom(it) }
?.use { room -> ?.use { room ->
room.canUserJoinCall(client.sessionId).getOrNull() room.roomPermissions().use(false){ perms -> perms.canCall()}
} }
.orFalse() .orFalse()
} }

View file

@ -130,57 +130,6 @@ interface BaseRoom : Closeable {
*/ */
suspend fun forget(): Result<Unit> 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. * 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 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.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.MessageEventType
import io.element.android.libraries.matrix.api.room.StateEventType 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. * Provides information about the permissions of users in a room.
@ -120,23 +129,38 @@ interface RoomPermissions : AutoCloseable {
fun canUserTriggerRoomNotification(userId: UserId): Boolean 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 { fun RoomPermissions.canEditRolesAndPermissions(): Boolean {
return canOwnUserSendState(StateEventType.ROOM_POWER_LEVELS) 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 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( data class RoomPowerLevelsValues(
val ban: Long, val ban: Long,
val invite: Long, val invite: Long,
@ -24,50 +19,3 @@ data class RoomPowerLevelsValues(
val roomTopic: Long, val roomTopic: Long,
val spaceChild: 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.core.EventId
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader 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.BaseRoom
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory
import io.element.android.libraries.mediaviewer.impl.datasource.MediaGalleryDataSource 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.local.LocalMediaActions
import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems 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.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.eventId
import io.element.android.libraries.mediaviewer.impl.model.mediaInfo 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.mediaviewer.impl.model.mediaSource
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -80,6 +81,10 @@ class MediaGalleryPresenter(
mediaGalleryDataSource.start() mediaGalleryDataSource.start()
} }
val permissions by room.permissionsAsState(MediaPermissions.DEFAULT) { perms ->
perms.mediaPermissions()
}
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState() val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
localMediaActions.Configure() localMediaActions.Configure()
@ -119,8 +124,8 @@ class MediaGalleryPresenter(
eventId = event.mediaItem.eventId(), eventId = event.mediaItem.eventId(),
canDelete = when (event.mediaItem.mediaInfo().senderId) { canDelete = when (event.mediaItem.mediaInfo().senderId) {
null -> false null -> false
room.sessionId -> room.canRedactOwn().getOrElse { false } && event.mediaItem.eventId() != null room.sessionId -> permissions.canRedactOwn && event.mediaItem.eventId() != null
else -> room.canRedactOther().getOrElse { false } && event.mediaItem.eventId() != null else -> permissions.canRedactOther && event.mediaItem.eventId() != null
}, },
mediaInfo = event.mediaItem.mediaInfo(), mediaInfo = event.mediaItem.mediaInfo(),
thumbnailSource = when (event.mediaItem) { thumbnailSource = when (event.mediaItem) {
@ -202,6 +207,7 @@ class MediaGalleryPresenter(
CommonStrings.error_unknown CommonStrings.error_unknown
} }
} }
} }
private fun GroupedMediaItems?.find(eventId: EventId?): MediaItem.Event? { 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.designsystem.utils.snackbar.collectSnackbarMessageAsState
import io.element.android.libraries.matrix.api.core.EventId 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.JoinedRoom
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
import io.element.android.libraries.matrix.api.timeline.Timeline 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.matrix.api.timeline.item.event.toEventOrTransactionId
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint 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.R
import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState 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.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 io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -81,6 +82,9 @@ class MediaViewerPresenter(
NoMoreItemsBackwardSnackBarDisplayer(currentIndex, data) NoMoreItemsBackwardSnackBarDisplayer(currentIndex, data)
NoMoreItemsForwardSnackBarDisplayer(currentIndex, data) NoMoreItemsForwardSnackBarDisplayer(currentIndex, data)
val permissions by room.permissionsAsState(MediaPermissions.DEFAULT) { perms ->
perms.mediaPermissions()
}
var mediaBottomSheetState by remember { mutableStateOf<MediaBottomSheetState>(MediaBottomSheetState.Hidden) } var mediaBottomSheetState by remember { mutableStateOf<MediaBottomSheetState>(MediaBottomSheetState.Hidden) }
DisposableEffect(Unit) { DisposableEffect(Unit) {
@ -131,8 +135,8 @@ class MediaViewerPresenter(
eventId = event.data.eventId, eventId = event.data.eventId,
canDelete = when (event.data.mediaInfo.senderId) { canDelete = when (event.data.mediaInfo.senderId) {
null -> false null -> false
room.sessionId -> room.canRedactOwn().getOrElse { false } && event.data.eventId != null room.sessionId -> permissions.canRedactOwn && event.data.eventId != null
else -> room.canRedactOther().getOrElse { false } && event.data.eventId != null else -> permissions.canRedactOther && event.data.eventId != null
}, },
mediaInfo = event.data.mediaInfo, mediaInfo = event.data.mediaInfo,
thumbnailSource = event.data.thumbnailSource, thumbnailSource = event.data.thumbnailSource,