Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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(),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class KnockRequestsBannerPresenter(

val shouldShowBanner by remember {
derivedStateOf {
permissions.canHandle && knockRequests.isNotEmpty()
permissions.hasAny && knockRequests.isNotEmpty()
}
}

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ import dev.zacsweers.metro.BindingContainer
import dev.zacsweers.metro.ContributesTo
import dev.zacsweers.metro.Provides
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.featureflag.api.FeatureFlagService
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.powerlevels.permissionsFlow

@BindingContainer
@ContributesTo(RoomScope::class)
Expand All @@ -25,7 +28,9 @@ object KnockRequestsModule {
fun knockRequestsService(room: JoinedRoom, featureFlagService: FeatureFlagService): KnockRequestsService {
return KnockRequestsService(
knockRequestsFlow = room.knockRequestsFlow,
permissionsFlow = room.knockRequestPermissionsFlow(),
permissionsFlow = room.permissionsFlow(KnockRequestPermissions.DEFAULT) { perms ->
perms.knockRequestPermissions()
},
isKnockFeatureEnabledFlow = featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock),
coroutineScope = room.roomCoroutineScope
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

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.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.knock.KnockRequest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
package io.element.android.features.knockrequests.impl.list

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.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
package io.element.android.features.knockrequests.impl.list

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.aKnockRequestPresentable
import io.element.android.libraries.architecture.AsyncAction
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
package io.element.android.features.knockrequests.impl.banner

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.libraries.matrix.api.room.knock.KnockRequest
import io.element.android.libraries.matrix.test.A_USER_ID
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
package io.element.android.features.knockrequests.impl.list

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.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,10 @@ import android.os.Build
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
Expand Down Expand Up @@ -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.permalink.PermalinkParser
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.RoomMembersState
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.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.room.powerlevels.permissionsAsState
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.model.getAvatarData
Expand Down Expand Up @@ -170,7 +164,9 @@ class MessagesPresenter(
val roomCallState = roomCallStatePresenter.present()
val roomMemberModerationState = roomMemberModerationPresenter.present()

val userEventPermissions by userEventPermissions(roomInfo)
val userEventPermissions by room.permissionsAsState(UserEventPermissions.DEFAULT) { perms ->
perms.userEventPermissions()
}

val roomAvatar by remember {
derivedStateOf { roomInfo.avatarData() }
Expand Down Expand Up @@ -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 {
return AvatarData(
id = id.value,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

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.
* It's dependent of the user's power level in the room.
Expand All @@ -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()
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.getDirectRoomMember
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.item.event.toEventOrTransactionId
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
Expand Down Expand Up @@ -98,6 +99,7 @@ import timber.log.Timber
import kotlin.time.Duration.Companion.seconds
import io.element.android.libraries.core.mimetype.MimeTypes.Any as AnyMimeTypes

@Suppress("LargeClass")
@AssistedInject
class MessageComposerPresenter(
@Assisted private val navigator: MessagesNavigator,
Expand Down Expand Up @@ -396,7 +398,9 @@ class MessageComposerPresenter(
val currentUserId = room.sessionId

suspend fun canSendRoomMention(): Boolean {
val userCanSendAtRoom = room.canUserTriggerRoomNotification(currentUserId).getOrDefault(false)
val userCanSendAtRoom = room.roomPermissions().use(false) { perms ->
perms.canOwnUserTriggerRoomNotification()
}
return !room.isDm() && userCanSendAtRoom
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ package io.element.android.features.messages.impl.pinned.list

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
Expand All @@ -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.protection.TimelineProtectionState
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.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
Expand All @@ -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.FeatureFlags
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.powerlevels.canRedactOther
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
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.ui.room.isDmAsState
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
Expand Down Expand Up @@ -97,31 +95,33 @@ class PinnedMessagesListPresenter(
@Composable
override fun present(): PinnedMessagesListState {
htmlConverterProvider.Update()
val isDm by room.isDmAsState()

val timelineRoomInfo = remember(isDm) {
TimelineRoomInfo(
isDm = isDm,
name = room.info().name,
// We don't need to compute those values
userHasPermissionToSendMessage = false,
userHasPermissionToSendReaction = false,
// We do not care about the call state here.
roomCallState = aStandByCallState(),
// don't compute this value or the pin icon will be shown
pinnedEventIds = persistentListOf(),
typingNotificationState = TypingNotificationState(
renderTypingNotifications = false,
typingMembers = persistentListOf(),
reserveSpace = false,
),
predecessorRoom = room.predecessorRoom(),
)
val roomInfo by room.roomInfoFlow.collectAsState()
val timelineRoomInfo by remember {
derivedStateOf {
TimelineRoomInfo(
isDm = roomInfo.isDm,
name = roomInfo.name,
// We don't need to compute those values
userHasPermissionToSendMessage = false,
userHasPermissionToSendReaction = false,
// We do not care about the call state here.
roomCallState = aStandByCallState(),
// don't compute this value or the pin icon will be shown
pinnedEventIds = persistentListOf(),
typingNotificationState = TypingNotificationState(
renderTypingNotifications = false,
typingMembers = persistentListOf(),
reserveSpace = false,
),
predecessorRoom = room.predecessorRoom(),
)
}
}
val timelineProtectionState = timelineProtectionPresenter.present()
val linkState = linkPresenter.present()
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
val userEventPermissions by userEventPermissions(syncUpdateFlow.value)
val userEventPermissions by room.permissionsAsState(UserEventPermissions.DEFAULT) { perms ->
perms.userEventPermissions()
}

val displayThreadSummaries by featureFlagService.isFeatureEnabledFlow(FeatureFlags.Threads).collectAsState(false)

Expand Down Expand Up @@ -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
private fun PinnedMessagesListEffect(onItemsChange: (AsyncData<ImmutableList<TimelineItem>>) -> Unit) {
val updatedOnItemsChange by rememberUpdatedState(onItemsChange)
Expand Down
Loading
Loading