From 44535243ef1d155b0ce5a18fd8e0cabbf1a6cff8 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 5 Dec 2025 12:53:58 +0100 Subject: [PATCH 1/5] misc(power level) : introduce RoomPermissions --- .../libraries/matrix/api/room/BaseRoom.kt | 6 + .../api/room/powerlevels/RoomPermissions.kt | 142 ++++++++++++++++++ .../matrix/impl/room/RustBaseRoom.kt | 8 + .../room/powerlevels/RustRoomPermissions.kt | 95 ++++++++++++ .../matrix/test/room/FakeBaseRoom.kt | 7 + .../room/powerlevels/FakeRoomPermissions.kt | 58 +++++++ 6 files changed, 316 insertions(+) create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPermissions.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/powerlevels/RustRoomPermissions.kt create mode 100644 libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/powerlevels/FakeRoomPermissions.kt diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt index 41e6ce0e623..cde3a5eff71 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt @@ -14,6 +14,7 @@ import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.draft.ComposerDraft +import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues import io.element.android.libraries.matrix.api.room.tombstone.PredecessorRoom import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility @@ -99,6 +100,11 @@ interface BaseRoom : Closeable { */ suspend fun userRole(userId: UserId): Result + /** + * Gets the permissions of the room. + */ + suspend fun roomPermissions(): Result + /** * Gets the display name of the user with the provided [userId] in the room. */ diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPermissions.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPermissions.kt new file mode 100644 index 00000000000..cdc735b8199 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPermissions.kt @@ -0,0 +1,142 @@ +/* + * 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.matrix.api.room.powerlevels + +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.MessageEventType +import io.element.android.libraries.matrix.api.room.StateEventType + +/** + * Provides information about the permissions of users in a room. + */ +interface RoomPermissions : AutoCloseable { + fun canOwnUserBan(): Boolean + + /** + * Returns true if the current user is able to invite in the room. + */ + fun canOwnUserInvite(): Boolean + + /** + * Returns true if the current user is able to kick in the room. + */ + fun canOwnUserKick(): Boolean + + /** + * Returns true if the current user is able to pin or unpin events in the + * room. + */ + fun canOwnUserPinUnpin(): Boolean + + /** + * Returns true if the current user user is able to redact messages of + * other users in the room. + */ + fun canOwnUserRedactOther(): Boolean + + /** + * Returns true if the current user is able to redact their own messages in + * the room. + */ + fun canOwnUserRedactOwn(): Boolean + + /** + * Returns true if the current user is able to send a specific message type + * in the room. + */ + fun canOwnUserSendMessage(message: MessageEventType): Boolean + + /** + * Returns true if the current user is able to send a specific state event + * type in the room. + */ + fun canOwnUserSendState(stateEvent: StateEventType): Boolean + + /** + * Returns true if the current user is able to trigger a notification in + * the room. + */ + fun canOwnUserTriggerRoomNotification(): Boolean + + /** + * Returns true if the user with the given userId is able to ban in the + * room. + */ + fun canUserBan(userId: UserId): Boolean + + /** + * Returns true if the user with the given userId is able to invite in the + * room. + */ + fun canUserInvite(userId: UserId): Boolean + + /** + * Returns true if the user with the given userId is able to kick in the + * room. + */ + fun canUserKick(userId: UserId): Boolean + + /** + * Returns true if the user with the given userId is able to pin or unpin + * events in the room. + */ + fun canUserPinUnpin(userId: UserId): Boolean + + /** + * Returns true if the user with the given userId is able to redact + * messages of other users in the room. + */ + fun canUserRedactOther(userId: UserId): Boolean + + /** + * Returns true if the user with the given userId is able to redact + * their own messages in the room. + */ + fun canUserRedactOwn(userId: UserId): Boolean + + /** + * Returns true if the user with the given userId is able to send a + * specific message type in the room. + */ + fun canUserSendMessage(userId: UserId, message: MessageEventType): Boolean + + /** + * Returns true if the user with the given userId is able to send a + * specific state event type in the room. + */ + fun canUserSendState(userId: UserId, stateEvent: StateEventType): Boolean + + /** + * Returns true if the user with the given userId is able to trigger a + * notification in the room. + * + * The call may fail if there is an error in getting the power levels. + */ + 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) +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt index be7f6240eee..eec9591e32a 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt @@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.api.room.RoomMembersState import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.draft.ComposerDraft +import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues import io.element.android.libraries.matrix.api.room.tombstone.PredecessorRoom import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility @@ -33,6 +34,7 @@ import io.element.android.libraries.matrix.impl.room.draft.into import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper import io.element.android.libraries.matrix.impl.room.powerlevels.RoomPowerLevelsValuesMapper +import io.element.android.libraries.matrix.impl.room.powerlevels.RustRoomPermissions import io.element.android.libraries.matrix.impl.room.tombstone.map import io.element.android.libraries.matrix.impl.roomdirectory.map import io.element.android.libraries.matrix.impl.timeline.toRustReceiptType @@ -178,6 +180,12 @@ class RustBaseRoom( } } + override suspend fun roomPermissions(): Result = withContext(roomDispatcher) { + runCatchingExceptions { + RustRoomPermissions(innerRoom.getPowerLevels()) + } + } + override suspend fun canUserInvite(userId: UserId): Result = withContext(roomDispatcher) { runCatchingExceptions { innerRoom.getPowerLevels().use { it.canUserInvite(userId.value) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/powerlevels/RustRoomPermissions.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/powerlevels/RustRoomPermissions.kt new file mode 100644 index 00000000000..57e20b166f7 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/powerlevels/RustRoomPermissions.kt @@ -0,0 +1,95 @@ +/* + * 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.matrix.impl.room.powerlevels + +import io.element.android.libraries.matrix.api.core.UserId +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.powerlevels.RoomPermissions +import io.element.android.libraries.matrix.impl.room.map +import org.matrix.rustcomponents.sdk.RoomPowerLevels + +class RustRoomPermissions( + private val inner: RoomPowerLevels, +) : RoomPermissions { + override fun canOwnUserBan(): Boolean { + return inner.canOwnUserBan() + } + + override fun canOwnUserInvite(): Boolean { + return inner.canOwnUserInvite() + } + + override fun canOwnUserKick(): Boolean { + return inner.canOwnUserKick() + } + + override fun canOwnUserPinUnpin(): Boolean { + return inner.canOwnUserPinUnpin() + } + + override fun canOwnUserRedactOther(): Boolean { + return inner.canOwnUserRedactOther() + } + + override fun canOwnUserRedactOwn(): Boolean { + return inner.canOwnUserRedactOwn() + } + + override fun canOwnUserSendMessage(message: MessageEventType): Boolean { + return inner.canOwnUserSendMessage(message.map()) + } + + override fun canOwnUserSendState(stateEvent: StateEventType): Boolean { + return inner.canOwnUserSendState(stateEvent.map()) + } + + override fun canOwnUserTriggerRoomNotification(): Boolean { + return inner.canOwnUserTriggerRoomNotification() + } + + override fun canUserBan(userId: UserId): Boolean { + return inner.canUserBan(userId.value) + } + + override fun canUserInvite(userId: UserId): Boolean { + return inner.canUserInvite(userId.value) + } + + override fun canUserKick(userId: UserId): Boolean { + return inner.canUserKick(userId.value) + } + + override fun canUserPinUnpin(userId: UserId): Boolean { + return inner.canUserPinUnpin(userId.value) + } + + override fun canUserRedactOther(userId: UserId): Boolean { + return inner.canUserRedactOther(userId.value) + } + + override fun canUserRedactOwn(userId: UserId): Boolean { + return inner.canUserRedactOwn(userId.value) + } + + override fun canUserSendMessage(userId: UserId, message: MessageEventType): Boolean { + return inner.canUserSendMessage(userId.value, message.map()) + } + + override fun canUserSendState(userId: UserId, stateEvent: StateEventType): Boolean { + return inner.canUserSendState(userId.value, stateEvent.map()) + } + + override fun canUserTriggerRoomNotification(userId: UserId): Boolean { + return inner.canUserTriggerRoomNotification(userId.value) + } + + override fun close() { + inner.close() + } +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt index 56f880dc11d..b05eff77e24 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt @@ -21,12 +21,14 @@ 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.StateEventType import io.element.android.libraries.matrix.api.room.draft.ComposerDraft +import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues import io.element.android.libraries.matrix.api.room.tombstone.PredecessorRoom import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.simulateLongTask import kotlinx.coroutines.CoroutineScope @@ -48,6 +50,7 @@ class FakeBaseRoom( private val userRoleResult: () -> Result = { lambdaError() }, private val getUpdatedMemberResult: (UserId) -> Result = { lambdaError() }, private val joinRoomResult: () -> Result = { lambdaError() }, + private val roomPermissionsResult: () -> Result = { Result.success(FakeRoomPermissions()) }, private val canInviteResult: (UserId) -> Result = { lambdaError() }, private val canKickResult: (UserId) -> Result = { lambdaError() }, private val canBanResult: (UserId) -> Result = { lambdaError() }, @@ -129,6 +132,10 @@ class FakeBaseRoom( return userRoleResult() } + override suspend fun roomPermissions(): Result { + return roomPermissionsResult() + } + override suspend fun getPermalink(): Result { return roomPermalinkResult() } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/powerlevels/FakeRoomPermissions.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/powerlevels/FakeRoomPermissions.kt new file mode 100644 index 00000000000..04ecb1f60b9 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/powerlevels/FakeRoomPermissions.kt @@ -0,0 +1,58 @@ +/* + * 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.matrix.test.room.powerlevels + +import io.element.android.libraries.matrix.api.core.UserId +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.powerlevels.RoomPermissions + +data class FakeRoomPermissions( + val ownerCanBan: Boolean = false, + val ownerCanInvite: Boolean = false, + val ownerCanKick: Boolean = false, + val ownerCanPinUnpin: Boolean = false, + val ownerCanRedactOther: Boolean = false, + val ownerCanRedactOwn: Boolean = false, + val ownerCanTriggerRoomNotification: Boolean = false, + val ownerCanSendMessage: (MessageEventType) -> Boolean = { false }, + val ownerCanSendState: (StateEventType) -> Boolean = { false }, + val userCanBan: (UserId) -> Boolean = { false }, + val userCanInvite: (UserId) -> Boolean = { false }, + val userCanKick: (UserId) -> Boolean = { false }, + val userCanPinUnpin: (UserId) -> Boolean = { false }, + val userCanRedactOther: (UserId) -> Boolean = { false }, + val userCanRedactOwn: (UserId) -> Boolean = { false }, + val userCanTriggerRoomNotification: (UserId) -> Boolean = { false }, + val userCanSendMessage: (UserId, MessageEventType) -> Boolean = { _, _ -> false }, + val userCanSendState: (UserId, StateEventType) -> Boolean = { _, _ -> false }, +) : RoomPermissions { + + override fun canOwnUserBan(): Boolean = ownerCanBan + override fun canOwnUserInvite(): Boolean = ownerCanInvite + override fun canOwnUserKick(): Boolean = ownerCanKick + override fun canOwnUserPinUnpin(): Boolean = ownerCanPinUnpin + override fun canOwnUserRedactOther(): Boolean = ownerCanRedactOther + override fun canOwnUserRedactOwn(): Boolean = ownerCanRedactOwn + override fun canOwnUserSendMessage(message: MessageEventType): Boolean = ownerCanSendMessage(message) + override fun canOwnUserSendState(stateEvent: StateEventType): Boolean = ownerCanSendState(stateEvent) + override fun canOwnUserTriggerRoomNotification(): Boolean = ownerCanTriggerRoomNotification + override fun canUserBan(userId: UserId): Boolean = userCanBan(userId) + override fun canUserInvite(userId: UserId): Boolean = userCanInvite(userId) + override fun canUserKick(userId: UserId): Boolean = userCanKick(userId) + override fun canUserPinUnpin(userId: UserId): Boolean = userCanPinUnpin(userId) + override fun canUserRedactOther(userId: UserId): Boolean = userCanRedactOther(userId) + override fun canUserRedactOwn(userId: UserId): Boolean = userCanRedactOwn(userId) + override fun canUserSendMessage(userId: UserId, message: MessageEventType): Boolean = userCanSendMessage(userId, message) + override fun canUserSendState(userId: UserId, stateEvent: StateEventType): Boolean = userCanSendState(userId, stateEvent) + override fun canUserTriggerRoomNotification(userId: UserId): Boolean = userCanTriggerRoomNotification(userId) + + override fun close() { + // no-op for the fake + } +} From d654280e300a9cff2d7a7190df1faf6d009f83ac Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 8 Dec 2025 22:23:07 +0100 Subject: [PATCH 2/5] misc(power level) : use new api --- .../api/KnockRequestPermissions.kt | 34 +++++++++ .../banner/KnockRequestsBannerPresenter.kt | 2 +- .../impl/data/KnockRequestPermissions.kt | 33 --------- .../impl/data/KnockRequestsModule.kt | 7 +- .../impl/data/KnockRequestsService.kt | 1 + .../impl/list/KnockRequestsListState.kt | 2 +- .../list/KnockRequestsListStateProvider.kt | 2 +- .../KnockRequestsBannerPresenterTest.kt | 2 +- .../list/KnockRequestsListPresenterTest.kt | 2 +- .../messages/impl/MessagesPresenter.kt | 30 ++------ .../messages/impl/UserEventPermissions.kt | 13 ++++ .../MessageComposerPresenter.kt | 5 +- .../list/PinnedMessagesListPresenter.kt | 69 ++++++++---------- .../impl/timeline/TimelinePresenter.kt | 19 +++-- .../roomcall/impl/RoomCallStatePresenter.kt | 6 +- .../roomdetails/impl/RoomDetailsPresenter.kt | 71 +++++++++---------- .../impl/members/RoomMemberListPresenter.kt | 5 +- .../api/RoomDetailsEditPermissions.kt | 37 ++++++++++ .../impl/RoomDetailsEditPresenter.kt | 21 +++--- .../impl/RoomMemberModerationPresenter.kt | 30 ++++---- .../api/SecurityAndPrivacyPermissions.kt | 24 +++---- .../impl/root/SecurityAndPrivacyPresenter.kt | 8 ++- .../impl/root/UserProfilePresenter.kt | 7 +- .../libraries/matrix/api/room/BaseRoom.kt | 51 ------------- .../api/room/powerlevels/RoomPermissions.kt | 50 +++++++++---- .../room/powerlevels/RoomPowerLevelsValues.kt | 52 -------------- .../impl/gallery/MediaGalleryPresenter.kt | 14 ++-- .../impl/model/MediaPermissions.kt | 29 ++++++++ .../impl/viewer/MediaViewerPresenter.kt | 12 ++-- 29 files changed, 308 insertions(+), 330 deletions(-) create mode 100644 features/knockrequests/api/src/main/kotlin/io/element/android/features/knockrequests/api/KnockRequestPermissions.kt delete mode 100644 features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPermissions.kt create mode 100644 features/roomdetailsedit/api/src/main/kotlin/io/element/android/features/roomdetailsedit/api/RoomDetailsEditPermissions.kt create mode 100644 libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/model/MediaPermissions.kt diff --git a/features/knockrequests/api/src/main/kotlin/io/element/android/features/knockrequests/api/KnockRequestPermissions.kt b/features/knockrequests/api/src/main/kotlin/io/element/android/features/knockrequests/api/KnockRequestPermissions.kt new file mode 100644 index 00000000000..82bceb5be0d --- /dev/null +++ b/features/knockrequests/api/src/main/kotlin/io/element/android/features/knockrequests/api/KnockRequestPermissions.kt @@ -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(), + ) +} diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt index 641efffaa32..f340d597b15 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt @@ -49,7 +49,7 @@ class KnockRequestsBannerPresenter( val shouldShowBanner by remember { derivedStateOf { - permissions.canHandle && knockRequests.isNotEmpty() + permissions.hasAny && knockRequests.isNotEmpty() } } diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPermissions.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPermissions.kt deleted file mode 100644 index 2ca4d4df749..00000000000 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPermissions.kt +++ /dev/null @@ -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 { - return syncUpdateFlow.map { - val canAccept = canInvite().getOrDefault(false) - val canDecline = canKick().getOrDefault(false) - val canBan = canBan().getOrDefault(false) - KnockRequestPermissions(canAccept, canDecline, canBan) - } -} diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsModule.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsModule.kt index eeba54f87d7..b51b78f1058 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsModule.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsModule.kt @@ -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) @@ -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 ) diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt index 04c2f7b316d..98570e6b28e 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt @@ -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 diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListState.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListState.kt index a1bb90cae89..ae770a297d3 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListState.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListState.kt @@ -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 diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt index 2c4c92a6b68..85fc0675ad0 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt @@ -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 diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt index 9eaa51cecb8..3161d3e81f0 100644 --- a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt +++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt @@ -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 diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt index b0d0b68c917..7102b017736 100644 --- a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt +++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt @@ -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 diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index a5b3106b3d6..8a5f3cf4239 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -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 @@ -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 @@ -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() } @@ -301,24 +297,6 @@ class MessagesPresenter( ) } - @Composable - private fun userEventPermissions(roomInfo: RoomInfo): State { - 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, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/UserEventPermissions.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/UserEventPermissions.kt index f7d221950b8..349c8e58dc2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/UserEventPermissions.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/UserEventPermissions.kt @@ -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. @@ -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() + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index b193e97fa38..b2c5182a0a9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -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 @@ -396,7 +397,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 } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt index cdc1f85f1d8..b2d2caa7f91 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt @@ -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 @@ -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 @@ -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 @@ -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) @@ -192,19 +192,6 @@ class PinnedMessagesListPresenter( } } - @Composable - private fun userEventPermissions(updateKey: Long): State { - 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>) -> Unit) { val updatedOnItemsChange by rememberUpdatedState(onItemsChange) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index e44cac4f276..4815602c8e5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -24,6 +24,7 @@ import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedInject 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.ResolveVerifiedUserSendFailureState 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.virtual.TimelineItemTypingNotificationModel 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.poll.api.actions.EndPollAction 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.asEventId 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.powerlevels.permissionsAsState 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.Timeline 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.ui.room.canSendMessageAsState 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.NotificationTapOpensTimeline @@ -128,11 +129,6 @@ class TimelinePresenter( 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(null) } val newEventState = remember { mutableStateOf(NewEventState.None) } @@ -285,13 +281,16 @@ class TimelinePresenter( val typingNotificationState = typingNotificationPresenter.present() val roomCallState = roomCallStatePresenter.present() + val userEventPermissions by room.permissionsAsState(UserEventPermissions.DEFAULT) { perms -> + perms.userEventPermissions() + } val timelineRoomInfo by remember(typingNotificationState, roomCallState, roomInfo) { derivedStateOf { TimelineRoomInfo( name = roomInfo.name, - isDm = roomInfo.isDm.orFalse(), - userHasPermissionToSendMessage = userHasPermissionToSendMessage, - userHasPermissionToSendReaction = userHasPermissionToSendReaction, + isDm = roomInfo.isDm, + userHasPermissionToSendMessage = userEventPermissions.canSendMessage, + userHasPermissionToSendReaction = userEventPermissions.canSendReaction, roomCallState = roomCallState, pinnedEventIds = roomInfo.pinnedEventIds, typingNotificationState = typingNotificationState, diff --git a/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt b/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt index 5de47f9ff2a..b18a2772a34 100644 --- a/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt +++ b/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt @@ -21,7 +21,8 @@ import io.element.android.features.enterprise.api.SessionEnterpriseService import io.element.android.features.roomcall.api.RoomCallState import io.element.android.libraries.architecture.Presenter 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 class RoomCallStatePresenter( @@ -35,8 +36,7 @@ class RoomCallStatePresenter( value = sessionEnterpriseService.isElementCallAvailable() } val roomInfo by room.roomInfoFlow.collectAsState() - val syncUpdateFlow = room.syncUpdateFlow.collectAsState() - val canJoinCall by room.canCall(updateKey = syncUpdateFlow.value) + val canJoinCall by room.permissionsAsState(false) { perms -> perms.canCall() } val isUserInTheCall by remember { derivedStateOf { room.sessionId in roomInfo.activeRoomCallParticipants diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index 95c4f1e9e2c..83840ad604b 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -10,6 +10,7 @@ package io.element.android.features.roomdetails.impl 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 @@ -18,11 +19,14 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import dev.zacsweers.metro.Inject import im.vector.app.features.analytics.plan.Interaction +import io.element.android.features.knockrequests.api.knockRequestPermissions import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.roomcall.api.RoomCallState 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.architecture.Presenter 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.room.JoinedRoom 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.StateEventType +import io.element.android.libraries.matrix.api.room.isDm 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.canSendState +import io.element.android.libraries.matrix.api.room.powerlevels.canEditRolesAndPermissions +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.ui.room.canHandleKnockRequestsAsState 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.isDmAsState -import io.element.android.libraries.matrix.ui.room.isOwnUserAdmin import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.ui.strings.CommonStrings @@ -77,8 +77,6 @@ class RoomDetailsPresenter( val scope = rememberCoroutineScope() val leaveRoomState = leaveRoomPresenter.present() val roomInfo by room.roomInfoFlow.collectAsState() - val isUserAdmin = room.isOwnUserAdmin() - val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val roomAvatar by remember { derivedStateOf { roomInfo.avatarUrl } } val roomName by remember { derivedStateOf { roomInfo.name?.trim().orEmpty() } } @@ -93,15 +91,11 @@ class RoomDetailsPresenter( observeNotificationSettings() } + val isDm = roomInfo.isDm val membersState by room.membersStateFlow.collectAsState() - val canInvite by getCanInvite(membersState) - + val permissions by getPermissions() val canonicalAlias by remember { derivedStateOf { roomInfo.canonicalAlias } } 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 currentMember by room.getCurrentRoomMember(membersState) val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMember) @@ -109,16 +103,15 @@ class RoomDetailsPresenter( val roomCallState = roomCallStatePresenter.present() val joinedMemberCount by remember { derivedStateOf { roomInfo.joinedMembersCount } } - val topicState = remember(canEditTopic, roomTopic, roomType) { + val topicState = remember(permissions.editDetailsPermissions.canEditTopic, roomTopic, roomType) { val topic = roomTopic when { !topic.isNullOrBlank() -> RoomTopicState.ExistingTopic(topic) - canEditTopic && roomType is RoomDetailsType.Room -> RoomTopicState.CanAddTopic + permissions.editDetailsPermissions.canEditTopic && roomType is RoomDetailsType.Room -> RoomTopicState.CanAddTopic else -> RoomTopicState.Hidden } } - val canHandleKnockRequests by room.canHandleKnockRequestsAsState(syncUpdateFlow.value) val isKnockRequestsEnabled by remember { featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock) }.collectAsState(false) @@ -126,7 +119,7 @@ class RoomDetailsPresenter( room.knockRequestsFlow.collect { value = it.size } } val canShowKnockRequests by remember { - derivedStateOf { isKnockRequestsEnabled && canHandleKnockRequests && joinRule == JoinRule.Knock } + derivedStateOf { isKnockRequestsEnabled && permissions.canManageKnockRequests && joinRule == JoinRule.Knock } } val isDeveloperModeEnabled by remember { appPreferencesStore.isDeveloperModeEnabledFlow() @@ -162,13 +155,6 @@ class RoomDetailsPresenter( 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) { room.roomMemberIdentityStateChange(waitForEncryption = true) .onEach { identities -> value = identities.any { it.identityState == IdentityState.VerificationViolation } } @@ -185,22 +171,22 @@ class RoomDetailsPresenter( roomTopic = topicState, memberCount = joinedMemberCount, isEncrypted = isEncrypted, - canInvite = canInvite, - canEdit = (canEditAvatar || canEditName || canEditTopic) && roomType == RoomDetailsType.Room, + canInvite = permissions.canInvite, + canEdit = roomType == RoomDetailsType.Room && permissions.editDetailsPermissions.hasAny, roomCallState = roomCallState, roomType = roomType, roomMemberDetailsState = roomMemberDetailsState, leaveRoomState = leaveRoomState, roomNotificationSettings = roomNotificationSettingsState.roomNotificationSettings(), isFavorite = isFavorite, - displayRolesAndPermissionsSettings = !isDm && isUserAdmin, + displayRolesAndPermissionsSettings = !isDm && permissions.canEditRolesAndPermissions, isPublic = joinRule == JoinRule.Public, heroes = roomInfo.heroes.toImmutableList(), pinnedMessagesCount = pinnedMessagesCount, snackbarMessage = snackbarMessage, canShowKnockRequests = canShowKnockRequests, knockRequestsCount = knockRequestsCount, - canShowSecurityAndPrivacy = canShowSecurityAndPrivacy, + canShowSecurityAndPrivacy = !isDm && permissions.canEditSecurityAndPrivacy, hasMemberVerificationViolations = hasMemberVerificationViolations, canReportRoom = canReportRoom, isTombstoned = roomInfo.successorRoom != null, @@ -232,14 +218,25 @@ class RoomDetailsPresenter( } } - @Composable - private fun getCanInvite(membersState: RoomMembersState) = produceState(false, membersState) { - value = room.canInvite().getOrElse { false } - } + private data class Permissions( + val canInvite: Boolean = false, + val editDetailsPermissions: RoomDetailsEditPermissions = RoomDetailsEditPermissions.DEFAULT, + val canManageKnockRequests: Boolean = false, + val canEditRolesAndPermissions: Boolean = false, + val canEditSecurityAndPrivacy: Boolean = false, + ) @Composable - private fun getCanSendState(membersState: RoomMembersState, type: StateEventType) = produceState(false, membersState) { - value = room.canSendState(type).getOrElse { false } + private fun getPermissions(): State { + 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() { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt index f1d3f61f7e9..6917057a062 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt @@ -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.RoomMembersState 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.toMatrixUser 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 kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.persistentMapOf @@ -58,8 +58,7 @@ class RoomMemberListPresenter( override fun present(): RoomMemberListState { var searchQuery by rememberSaveable { mutableStateOf("") } val membersState by room.membersStateFlow.collectAsState() - val syncUpdateFlow = room.syncUpdateFlow.collectAsState() - val canInvite by room.canInviteAsState(syncUpdateFlow.value) + val canInvite by room.permissionsAsState(false) { perms -> perms.canOwnUserInvite() } val roomModerationState = roomMembersModerationPresenter.present() val roomMemberIdentityStates by produceState(persistentMapOf()) { diff --git a/features/roomdetailsedit/api/src/main/kotlin/io/element/android/features/roomdetailsedit/api/RoomDetailsEditPermissions.kt b/features/roomdetailsedit/api/src/main/kotlin/io/element/android/features/roomdetailsedit/api/RoomDetailsEditPermissions.kt new file mode 100644 index 00000000000..f95f4466f2e --- /dev/null +++ b/features/roomdetailsedit/api/src/main/kotlin/io/element/android/features/roomdetailsedit/api/RoomDetailsEditPermissions.kt @@ -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), + ) +} diff --git a/features/roomdetailsedit/impl/src/main/kotlin/io/element/android/features/roomdetailsedit/impl/RoomDetailsEditPresenter.kt b/features/roomdetailsedit/impl/src/main/kotlin/io/element/android/features/roomdetailsedit/impl/RoomDetailsEditPresenter.kt index 5e685ff81f8..ed729928d87 100644 --- a/features/roomdetailsedit/impl/src/main/kotlin/io/element/android/features/roomdetailsedit/impl/RoomDetailsEditPresenter.kt +++ b/features/roomdetailsedit/impl/src/main/kotlin/io/element/android/features/roomdetailsedit/impl/RoomDetailsEditPresenter.kt @@ -23,6 +23,8 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.core.net.toUri 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.architecture.AsyncAction 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.mimetype.MimeTypes 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.canSendState +import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.mediapickers.api.PickerProvider import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider @@ -93,14 +94,8 @@ class RoomDetailsEditPresenter( } } - var canChangeName by remember { mutableStateOf(false) } - var canChangeTopic by remember { mutableStateOf(false) } - 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 permissions by room.permissionsAsState(RoomDetailsEditPermissions.DEFAULT){perms -> + perms.roomDetailsEditPermissions() } val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker( @@ -181,11 +176,11 @@ class RoomDetailsEditPresenter( return RoomDetailsEditState( roomId = room.roomId, roomRawName = roomRawNameEdited, - canChangeName = canChangeName, + canChangeName = permissions.canEditName, roomTopic = roomTopicEdited, - canChangeTopic = canChangeTopic, + canChangeTopic = permissions.canEditTopic, roomAvatarUrl = roomAvatarUriEdited, - canChangeAvatar = canChangeAvatar, + canChangeAvatar = permissions.canEditAvatar, avatarActions = avatarActions, saveButtonEnabled = saveButtonEnabled, saveAction = saveAction.value, diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt index 31ff2da2b27..96749ba5ac3 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt @@ -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.RoomMember 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.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.services.analytics.api.AnalyticsService import kotlinx.collections.immutable.ImmutableList @@ -56,8 +55,12 @@ class RoomMemberModerationPresenter( override fun present(): RoomMemberModerationState { val coroutineScope = rememberCoroutineScope() val syncUpdateFlow = room.syncUpdateFlow.collectAsState() - val canBan = room.canBanAsState(syncUpdateFlow.value) - val canKick = room.canKickAsState(syncUpdateFlow.value) + val permissions by room.permissionsAsState(Permissions()) { perms -> + Permissions( + canKick = perms.canOwnUserKick(), + canBan = perms.canOwnUserBan(), + ) + } val currentUserMemberPowerLevel = room.userPowerLevelAsState(syncUpdateFlow.value) val kickUserAsyncAction = @@ -80,8 +83,7 @@ class RoomMemberModerationPresenter( } moderationActions.value = computeModerationActions( member = member, - canKick = canKick.value, - canBan = canBan.value, + permissions = permissions, currentUserMemberPowerLevel = currentUserMemberPowerLevel.value, ) } @@ -134,8 +136,8 @@ class RoomMemberModerationPresenter( } return InternalRoomMemberModerationState( - canKick = canKick.value, - canBan = canBan.value, + canKick = permissions.canKick, + canBan = permissions.canBan, selectedUser = selectedUser, actions = moderationActions.value, kickUserAsyncAction = kickUserAsyncAction.value, @@ -147,8 +149,7 @@ class RoomMemberModerationPresenter( private fun computeModerationActions( member: RoomMember?, - canKick: Boolean, - canBan: Boolean, + permissions: Permissions, currentUserMemberPowerLevel: Long, ): ImmutableList { return buildList { @@ -158,11 +159,11 @@ class RoomMemberModerationPresenter( val canModerateThisUser = currentUserMemberPowerLevel > targetMemberPowerLevel // Assume the member is joined when it's unknown val membership = member?.membership ?: RoomMembershipState.JOIN - if (canKick) { + if (permissions.canKick) { val isKickEnabled = canModerateThisUser && membership.isActive() add(ModerationActionState(action = ModerationAction.KickUser, isEnabled = isKickEnabled)) } - if (canBan) { + if (permissions.canBan) { if (membership == RoomMembershipState.BAN) { add(ModerationActionState(action = ModerationAction.UnbanUser, isEnabled = canModerateThisUser)) } else { @@ -208,6 +209,11 @@ class RoomMemberModerationPresenter( ) } + private data class Permissions( + val canKick: Boolean = false, + val canBan: Boolean = false, + ) + private fun CoroutineScope.runActionAndWaitForMembershipChange( action: MutableState>, block: suspend () -> Result diff --git a/features/securityandprivacy/api/src/main/kotlin/io/element/android/features/securityandprivacy/api/SecurityAndPrivacyPermissions.kt b/features/securityandprivacy/api/src/main/kotlin/io/element/android/features/securityandprivacy/api/SecurityAndPrivacyPermissions.kt index 82ab30581e9..bacd863ca6e 100644 --- a/features/securityandprivacy/api/src/main/kotlin/io/element/android/features/securityandprivacy/api/SecurityAndPrivacyPermissions.kt +++ b/features/securityandprivacy/api/src/main/kotlin/io/element/android/features/securityandprivacy/api/SecurityAndPrivacyPermissions.kt @@ -8,13 +8,8 @@ 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.powerlevels.canSendState +import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions data class SecurityAndPrivacyPermissions( val canChangeRoomAccess: Boolean, @@ -37,14 +32,11 @@ data class SecurityAndPrivacyPermissions( } } -@Composable -fun BaseRoom.securityAndPrivacyPermissionsAsState(updateKey: Long): State { - return produceState(DEFAULT, key1 = updateKey) { - value = SecurityAndPrivacyPermissions( - canChangeRoomAccess = canSendState(type = StateEventType.ROOM_JOIN_RULES).getOrElse { false }, - canChangeHistoryVisibility = canSendState(type = StateEventType.ROOM_HISTORY_VISIBILITY).getOrElse { false }, - canChangeEncryption = canSendState(type = StateEventType.ROOM_ENCRYPTION).getOrElse { false }, - canChangeRoomVisibility = canSendState(type = StateEventType.ROOM_CANONICAL_ALIAS).getOrElse { false }, - ) - } +fun RoomPermissions.securityAndPrivacyPermissions(): SecurityAndPrivacyPermissions { + return SecurityAndPrivacyPermissions( + canChangeRoomAccess = canOwnUserSendState(StateEventType.ROOM_JOIN_RULES), + canChangeHistoryVisibility = canOwnUserSendState(StateEventType.ROOM_HISTORY_VISIBILITY), + canChangeEncryption = canOwnUserSendState(StateEventType.ROOM_ENCRYPTION), + canChangeRoomVisibility = canOwnUserSendState(StateEventType.ROOM_CANONICAL_ALIAS), + ) } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt index b77ad0e7d6c..af0f44be38b 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt @@ -21,7 +21,8 @@ import androidx.compose.runtime.setValue import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory 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.editroomaddress.matchesServer 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.history.RoomHistoryVisibility 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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async @@ -106,7 +108,9 @@ class SecurityAndPrivacyPresenter( ) 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) { when (event) { diff --git a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt index 9bf31e0b07b..8b7eddca547 100644 --- a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt +++ b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt @@ -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.core.RoomId 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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.distinctUntilChanged @@ -56,7 +58,7 @@ class UserProfilePresenter( @Composable private fun getDmRoomId(): State { - return produceState(initialValue = null) { + return produceState(initialValue = null) { value = client.findDM(userId).getOrNull() } } @@ -66,7 +68,6 @@ class UserProfilePresenter( val isElementCallAvailable by produceState(initialValue = false, roomId) { value = sessionEnterpriseService.isElementCallAvailable() } - return produceState(initialValue = false, isElementCallAvailable, roomId) { value = when { isElementCallAvailable.not() -> false @@ -75,7 +76,7 @@ class UserProfilePresenter( roomId ?.let { client.getRoom(it) } ?.use { room -> - room.canUserJoinCall(client.sessionId).getOrNull() + room.roomPermissions().use(false){ perms -> perms.canCall()} } .orFalse() } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt index cde3a5eff71..481171ace0a 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt @@ -130,57 +130,6 @@ interface BaseRoom : Closeable { */ suspend fun forget(): Result - /** - * Returns `true` if the user with the provided [userId] can invite other users to the room. - */ - suspend fun canUserInvite(userId: UserId): Result - - /** - * Returns `true` if the user with the provided [userId] can kick other users from the room. - */ - suspend fun canUserKick(userId: UserId): Result - - /** - * Returns `true` if the user with the provided [userId] can ban other users from the room. - */ - suspend fun canUserBan(userId: UserId): Result - - /** - * Returns `true` if the user with the provided [userId] can redact their own messages. - */ - suspend fun canUserRedactOwn(userId: UserId): Result - - /** - * Returns `true` if the user with the provided [userId] can redact messages from other users. - */ - suspend fun canUserRedactOther(userId: UserId): Result - - /** - * Returns `true` if the user with the provided [userId] can send state events. - */ - suspend fun canUserSendState(userId: UserId, type: StateEventType): Result - - /** - * Returns `true` if the user with the provided [userId] can send messages. - */ - suspend fun canUserSendMessage(userId: UserId, type: MessageEventType): Result - - /** - * Returns `true` if the user with the provided [userId] can trigger an `@room` notification. - */ - suspend fun canUserTriggerRoomNotification(userId: UserId): Result - - /** - * Returns `true` if the user with the provided [userId] can pin or unpin messages. - */ - suspend fun canUserPinUnpin(userId: UserId): Result - - /** - * Returns `true` if the user with the provided [userId] can join or starts calls. - */ - suspend fun canUserJoinCall(userId: UserId): Result = - canUserSendState(userId, StateEventType.CALL_MEMBER) - /** * Sets the room as favorite or not, based on the [isFavorite] parameter. */ diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPermissions.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPermissions.kt index cdc735b8199..735627e10ec 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPermissions.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPermissions.kt @@ -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.canEditRolesAndPermissions(): Boolean { + return canOwnUserSendState(StateEventType.ROOM_POWER_LEVELS) } -fun RoomPermissions.canManageKnockRequests(): Boolean { - return canOwnUserInvite() || canOwnUserBan() || canOwnUserKick() +fun RoomPermissions.canCall(): Boolean { + return canOwnUserSendState(StateEventType.CALL_MEMBER) } -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 Result.use(default: T, block: (RoomPermissions) -> T): T { + return fold( + onSuccess = { perms -> + perms.use(block) + }, + onFailure = { + default + } + ) } -fun RoomPermissions.canEditRolesAndPermissions(): Boolean { - return canOwnUserSendState(StateEventType.ROOM_POWER_LEVELS) +fun BaseRoom.permissionsFlow(default: T, block: (RoomPermissions) -> T): Flow { + return roomInfoFlow + .map { info -> info.roomPowerLevels } + .distinctUntilChanged() + .map { + roomPermissions().use(default, block) + } +} + +@Composable +fun BaseRoom.permissionsAsState(default: T, block: (RoomPermissions) -> T): State { + return remember(this, default, block) { + Timber.d("Computing permissionsAsState for room $roomId with default=$default") + permissionsFlow(default, block) + }.collectAsState(default) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPowerLevelsValues.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPowerLevelsValues.kt index d20b6141eb9..a7eebbb99c8 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPowerLevelsValues.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPowerLevelsValues.kt @@ -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 = canUserInvite(sessionId) - -/** - * Shortcut for calling [BaseRoom.canUserKick] with our own user. - */ -suspend fun BaseRoom.canKick(): Result = canUserKick(sessionId) - -/** - * Shortcut for calling [BaseRoom.canUserBan] with our own user. - */ -suspend fun BaseRoom.canBan(): Result = canUserBan(sessionId) - -/** - * Shortcut for calling [BaseRoom.canUserSendState] with our own user. - */ -suspend fun BaseRoom.canSendState(type: StateEventType): Result = canUserSendState(sessionId, type) - -/** - * Shortcut for calling [BaseRoom.canUserSendMessage] with our own user. - */ -suspend fun BaseRoom.canSendMessage(type: MessageEventType): Result = canUserSendMessage(sessionId, type) - -/** - * Shortcut for calling [BaseRoom.canUserRedactOwn] with our own user. - */ -suspend fun BaseRoom.canRedactOwn(): Result = canUserRedactOwn(sessionId) - -/** - * Shortcut for calling [BaseRoom.canRedactOther] with our own user. - */ -suspend fun BaseRoom.canRedactOther(): Result = canUserRedactOther(sessionId) - -/** - * Shortcut for checking if current user can handle knock requests. - */ -suspend fun BaseRoom.canHandleKnockRequests(): Result = runCatchingExceptions { - canInvite().getOrThrow() || canBan().getOrThrow() || canKick().getOrThrow() -} - -/** - * Shortcut for calling [BaseRoom.canUserPinUnpin] with our own user. - */ -suspend fun BaseRoom.canPinUnpin(): Result = canUserPinUnpin(sessionId) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt index aee9af82b7d..e5e493ee63b 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt @@ -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? { diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/model/MediaPermissions.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/model/MediaPermissions.kt new file mode 100644 index 00000000000..4a111965e96 --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/model/MediaPermissions.kt @@ -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(), + ) +} diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt index 4709a4ec519..dc0feb70cf0 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt @@ -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.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, From 541a1d29bdba24c9116219b919c016700a043755 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 8 Dec 2025 22:23:17 +0100 Subject: [PATCH 3/5] misc(power level) : remove old api --- .../matrix/impl/room/RustBaseRoom.kt | 54 ------------- .../matrix/ui/room/MatrixRoomState.kt | 79 ------------------- 2 files changed, 133 deletions(-) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt index eec9591e32a..141957d1eda 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt @@ -186,60 +186,6 @@ class RustBaseRoom( } } - override suspend fun canUserInvite(userId: UserId): Result = withContext(roomDispatcher) { - runCatchingExceptions { - innerRoom.getPowerLevels().use { it.canUserInvite(userId.value) } - } - } - - override suspend fun canUserKick(userId: UserId): Result = withContext(roomDispatcher) { - runCatchingExceptions { - innerRoom.getPowerLevels().use { it.canUserKick(userId.value) } - } - } - - override suspend fun canUserBan(userId: UserId): Result = withContext(roomDispatcher) { - runCatchingExceptions { - innerRoom.getPowerLevels().use { it.canUserBan(userId.value) } - } - } - - override suspend fun canUserRedactOwn(userId: UserId): Result = withContext(roomDispatcher) { - runCatchingExceptions { - innerRoom.getPowerLevels().use { it.canUserRedactOwn(userId.value) } - } - } - - override suspend fun canUserRedactOther(userId: UserId): Result = withContext(roomDispatcher) { - runCatchingExceptions { - innerRoom.getPowerLevels().use { it.canUserRedactOther(userId.value) } - } - } - - override suspend fun canUserSendState(userId: UserId, type: StateEventType): Result = withContext(roomDispatcher) { - runCatchingExceptions { - innerRoom.getPowerLevels().use { it.canUserSendState(userId.value, type.map()) } - } - } - - override suspend fun canUserSendMessage(userId: UserId, type: MessageEventType): Result = withContext(roomDispatcher) { - runCatchingExceptions { - innerRoom.getPowerLevels().use { it.canUserSendMessage(userId.value, type.map()) } - } - } - - override suspend fun canUserTriggerRoomNotification(userId: UserId): Result = withContext(roomDispatcher) { - runCatchingExceptions { - innerRoom.getPowerLevels().use { it.canUserTriggerRoomNotification(userId.value) } - } - } - - override suspend fun canUserPinUnpin(userId: UserId): Result = withContext(roomDispatcher) { - runCatchingExceptions { - innerRoom.getPowerLevels().use { it.canUserPinUnpin(userId.value) } - } - } - override suspend fun clearEventCacheStorage(): Result = withContext(roomDispatcher) { runCatchingExceptions { innerRoom.clearEventCacheStorage() diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt index 03e0e5bea1a..d4e6127b697 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt @@ -14,88 +14,9 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState 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.RoomMember -import io.element.android.libraries.matrix.api.room.isDm -import io.element.android.libraries.matrix.api.room.powerlevels.canBan -import io.element.android.libraries.matrix.api.room.powerlevels.canHandleKnockRequests -import io.element.android.libraries.matrix.api.room.powerlevels.canInvite -import io.element.android.libraries.matrix.api.room.powerlevels.canKick -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.ui.model.roleOf -@Composable -fun BaseRoom.canSendMessageAsState(type: MessageEventType, updateKey: Long): State { - return produceState(initialValue = true, key1 = updateKey) { - value = canSendMessage(type).getOrElse { true } - } -} - -@Composable -fun BaseRoom.canInviteAsState(updateKey: Long): State { - return produceState(initialValue = false, key1 = updateKey) { - value = canInvite().getOrElse { false } - } -} - -@Composable -fun BaseRoom.canRedactOwnAsState(updateKey: Long): State { - return produceState(initialValue = false, key1 = updateKey) { - value = canRedactOwn().getOrElse { false } - } -} - -@Composable -fun BaseRoom.canRedactOtherAsState(updateKey: Long): State { - return produceState(initialValue = false, key1 = updateKey) { - value = canRedactOther().getOrElse { false } - } -} - -@Composable -fun BaseRoom.canCall(updateKey: Long): State { - return produceState(initialValue = false, key1 = updateKey) { - value = canUserJoinCall(sessionId).getOrElse { false } - } -} - -@Composable -fun BaseRoom.canPinUnpin(updateKey: Long): State { - return produceState(initialValue = false, key1 = updateKey) { - value = canUserPinUnpin(sessionId).getOrElse { false } - } -} - -@Composable -fun BaseRoom.isDmAsState(): State { - return produceState(initialValue = false) { - roomInfoFlow.collect { value = it.isDm } - } -} - -@Composable -fun BaseRoom.canKickAsState(updateKey: Long): State { - return produceState(initialValue = false, key1 = updateKey) { - value = canKick().getOrElse { false } - } -} - -@Composable -fun BaseRoom.canBanAsState(updateKey: Long): State { - return produceState(initialValue = false, key1 = updateKey) { - value = canBan().getOrElse { false } - } -} - -@Composable -fun BaseRoom.canHandleKnockRequestsAsState(updateKey: Long): State { - return produceState(initialValue = false, key1 = updateKey) { - value = canHandleKnockRequests().getOrElse { false } - } -} - @Composable fun BaseRoom.userPowerLevelAsState(updateKey: Long): State { return produceState(initialValue = 0, key1 = updateKey) { From d26f21a53b2e666a2a9de57aaec28b9aee62fa42 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 9 Dec 2025 20:50:31 +0100 Subject: [PATCH 4/5] misc(power level) : update tests following api change --- .../MessageComposerPresenter.kt | 3 +- .../impl/timeline/TimelinePresenter.kt | 2 +- .../messages/impl/MessagesPresenterTest.kt | 196 +++++--------- .../MessageComposerPresenterTest.kt | 45 +++- .../list/PinnedMessagesListPresenterTest.kt | 43 ++- .../impl/timeline/TimelinePresenterTest.kt | 151 +++++------ .../impl/RoomCallStatePresenterTest.kt | 28 +- .../roomdetails/impl/MatrixRoomFixture.kt | 27 +- .../impl/RoomDetailsPresenterTest.kt | 246 ++++++------------ .../members/RoomMemberListPresenterTest.kt | 23 +- .../api/RoomDetailsEditPermissions.kt | 2 +- .../impl/RoomDetailsEditPresenter.kt | 3 +- .../impl/RoomDetailsEditPresenterTest.kt | 65 ++--- .../impl/RoomMemberModerationPresenterTest.kt | 7 +- .../impl/root/SecurityAndPrivacyPresenter.kt | 1 - .../impl/SecurityAndPrivacyPresenterTest.kt | 39 ++- .../impl/root/UserProfilePresenter.kt | 2 +- .../impl/UserProfilePresenterTest.kt | 24 +- .../api/room/powerlevels/RoomPermissions.kt | 12 +- .../matrix/impl/room/RustBaseRoom.kt | 2 - .../matrix/test/room/FakeBaseRoom.kt | 56 +--- .../room/powerlevels/FakeRoomPermissions.kt | 72 ++--- .../impl/gallery/MediaGalleryPresenter.kt | 1 - .../impl/gallery/MediaGalleryPresenterTest.kt | 17 +- .../impl/viewer/MediaViewerPresenterTest.kt | 45 +++- 25 files changed, 475 insertions(+), 637 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index b2c5182a0a9..fd803109c75 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -99,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, @@ -397,7 +398,7 @@ class MessageComposerPresenter( val currentUserId = room.sessionId suspend fun canSendRoomMention(): Boolean { - val userCanSendAtRoom = room.roomPermissions().use(false){ perms -> + val userCanSendAtRoom = room.roomPermissions().use(false) { perms -> perms.canOwnUserTriggerRoomNotification() } return !room.isDm() && userCanSendAtRoom diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index 4815602c8e5..289ec2924dd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -39,7 +39,6 @@ import io.element.android.features.poll.api.actions.EndPollAction import io.element.android.features.poll.api.actions.SendPollResponseAction import io.element.android.features.roomcall.api.RoomCallState import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.featureflag.api.FeatureFlagService @@ -96,6 +95,7 @@ class TimelinePresenter( private val analyticsService: AnalyticsService, ) : Presenter { private val tag = "TimelinePresenter" + @AssistedFactory interface Factory { fun create( diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index de2c8a81c90..cd5bcf8a6d3 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -64,6 +64,7 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.room.MessageEventType 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.StateEventType import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo @@ -86,6 +87,7 @@ import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomMember +import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.matrix.test.timeline.aTimelineItemDebugInfo import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails @@ -143,11 +145,7 @@ class MessagesPresenterTest { fun `present - check that the room's unread flag is removed`() = runTest { val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, + roomPermissions = roomPermissions(), markAsReadResult = { lambdaError() } ), typingNoticeResult = { Result.success(Unit) }, @@ -173,11 +171,7 @@ class MessagesPresenterTest { } val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, + roomPermissions = roomPermissions(), ), liveTimeline = timeline, typingNoticeResult = { Result.success(Unit) }, @@ -223,11 +217,7 @@ class MessagesPresenterTest { } val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, + roomPermissions = roomPermissions(), ), liveTimeline = timeline, typingNoticeResult = { Result.success(Unit) }, @@ -288,11 +278,7 @@ class MessagesPresenterTest { val event = aMessageEvent() val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, + roomPermissions = roomPermissions(), eventPermalinkResult = { Result.success("a link") }, ), typingNoticeResult = { Result.success(Unit) }, @@ -514,11 +500,7 @@ class MessagesPresenterTest { val liveTimeline = FakeTimeline() val joinedRoom = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, + roomPermissions = roomPermissions(), ), liveTimeline = liveTimeline, typingNoticeResult = { Result.success(Unit) }, @@ -586,11 +568,7 @@ class MessagesPresenterTest { fun `present - shows prompt to reinvite users in DM`() = runTest { val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, + roomPermissions = roomPermissions(), ).apply { givenRoomInfo(aRoomInfo(isDirect = true, joinedMembersCount = 1, activeMembersCount = 1)) }, @@ -619,11 +597,7 @@ class MessagesPresenterTest { fun `present - doesn't show reinvite prompt in non-direct room`() = runTest { val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, + roomPermissions = roomPermissions(), ).apply { givenRoomInfo(aRoomInfo(isDirect = false, joinedMembersCount = 1, activeMembersCount = 1)) }, @@ -645,11 +619,7 @@ class MessagesPresenterTest { fun `present - doesn't show reinvite prompt if other party is present`() = runTest { val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, + roomPermissions = roomPermissions(), ).apply { givenRoomInfo(aRoomInfo(isDirect = true, joinedMembersCount = 2, activeMembersCount = 2)) }, @@ -672,11 +642,7 @@ class MessagesPresenterTest { val inviteUserResult = lambdaRecorder { _: UserId -> Result.success(Unit) } val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, + roomPermissions = roomPermissions(), ), typingNoticeResult = { Result.success(Unit) }, inviteUserResult = inviteUserResult, @@ -707,11 +673,7 @@ class MessagesPresenterTest { val inviteUserResult = lambdaRecorder { _: UserId -> Result.success(Unit) } val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, + roomPermissions = roomPermissions(), ), typingNoticeResult = { Result.success(Unit) }, inviteUserResult = inviteUserResult, @@ -744,11 +706,7 @@ class MessagesPresenterTest { fun `present - handle reinviting other user when memberlist is not ready`() = runTest { val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, + roomPermissions = roomPermissions(), ), typingNoticeResult = { Result.success(Unit) }, ) @@ -769,11 +727,7 @@ class MessagesPresenterTest { fun `present - handle reinviting other user when inviting fails`() = runTest { val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, + roomPermissions = roomPermissions(), ), typingNoticeResult = { Result.success(Unit) }, inviteUserResult = { Result.failure(RuntimeException("Oops!")) }, @@ -807,17 +761,7 @@ class MessagesPresenterTest { fun `present - permission to post`() = runTest { val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, - canUserSendMessageResult = { _, messageEventType -> - when (messageEventType) { - MessageEventType.RoomMessage -> Result.success(true) - MessageEventType.Reaction -> Result.success(true) - else -> lambdaError() - } - }, + roomPermissions = roomPermissions(), ), typingNoticeResult = { Result.success(Unit) }, ) @@ -833,17 +777,9 @@ class MessagesPresenterTest { fun `present - no permission to post`() = runTest { val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, - canUserSendMessageResult = { _, messageEventType -> - when (messageEventType) { - MessageEventType.RoomMessage -> Result.success(false) - MessageEventType.Reaction -> Result.success(false) - else -> lambdaError() - } - }, + roomPermissions = roomPermissions( + canSendMessage = false + ), ), typingNoticeResult = { Result.success(Unit) }, ) @@ -859,11 +795,9 @@ class MessagesPresenterTest { fun `present - permission to redact own`() = runTest { val joinedRoom = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canRedactOwnResult = { Result.success(true) }, - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOtherResult = { Result.success(false) }, - canUserJoinCallResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, + roomPermissions = roomPermissions( + canRedactOther = false + ), ), typingNoticeResult = { Result.success(Unit) }, ) @@ -880,11 +814,9 @@ class MessagesPresenterTest { fun `present - permission to redact other`() = runTest { val joinedRoom = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canRedactOtherResult = { Result.success(true) }, - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(false) }, - canUserJoinCallResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, + roomPermissions = roomPermissions( + canRedactOwn = false + ), ), typingNoticeResult = { Result.success(Unit) }, ) @@ -929,11 +861,7 @@ class MessagesPresenterTest { val timeline = FakeTimeline() val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, + roomPermissions = roomPermissions(), ), liveTimeline = timeline, typingNoticeResult = { Result.success(Unit) }, @@ -973,11 +901,7 @@ class MessagesPresenterTest { val analyticsService = FakeAnalyticsService() val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, + roomPermissions = roomPermissions(), ), liveTimeline = timeline, typingNoticeResult = { Result.success(Unit) }, @@ -1074,11 +998,7 @@ class MessagesPresenterTest { } val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, + roomPermissions = roomPermissions(), ), liveTimeline = timeline, typingNoticeResult = { Result.success(Unit) }, @@ -1115,11 +1035,7 @@ class MessagesPresenterTest { val successorReason = "This room has been moved to a new location" val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, + roomPermissions = roomPermissions(), initialRoomInfo = aRoomInfo( successorRoom = SuccessorRoom( roomId = successorRoomId, @@ -1143,11 +1059,7 @@ class MessagesPresenterTest { fun `present - room without successor room has null successor info in state`() = runTest { val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, + roomPermissions = roomPermissions(), initialRoomInfo = aRoomInfo(successorRoom = null) ), typingNoticeResult = { Result.success(Unit) }, @@ -1165,11 +1077,13 @@ class MessagesPresenterTest { val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( sessionId = A_SESSION_ID, - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, + roomPermissions = FakeRoomPermissions( + canSendState = { true }, + canSendMessage = { true }, + canRedactOther = true, + canRedactOwn = true, + canPinUnpin = true, + ), initialRoomInfo = aRoomInfo(isDirect = true, isEncrypted = true) ).apply { givenRoomMembersState(RoomMembersState.Ready(persistentListOf(aRoomMember(userId = A_SESSION_ID), aRoomMember(userId = A_USER_ID_2)))) @@ -1312,16 +1226,44 @@ class MessagesPresenterTest { } } + private fun roomPermissions( + canStartCall: Boolean = true, + canRedactOther: Boolean = true, + canRedactOwn: Boolean = true, + canSendMessage: Boolean = true, + canSendReaction: Boolean = true, + canPinUnpin: Boolean = true, + ) = FakeRoomPermissions( + canSendState = { type -> + when (type) { + StateEventType.CALL_MEMBER -> canStartCall + else -> lambdaError() + } + }, + canSendMessage = { type -> + when (type) { + MessageEventType.RoomMessage -> canSendMessage + MessageEventType.Reaction -> canSendReaction + else -> lambdaError() + } + }, + canRedactOther = canRedactOther, + canRedactOwn = canRedactOwn, + canPinUnpin = canPinUnpin, + ) + private fun TestScope.createMessagesPresenter( coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(), timeline: Timeline = FakeTimeline(), joinedRoom: FakeJoinedRoom = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, + roomPermissions = FakeRoomPermissions( + canSendState = { true }, + canSendMessage = { true }, + canRedactOther = true, + canRedactOwn = true, + canPinUnpin = true, + ), ).apply { givenRoomInfo(aRoomInfo(id = roomId, name = "")) }, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt index 4a6e7771163..2feafbc9e55 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt @@ -69,6 +69,7 @@ import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomMember +import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails import io.element.android.libraries.mediapickers.api.PickerProvider @@ -991,9 +992,12 @@ class MessageComposerPresenterTest { val invitedUser = aRoomMember(userId = A_USER_ID_3, membership = RoomMembershipState.INVITE) val bob = aRoomMember(userId = A_USER_ID_2, membership = RoomMembershipState.JOIN) val david = aRoomMember(userId = A_USER_ID_4, displayName = "Dave", membership = RoomMembershipState.JOIN) - var canUserTriggerRoomNotificationResult = true val room = FakeJoinedRoom( - baseRoom = FakeBaseRoom(canUserTriggerRoomNotificationResult = { Result.success(canUserTriggerRoomNotificationResult) }), + baseRoom = FakeBaseRoom( + roomPermissions = FakeRoomPermissions( + canTriggerRoomNotification = true, + ) + ), typingNoticeResult = { Result.success(Unit) } ).apply { givenRoomMembersState( @@ -1033,10 +1037,38 @@ class MessageComposerPresenterTest { // If the suggestion isn't a mention, no suggestions are returned initialState.eventSink(MessageComposerEvent.SuggestionReceived(Suggestion(0, 0, SuggestionType.Command, ""))) assertThat(awaitItem().suggestions).isEmpty() + } + } - // If user has no permission to send `@room` mentions, `RoomMemberSuggestion.Room` is not returned - canUserTriggerRoomNotificationResult = false + @Test + fun `present - room mention suggestions no permission`() = runTest { + val currentUser = aRoomMember(userId = A_USER_ID, membership = RoomMembershipState.JOIN) + val invitedUser = aRoomMember(userId = A_USER_ID_3, membership = RoomMembershipState.INVITE) + val bob = aRoomMember(userId = A_USER_ID_2, membership = RoomMembershipState.JOIN) + val david = aRoomMember(userId = A_USER_ID_4, displayName = "Dave", membership = RoomMembershipState.JOIN) + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = FakeRoomPermissions( + canTriggerRoomNotification = false, + ) + ), + typingNoticeResult = { Result.success(Unit) } + ).apply { + givenRoomMembersState( + RoomMembersState.Ready( + persistentListOf(currentUser, invitedUser, bob, david), + ) + ) + givenRoomInfo(aRoomInfo(isDirect = false)) + } + val presenter = createPresenter(room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + // An empty suggestion returns the joined members that are not the current user, but not the room initialState.eventSink(MessageComposerEvent.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, ""))) + skipItems(1) assertThat(awaitItem().suggestions) .containsExactly(ResolvedSuggestion.Member(bob), ResolvedSuggestion.Member(david)) } @@ -1049,7 +1081,9 @@ class MessageComposerPresenterTest { val bob = aRoomMember(userId = A_USER_ID_2, membership = RoomMembershipState.JOIN) val david = aRoomMember(userId = A_USER_ID_4, displayName = "Dave", membership = RoomMembershipState.JOIN) val room = FakeJoinedRoom( - baseRoom = FakeBaseRoom(canUserTriggerRoomNotificationResult = { Result.success(true) }), + baseRoom = FakeBaseRoom( + roomPermissions = FakeRoomPermissions(canTriggerRoomNotification = true), + ), typingNoticeResult = { Result.success(Unit) } ).apply { givenRoomMembersState( @@ -1069,7 +1103,6 @@ class MessageComposerPresenterTest { presenter.present() }.test { val initialState = awaitItem() - // An empty suggestion returns the joined members that are not the current user, but not the room initialState.eventSink(MessageComposerEvent.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, ""))) skipItems(1) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt index 8807951d9de..351f841fcf5 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt @@ -31,6 +31,7 @@ import io.element.android.libraries.matrix.test.A_UNIQUE_ID import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions import io.element.android.libraries.matrix.test.sync.FakeSyncService import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.matrix.test.timeline.aMessageContent @@ -55,9 +56,7 @@ class PinnedMessagesListPresenterTest { fun `present - initial state`() = runTest { val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, + roomPermissions = roomPermissions(), ).apply { givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID))) } @@ -74,9 +73,7 @@ class PinnedMessagesListPresenterTest { fun `present - timeline failure state`() = runTest { val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, + roomPermissions = roomPermissions(), ).apply { givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID))) }, @@ -95,9 +92,7 @@ class PinnedMessagesListPresenterTest { fun `present - empty state`() = runTest { val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, + roomPermissions = roomPermissions(), ).apply { givenRoomInfo(aRoomInfo(pinnedEventIds = listOf())) }, @@ -117,9 +112,7 @@ class PinnedMessagesListPresenterTest { val pinnedEventsTimeline = createPinnedMessagesTimeline() val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, + roomPermissions = roomPermissions(), ).apply { givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID))) }, @@ -146,9 +139,7 @@ class PinnedMessagesListPresenterTest { val analyticsService = FakeAnalyticsService() val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, + roomPermissions = roomPermissions(), ).apply { givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID))) }, @@ -194,9 +185,7 @@ class PinnedMessagesListPresenterTest { val pinnedEventsTimeline = createPinnedMessagesTimeline() val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, + roomPermissions = roomPermissions(), ).apply { givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID))) }, @@ -225,9 +214,7 @@ class PinnedMessagesListPresenterTest { val pinnedEventsTimeline = createPinnedMessagesTimeline() val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, + roomPermissions = roomPermissions(), ).apply { givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID))) }, @@ -256,9 +243,7 @@ class PinnedMessagesListPresenterTest { val pinnedEventsTimeline = createPinnedMessagesTimeline() val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, + roomPermissions = roomPermissions(), ).apply { givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID))) }, @@ -295,6 +280,16 @@ class PinnedMessagesListPresenterTest { ) } + private fun roomPermissions( + canRedactOther: Boolean = true, + canRedactOwn: Boolean = true, + canPinUnpin: Boolean = true, + ) = FakeRoomPermissions( + canRedactOther = canRedactOther, + canRedactOwn = canRedactOwn, + canPinUnpin = canPinUnpin, + ) + private fun TestScope.createPinnedMessagesListPresenter( navigator: PinnedMessagesListNavigator = FakePinnedMessagesListNavigator(), room: JoinedRoom = FakeJoinedRoom(), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt index 13c28da6e97..b84975c6a50 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt @@ -8,8 +8,6 @@ package io.element.android.features.messages.impl.timeline -import app.cash.molecule.RecompositionMode -import app.cash.molecule.moleculeFlow import app.cash.turbine.ReceiveTurbine import app.cash.turbine.test import com.google.common.truth.Truth.assertThat @@ -35,6 +33,7 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.ThreadId 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.room.MessageEventType import io.element.android.libraries.matrix.api.room.RoomMembersState import io.element.android.libraries.matrix.api.room.tombstone.PredecessorRoom import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem @@ -55,6 +54,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.aRoomMember +import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.matrix.test.timeline.aMessageContent import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem @@ -66,6 +66,7 @@ import io.element.android.tests.testutils.awaitLastSequentialItem import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.lambda.any import io.element.android.tests.testutils.lambda.assert +import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.test @@ -97,9 +98,7 @@ class TimelinePresenterTest { @Test fun `present - initial state`() = runTest { val presenter = createTimelinePresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitFirstItem() assertThat(initialState.timelineItems).isEmpty() assertThat(initialState.isLive).isTrue() @@ -118,9 +117,7 @@ class TimelinePresenterTest { this.paginateLambda = paginateLambda } val presenter = createTimelinePresenter(timeline = timeline) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitItem() initialState.eventSink.invoke(TimelineEvents.LoadMore(Timeline.PaginationDirection.BACKWARDS)) initialState.eventSink.invoke(TimelineEvents.LoadMore(Timeline.PaginationDirection.FORWARDS)) @@ -166,9 +163,6 @@ class TimelinePresenterTest { ) val room = FakeJoinedRoom( liveTimeline = timeline, - baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, - ) ) val sessionPreferencesStore = InMemorySessionPreferencesStore(isSendPublicReadReceiptsEnabled = isSendPublicReadReceiptsEnabled) val presenter = createTimelinePresenter( @@ -176,9 +170,7 @@ class TimelinePresenterTest { room = room, sessionPreferencesStore = sessionPreferencesStore, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitFirstItem() initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0)) runCurrent() @@ -211,9 +203,7 @@ class TimelinePresenterTest { this.sendReadReceiptLambda = sendReadReceiptsLambda } val presenter = createTimelinePresenter(timeline) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { skipItems(1) awaitItem().run { eventSink.invoke(TimelineEvents.OnScrollFinished(1)) @@ -252,9 +242,7 @@ class TimelinePresenterTest { timeline = timeline, sessionPreferencesStore = sessionPreferencesStore, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { skipItems(1) awaitItem().run { eventSink.invoke(TimelineEvents.OnScrollFinished(0)) @@ -290,9 +278,7 @@ class TimelinePresenterTest { this.sendReadReceiptLambda = sendReadReceiptsLambda } val presenter = createTimelinePresenter(timeline) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { skipItems(1) awaitItem().run { eventSink.invoke(TimelineEvents.OnScrollFinished(1)) @@ -320,9 +306,7 @@ class TimelinePresenterTest { this.sendReadReceiptLambda = sendReadReceiptsLambda } val presenter = createTimelinePresenter(timeline) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { skipItems(1) val initialState = awaitFirstItem() initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1)) @@ -339,9 +323,7 @@ class TimelinePresenterTest { markAsReadResult = { Result.success(Unit) }, ) val presenter = createTimelinePresenter(timeline) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitFirstItem() assertThat(initialState.newEventState).isEqualTo(NewEventState.None) assertThat(initialState.timelineItems.size).isEqualTo(0) @@ -390,9 +372,7 @@ class TimelinePresenterTest { timelineItems = timelineItems, ) val presenter = createTimelinePresenter(timeline) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitFirstItem() assertThat(initialState.newEventState).isEqualTo(NewEventState.None) assertThat(initialState.timelineItems.size).isEqualTo(0) @@ -446,9 +426,7 @@ class TimelinePresenterTest { val presenter = createTimelinePresenter( sendPollResponseAction = sendPollResponseAction, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitFirstItem() initialState.eventSink.invoke(TimelineEvents.SelectPollAnswer(AN_EVENT_ID, "anAnswerId")) } @@ -462,9 +440,7 @@ class TimelinePresenterTest { val presenter = createTimelinePresenter( endPollAction = endPollAction, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitFirstItem() initialState.eventSink.invoke(TimelineEvents.EndPoll(AN_EVENT_ID)) } @@ -481,9 +457,7 @@ class TimelinePresenterTest { val presenter = createTimelinePresenter( messagesNavigator = navigator, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { awaitFirstItem().eventSink(TimelineEvents.EditPoll(AN_EVENT_ID)) onEditPollClickLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID)) } @@ -500,9 +474,7 @@ class TimelinePresenterTest { ), redactedVoiceMessageManager = redactedVoiceMessageManager, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { assertThat(redactedVoiceMessageManager.invocations.size).isEqualTo(0) skipItems(2) assertThat(redactedVoiceMessageManager.invocations.size).isEqualTo(1) @@ -528,16 +500,14 @@ class TimelinePresenterTest { liveTimeline = liveTimeline, createTimelineResult = { Result.success(detachedTimeline) }, baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, + roomPermissions = roomPermissions(), threadRootIdForEventResult = { _ -> Result.success(null) }, ), ) val presenter = createTimelinePresenter( room = room, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitFirstItem() initialState.eventSink.invoke(TimelineEvents.FocusOnEvent(AN_EVENT_ID)) awaitItem().also { state -> @@ -579,15 +549,13 @@ class TimelinePresenterTest { ) ), baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, + roomPermissions = roomPermissions(), threadRootIdForEventResult = { Result.success(null) }, ), ), timelineItemIndexer = timelineItemIndexer, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitFirstItem() advanceUntilIdle() @@ -619,14 +587,12 @@ class TimelinePresenterTest { ), createTimelineResult = { Result.failure(RuntimeException("An error")) }, baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, + roomPermissions = roomPermissions(), threadRootIdForEventResult = { _ -> Result.success(null) }, ), ) ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitFirstItem() initialState.eventSink(TimelineEvents.FocusOnEvent(AN_EVENT_ID)) awaitItem().also { state -> @@ -668,7 +634,7 @@ class TimelinePresenterTest { liveTimeline = liveTimeline, createTimelineResult = { Result.success(detachedTimeline) }, baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, + roomPermissions = roomPermissions(), threadRootIdForEventResult = { _ -> Result.success(threadId) }, ), ) @@ -679,9 +645,7 @@ class TimelinePresenterTest { timeline = liveTimeline, messagesNavigator = navigator, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitFirstItem() initialState.eventSink.invoke(TimelineEvents.FocusOnEvent(AN_EVENT_ID)) @@ -729,7 +693,7 @@ class TimelinePresenterTest { liveTimeline = liveTimeline, createTimelineResult = { Result.success(detachedTimeline) }, baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, + roomPermissions = roomPermissions(), threadRootIdForEventResult = { _ -> Result.success(threadId) }, ), ) @@ -740,9 +704,7 @@ class TimelinePresenterTest { timeline = liveTimeline, messagesNavigator = navigator, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitFirstItem() initialState.eventSink.invoke(TimelineEvents.FocusOnEvent(AN_EVENT_ID)) @@ -785,7 +747,7 @@ class TimelinePresenterTest { liveTimeline = liveTimeline, createTimelineResult = { Result.success(detachedTimeline) }, baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, + roomPermissions = roomPermissions(), // Use a different thread id threadRootIdForEventResult = { _ -> Result.success(A_THREAD_ID_2) }, ), @@ -797,9 +759,7 @@ class TimelinePresenterTest { timeline = liveTimeline, messagesNavigator = navigator, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitFirstItem() initialState.eventSink.invoke(TimelineEvents.FocusOnEvent(AN_EVENT_ID)) @@ -846,7 +806,7 @@ class TimelinePresenterTest { liveTimeline = liveTimeline, createTimelineResult = { Result.success(detachedTimeline) }, baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, + roomPermissions = roomPermissions(), // The event is in the main timeline, not in a thread threadRootIdForEventResult = { _ -> Result.success(null) }, ), @@ -858,9 +818,7 @@ class TimelinePresenterTest { timeline = liveTimeline, messagesNavigator = navigator, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitFirstItem() initialState.eventSink.invoke(TimelineEvents.FocusOnEvent(AN_EVENT_ID)) @@ -891,9 +849,7 @@ class TimelinePresenterTest { fun `present - show shield hide shield`() = runTest { val presenter = createTimelinePresenter() val shield = aCriticalShield() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitFirstItem() assertThat(initialState.messageShield).isNull() initialState.eventSink(TimelineEvents.ShowShieldDialog(shield)) @@ -929,7 +885,9 @@ class TimelinePresenterTest { ) val room = FakeJoinedRoom( liveTimeline = timeline, - baseRoom = FakeBaseRoom(canUserSendMessageResult = { _, _ -> Result.success(true) }), + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + ), ).apply { givenRoomMembersState(RoomMembersState.Unknown) } @@ -937,9 +895,7 @@ class TimelinePresenterTest { val avatarUrl = "https://domain.com/avatar.jpg" val presenter = createTimelinePresenter(timeline, room) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = consumeItemsUntilPredicate(30.seconds) { it.timelineItems.isNotEmpty() }.last() val event = initialState.timelineItems.first() as TimelineItem.Event assertThat(event.senderAvatar.url).isNull() @@ -963,15 +919,13 @@ class TimelinePresenterTest { val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, + roomPermissions = roomPermissions(), predecessorRoomResult = { predecessorRoom } ), ) val presenter = createTimelinePresenter(room = room) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitFirstItem() assertThat(initialState.timelineRoomInfo.predecessorRoom).isNotNull() assertThat(initialState.timelineRoomInfo.predecessorRoom?.roomId).isEqualTo(predecessorRoomId) @@ -982,14 +936,12 @@ class TimelinePresenterTest { fun `present - timeline room info no predecessor`() = runTest { val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, + roomPermissions = roomPermissions(), predecessorRoomResult = { null } ), ) val presenter = createTimelinePresenter(room = room) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitFirstItem() assertThat(initialState.timelineRoomInfo.predecessorRoom).isNull() } @@ -999,7 +951,7 @@ class TimelinePresenterTest { fun `present - timeline event navigate to room`() = runTest { val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, + roomPermissions = roomPermissions(), ), ) val onNavigateToRoomLambda = lambdaRecorder, Unit> { _, _, _ -> } @@ -1025,11 +977,32 @@ class TimelinePresenterTest { return awaitItem() } + private fun roomPermissions( + canRedactOther: Boolean = false, + canRedactOwn: Boolean = true, + canSendMessage: Boolean = true, + canSendReaction: Boolean = true, + canPinUnpin: Boolean = false, + ) = FakeRoomPermissions( + canSendMessage = { type -> + when (type) { + MessageEventType.RoomMessage -> canSendMessage + MessageEventType.Reaction -> canSendReaction + else -> lambdaError() + } + }, + canRedactOther = canRedactOther, + canRedactOwn = canRedactOwn, + canPinUnpin = canPinUnpin, + ) + private fun TestScope.createTimelinePresenter( timeline: Timeline = FakeTimeline(), room: FakeJoinedRoom = FakeJoinedRoom( liveTimeline = timeline, - baseRoom = FakeBaseRoom(canUserSendMessageResult = { _, _ -> Result.success(true) }), + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + ), ), redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(), messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(), diff --git a/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt b/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt index 1aceee227a7..bb0dc04c188 100644 --- a/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt +++ b/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt @@ -15,9 +15,12 @@ import io.element.android.features.call.test.FakeCurrentCallService import io.element.android.features.enterprise.test.FakeSessionEnterpriseService import io.element.android.features.roomcall.api.RoomCallState import io.element.android.libraries.matrix.api.room.JoinedRoom +import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions +import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.test import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest @@ -28,7 +31,7 @@ class RoomCallStatePresenterTest { fun `present - initial state`() = runTest { val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canUserJoinCallResult = { Result.success(false) }, + roomPermissions = roomPermissions(false), ) ) val presenter = createRoomCallStatePresenter(joinedRoom = room) @@ -47,7 +50,7 @@ class RoomCallStatePresenterTest { fun `present - element call not available`() = runTest { val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canUserJoinCallResult = { Result.success(false) }, + roomPermissions = roomPermissions(false), ) ) val presenter = createRoomCallStatePresenter( @@ -66,7 +69,7 @@ class RoomCallStatePresenterTest { fun `present - initial state - user can join call`() = runTest { val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canUserJoinCallResult = { Result.success(true) }, + roomPermissions = roomPermissions(true), ) ) val presenter = createRoomCallStatePresenter(joinedRoom = room) @@ -85,7 +88,7 @@ class RoomCallStatePresenterTest { fun `present - call is disabled if user cannot join it even if there is an ongoing call`() = runTest { val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canUserJoinCallResult = { Result.success(false) }, + roomPermissions = roomPermissions(false), initialRoomInfo = aRoomInfo(hasRoomCall = true), ) ) @@ -106,7 +109,7 @@ class RoomCallStatePresenterTest { fun `present - user has joined the call on another session`() = runTest { val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canUserJoinCallResult = { Result.success(true) }, + roomPermissions = roomPermissions(true), ).apply { givenRoomInfo( aRoomInfo( @@ -133,7 +136,7 @@ class RoomCallStatePresenterTest { fun `present - user has joined the call locally`() = runTest { val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canUserJoinCallResult = { Result.success(true) }, + roomPermissions = roomPermissions(true), ).apply { givenRoomInfo( aRoomInfo( @@ -163,7 +166,7 @@ class RoomCallStatePresenterTest { fun `present - user leaves the call`() = runTest { val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canUserJoinCallResult = { Result.success(true) }, + roomPermissions = roomPermissions(true), ).apply { givenRoomInfo( aRoomInfo( @@ -223,6 +226,17 @@ class RoomCallStatePresenterTest { } } + private fun roomPermissions(canJoinCall: Boolean): FakeRoomPermissions { + return FakeRoomPermissions( + canSendState = { stateEvent -> + when (stateEvent) { + StateEventType.CALL_MEMBER -> canJoinCall + else -> lambdaError() + } + } + ) + } + private fun createRoomCallStatePresenter( joinedRoom: JoinedRoom, currentCallService: CurrentCallService = FakeCurrentCallService(), diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/MatrixRoomFixture.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/MatrixRoomFixture.kt index 5043aea88c9..2d857443459 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/MatrixRoomFixture.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/MatrixRoomFixture.kt @@ -13,8 +13,8 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.RoomMember -import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.join.JoinRule +import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.A_ROOM_ALIAS import io.element.android.libraries.matrix.test.A_ROOM_ID @@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.test.notificationsettings.FakeNotific import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions import io.element.android.tests.testutils.lambda.lambdaError fun aRoom( @@ -35,6 +36,7 @@ fun aRoom( topic: String? = A_ROOM_TOPIC, avatarUrl: String? = AN_AVATAR_URL, canonicalAlias: RoomAlias? = A_ROOM_ALIAS, + roomPermissions: RoomPermissions = FakeRoomPermissions(), isEncrypted: Boolean = true, isPublic: Boolean = true, isDirect: Boolean = false, @@ -42,29 +44,20 @@ fun aRoom( activeMemberCount: Long = 1, joinedMemberCount: Long = 1, invitedMemberCount: Long = 0, - canInviteResult: (UserId) -> Result = { lambdaError() }, - canBanResult: (UserId) -> Result = { lambdaError() }, - canKickResult: (UserId) -> Result = { lambdaError() }, - canSendStateResult: (UserId, StateEventType) -> Result = { _, _ -> lambdaError() }, userDisplayNameResult: (UserId) -> Result = { lambdaError() }, userAvatarUrlResult: () -> Result = { lambdaError() }, - canUserJoinCallResult: (UserId) -> Result = { lambdaError() }, getUpdatedMemberResult: (UserId) -> Result = { lambdaError() }, userRoleResult: () -> Result = { lambdaError() }, setIsFavoriteResult: (Boolean) -> Result = { lambdaError() }, ) = FakeBaseRoom( sessionId = sessionId, roomId = roomId, - canInviteResult = canInviteResult, - canBanResult = canBanResult, - canKickResult = canKickResult, - canSendStateResult = canSendStateResult, userDisplayNameResult = userDisplayNameResult, userAvatarUrlResult = userAvatarUrlResult, - canUserJoinCallResult = canUserJoinCallResult, getUpdatedMemberResult = getUpdatedMemberResult, userRoleResult = userRoleResult, setIsFavoriteResult = setIsFavoriteResult, + roomPermissions = roomPermissions, initialRoomInfo = aRoomInfo( name = displayName, rawName = rawName, @@ -89,6 +82,7 @@ fun aJoinedRoom( topic: String? = A_ROOM_TOPIC, avatarUrl: String? = AN_AVATAR_URL, canonicalAlias: RoomAlias? = A_ROOM_ALIAS, + roomPermissions: RoomPermissions = FakeRoomPermissions(), isEncrypted: Boolean = true, isPublic: Boolean = true, isDirect: Boolean = false, @@ -97,17 +91,12 @@ fun aJoinedRoom( joinedMemberCount: Long = 1, invitedMemberCount: Long = 0, notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), - canInviteResult: (UserId) -> Result = { lambdaError() }, - canBanResult: (UserId) -> Result = { lambdaError() }, - canKickResult: (UserId) -> Result = { lambdaError() }, - canSendStateResult: (UserId, StateEventType) -> Result = { _, _ -> lambdaError() }, userDisplayNameResult: (UserId) -> Result = { lambdaError() }, userAvatarUrlResult: () -> Result = { lambdaError() }, setNameResult: (String) -> Result = { lambdaError() }, setTopicResult: (String) -> Result = { lambdaError() }, updateAvatarResult: (String, ByteArray) -> Result = { _, _ -> lambdaError() }, removeAvatarResult: () -> Result = { lambdaError() }, - canUserJoinCallResult: (UserId) -> Result = { lambdaError() }, getUpdatedMemberResult: (UserId) -> Result = { lambdaError() }, userRoleResult: () -> Result = { lambdaError() }, kickUserResult: (UserId, String?) -> Result = { _, _ -> lambdaError() }, @@ -132,13 +121,9 @@ fun aJoinedRoom( baseRoom = aRoom( sessionId = sessionId, roomId = roomId, - canInviteResult = canInviteResult, - canBanResult = canBanResult, - canKickResult = canKickResult, - canSendStateResult = canSendStateResult, + roomPermissions = roomPermissions, userDisplayNameResult = userDisplayNameResult, userAvatarUrlResult = userAvatarUrlResult, - canUserJoinCallResult = canUserJoinCallResult, getUpdatedMemberResult = getUpdatedMemberResult, userRoleResult = userRoleResult, setIsFavoriteResult = setIsFavoriteResult, diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt index 9cf46c41c07..7d6219004e9 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt @@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.room.RoomMembersState import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.join.JoinRule +import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_NAME @@ -41,6 +42,7 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.services.analytics.api.AnalyticsService @@ -119,9 +121,7 @@ class RoomDetailsPresenterTest { @Test fun `present - initial state is created from initial room info`() = runTest { val room = aJoinedRoom( - canInviteResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canSendStateResult = { _, _ -> Result.success(true) }, + roomPermissions = roomPermissions(), ) val presenter = createRoomDetailsPresenter(room) presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { @@ -148,9 +148,7 @@ class RoomDetailsPresenterTest { pinnedEventIds = listOf(AN_EVENT_ID), ) val room = aJoinedRoom( - canInviteResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canSendStateResult = { _, _ -> Result.success(true) }, + roomPermissions = roomPermissions(), ).apply { givenRoomInfo(roomInfo) } @@ -170,9 +168,7 @@ class RoomDetailsPresenterTest { fun `present - initial state with no room name`() = runTest { val room = aJoinedRoom( displayName = "", - canInviteResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canSendStateResult = { _, _ -> Result.success(true) }, + roomPermissions = roomPermissions(), ) val presenter = createRoomDetailsPresenter(room) presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { @@ -188,9 +184,7 @@ class RoomDetailsPresenterTest { val myRoomMember = aRoomMember(A_SESSION_ID) val otherRoomMember = aRoomMember(A_USER_ID_2) val room = aJoinedRoom( - canInviteResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canSendStateResult = { _, _ -> Result.success(true) }, + roomPermissions = roomPermissions(), getUpdatedMemberResult = { userId -> when (userId) { A_SESSION_ID -> Result.success(myRoomMember) @@ -225,9 +219,9 @@ class RoomDetailsPresenterTest { @Test fun `present - initial state when user can invite others to room`() = runTest { val room = aJoinedRoom( - canInviteResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canSendStateResult = { _, _ -> Result.success(true) }, + roomPermissions = roomPermissions( + canInvite = true, + ), ) val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { @@ -243,26 +237,9 @@ class RoomDetailsPresenterTest { @Test fun `present - initial state when user can not invite others to room`() = runTest { val room = aJoinedRoom( - canInviteResult = { Result.success(false) }, - canKickResult = { Result.success(false) }, - canBanResult = { Result.success(false) }, - canUserJoinCallResult = { Result.success(true) }, - canSendStateResult = { _, _ -> Result.success(true) }, - ) - val presenter = createRoomDetailsPresenter(room) - presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { - assertThat(awaitItem().canInvite).isFalse() - - cancelAndIgnoreRemainingEvents() - } - } - - @Test - fun `present - initial state when canInvite errors`() = runTest { - val room = aJoinedRoom( - canInviteResult = { Result.failure(RuntimeException("Whoops")) }, - canUserJoinCallResult = { Result.success(true) }, - canSendStateResult = { _, _ -> Result.success(true) }, + roomPermissions = roomPermissions( + canInvite = false, + ), ) val presenter = createRoomDetailsPresenter(room) presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { @@ -275,17 +252,11 @@ class RoomDetailsPresenterTest { @Test fun `present - initial state when user can edit one attribute`() = runTest { val room = aJoinedRoom( - canSendStateResult = { _, stateEventType -> - when (stateEventType) { - StateEventType.ROOM_TOPIC -> Result.success(true) - StateEventType.ROOM_NAME -> Result.success(false) - else -> Result.failure(RuntimeException("Whelp")) - } - }, - canBanResult = { Result.success(false) }, - canKickResult = { Result.success(false) }, - canInviteResult = { Result.success(false) }, - canUserJoinCallResult = { Result.success(true) }, + roomPermissions = roomPermissions( + canChangeName = true, + canChangeTopic = false, + canChangeAvatar = false, + ), ) val presenter = createRoomDetailsPresenter(room) presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { @@ -303,18 +274,7 @@ class RoomDetailsPresenterTest { val myRoomMember = aRoomMember(A_SESSION_ID) val otherRoomMember = aRoomMember(A_USER_ID_2) val room = aJoinedRoom( - canSendStateResult = { _, stateEventType -> - when (stateEventType) { - StateEventType.ROOM_TOPIC, - StateEventType.ROOM_NAME, - StateEventType.ROOM_AVATAR -> Result.success(true) - else -> Result.failure(RuntimeException("Whelp")) - } - }, - canKickResult = { Result.success(false) }, - canBanResult = { Result.success(false) }, - canInviteResult = { Result.success(false) }, - canUserJoinCallResult = { Result.success(true) }, + roomPermissions = roomPermissions(), getUpdatedMemberResult = { userId -> when (userId) { A_SESSION_ID -> Result.success(myRoomMember) @@ -354,18 +314,9 @@ class RoomDetailsPresenterTest { val room = aJoinedRoom( isDirect = true, topic = null, - canSendStateResult = { _, stateEventType -> - when (stateEventType) { - StateEventType.ROOM_AVATAR, - StateEventType.ROOM_TOPIC, - StateEventType.ROOM_NAME -> Result.success(true) - else -> Result.failure(RuntimeException("Whelp")) - } - }, + roomPermissions = roomPermissions(), userDisplayNameResult = { Result.success(A_USER_NAME) }, userAvatarUrlResult = { Result.success(AN_AVATAR_URL) }, - canInviteResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, getUpdatedMemberResult = { userId -> when (userId) { A_SESSION_ID -> Result.success(myRoomMember) @@ -400,24 +351,11 @@ class RoomDetailsPresenterTest { @Test fun `present - initial state when user can edit all attributes`() = runTest { val room = aJoinedRoom( - canSendStateResult = { _, stateEventType -> - when (stateEventType) { - StateEventType.ROOM_TOPIC, - StateEventType.ROOM_NAME, - StateEventType.ROOM_AVATAR -> Result.success(true) - else -> Result.failure(RuntimeException("Whelp")) - } - }, - canKickResult = { - Result.success(false) - }, - canBanResult = { - Result.success(false) - }, - canInviteResult = { - Result.success(false) - }, - canUserJoinCallResult = { Result.success(true) }, + roomPermissions = roomPermissions( + canChangeAvatar = true, + canChangeName = true, + canChangeTopic = true, + ), ) val presenter = createRoomDetailsPresenter(room) presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { @@ -433,24 +371,11 @@ class RoomDetailsPresenterTest { @Test fun `present - initial state when user can edit no attributes`() = runTest { val room = aJoinedRoom( - canSendStateResult = { _, stateEventType -> - when (stateEventType) { - StateEventType.ROOM_TOPIC, - StateEventType.ROOM_NAME, - StateEventType.ROOM_AVATAR -> Result.success(false) - else -> Result.failure(RuntimeException("Whelp")) - } - }, - canBanResult = { - Result.success(false) - }, - canKickResult = { - Result.success(false) - }, - canInviteResult = { - Result.success(false) - }, - canUserJoinCallResult = { Result.success(true) }, + roomPermissions = roomPermissions( + canChangeAvatar = false, + canChangeName = false, + canChangeTopic = false, + ), ) val presenter = createRoomDetailsPresenter(room) presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { @@ -465,24 +390,9 @@ class RoomDetailsPresenterTest { fun `present - topic state is hidden when no topic and user has no permission`() = runTest { val room = aJoinedRoom( topic = null, - canSendStateResult = { _, stateEventType -> - when (stateEventType) { - StateEventType.ROOM_AVATAR, - StateEventType.ROOM_NAME -> Result.success(true) - StateEventType.ROOM_TOPIC -> Result.success(false) - else -> Result.failure(RuntimeException("Whelp")) - } - }, - canKickResult = { - Result.success(false) - }, - canBanResult = { - Result.success(false) - }, - canInviteResult = { - Result.success(false) - }, - canUserJoinCallResult = { Result.success(true) }, + roomPermissions = roomPermissions( + canChangeTopic = false + ), ) val presenter = createRoomDetailsPresenter(room) presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { @@ -497,24 +407,7 @@ class RoomDetailsPresenterTest { fun `present - topic state is 'can add topic' when no topic and user has permission`() = runTest { val room = aJoinedRoom( topic = null, - canSendStateResult = { _, stateEventType -> - when (stateEventType) { - StateEventType.ROOM_AVATAR, - StateEventType.ROOM_TOPIC, - StateEventType.ROOM_NAME -> Result.success(true) - else -> Result.failure(RuntimeException("Whelp")) - } - }, - canKickResult = { - Result.success(false) - }, - canBanResult = { - Result.success(false) - }, - canInviteResult = { - Result.success(false) - }, - canUserJoinCallResult = { Result.success(true) }, + roomPermissions = roomPermissions(), ).apply { givenRoomInfo(aRoomInfo(topic = null)) } @@ -534,9 +427,7 @@ class RoomDetailsPresenterTest { fun `present - leave room event is passed on to leave room presenter`() = runTest { val leaveRoomEventRecorder = EventsRecorder() val room = aJoinedRoom( - canInviteResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canSendStateResult = { _, _ -> Result.success(true) }, + roomPermissions = roomPermissions(), ) val presenter = createRoomDetailsPresenter( room = room, @@ -555,9 +446,7 @@ class RoomDetailsPresenterTest { val notificationSettingsService = FakeNotificationSettingsService() val room = aJoinedRoom( notificationSettingsService = notificationSettingsService, - canInviteResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canSendStateResult = { _, _ -> Result.success(true) }, + roomPermissions = roomPermissions(), ) val presenter = createRoomDetailsPresenter( room = room, @@ -584,9 +473,7 @@ class RoomDetailsPresenterTest { FakeNotificationSettingsService(initialRoomMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) val room = aJoinedRoom( notificationSettingsService = notificationSettingsService, - canInviteResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canSendStateResult = { _, _ -> Result.success(true) }, + roomPermissions = roomPermissions(), ) val presenter = createRoomDetailsPresenter( room = room, @@ -612,9 +499,7 @@ class RoomDetailsPresenterTest { ) val room = aJoinedRoom( notificationSettingsService = notificationSettingsService, - canInviteResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canSendStateResult = { _, _ -> Result.success(true) }, + roomPermissions = roomPermissions(), ) val presenter = createRoomDetailsPresenter( room = room, @@ -637,9 +522,7 @@ class RoomDetailsPresenterTest { val setIsFavoriteResult = lambdaRecorder> { _ -> Result.success(Unit) } val room = aJoinedRoom( setIsFavoriteResult = setIsFavoriteResult, - canInviteResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canSendStateResult = { _, _ -> Result.success(true) }, + roomPermissions = roomPermissions(), ) val analyticsService = FakeAnalyticsService() val presenter = @@ -665,9 +548,7 @@ class RoomDetailsPresenterTest { @Test fun `present - changes in room info updates the is favorite flag`() = runTest { val room = aJoinedRoom( - canInviteResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canSendStateResult = { _, _ -> Result.success(true) }, + roomPermissions = roomPermissions(), ) val presenter = createRoomDetailsPresenter(room = room) presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { @@ -686,9 +567,7 @@ class RoomDetailsPresenterTest { @Test fun `present - show knock requests`() = runTest { val room = aJoinedRoom( - canInviteResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canSendStateResult = { _, _ -> Result.success(true) }, + roomPermissions = roomPermissions(), joinRule = JoinRule.Knock, ) val featureFlagService = FakeFeatureFlagService( @@ -712,9 +591,7 @@ class RoomDetailsPresenterTest { @Test fun `present - show security and privacy`() = runTest { val room = aJoinedRoom( - canInviteResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canSendStateResult = { _, _ -> Result.success(true) }, + roomPermissions = roomPermissions(), ) val featureFlagService = FakeFeatureFlagService() val presenter = createRoomDetailsPresenter(room = room, featureFlagService = featureFlagService) @@ -729,9 +606,7 @@ class RoomDetailsPresenterTest { @Test fun `present - show debug info`() = runTest { val room = aJoinedRoom( - canInviteResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canSendStateResult = { _, _ -> Result.success(true) }, + roomPermissions = roomPermissions(), ) val inMemoryAppPreferencesStore = InMemoryAppPreferencesStore( isDeveloperModeEnabled = true, @@ -744,4 +619,41 @@ class RoomDetailsPresenterTest { } } } + + private fun roomPermissions( + canInvite: Boolean = true, + canKick: Boolean = true, + canBan: Boolean = true, + canRedactOther: Boolean = true, + canRedactOwn: Boolean = true, + canChangeRoomAccess: Boolean = true, + canChangeHistoryVisibility: Boolean = true, + canChangeEncryption: Boolean = true, + canChangeRoomVisibility: Boolean = true, + canChangeName: Boolean = true, + canChangeTopic: Boolean = true, + canChangeAvatar: Boolean = true, + canChangePowerLevels: Boolean = true, + ): RoomPermissions { + return FakeRoomPermissions( + canInvite = canInvite, + canKick = canKick, + canBan = canBan, + canRedactOther = canRedactOther, + canRedactOwn = canRedactOwn, + canSendState = { eventType -> + when (eventType) { + StateEventType.ROOM_JOIN_RULES -> canChangeRoomAccess + StateEventType.ROOM_HISTORY_VISIBILITY -> canChangeHistoryVisibility + StateEventType.ROOM_ENCRYPTION -> canChangeEncryption + StateEventType.ROOM_CANONICAL_ALIAS -> canChangeRoomVisibility + StateEventType.ROOM_AVATAR -> canChangeAvatar + StateEventType.ROOM_NAME -> canChangeName + StateEventType.ROOM_TOPIC -> canChangeTopic + StateEventType.ROOM_POWER_LEVELS -> canChangePowerLevels + else -> lambdaError() + } + } + ) + } } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenterTest.kt index 8d3d0e35e35..f1cab1524a3 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenterTest.kt @@ -12,7 +12,6 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers -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.RoomMembersState import io.element.android.libraries.matrix.api.room.RoomMembershipState @@ -20,6 +19,7 @@ import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.test import io.element.android.tests.testutils.testCoroutineDispatchers @@ -171,20 +171,7 @@ class RoomMemberListPresenterTest { fun `present - asynchronously sets canInvite when user does not have correct power level`() = runTest { val presenter = createPresenter( joinedRoom = createFakeJoinedRoom( - canInviteResult = { Result.success(false) }, - ) - ) - presenter.test { - val loadedState = awaitItem() - assertThat(loadedState.canInvite).isFalse() - } - } - - @Test - fun `present - asynchronously sets canInvite when power level check fails`() = runTest { - val presenter = createPresenter( - joinedRoom = createFakeJoinedRoom( - canInviteResult = { Result.failure(RuntimeException("Eek")) }, + canInvite = false, ) ) presenter.test { @@ -207,12 +194,14 @@ class RoomMemberListPresenterTest { private fun createFakeJoinedRoom( updateMembersResult: () -> Unit = { }, - canInviteResult: (UserId) -> Result = { Result.success(true) }, + canInvite: Boolean = true, ): FakeJoinedRoom { return FakeJoinedRoom( baseRoom = FakeBaseRoom( updateMembersResult = updateMembersResult, - canInviteResult = canInviteResult, + roomPermissions = FakeRoomPermissions( + canInvite = canInvite, + ), ).apply { // Needed to avoid discarding the loaded members as a partial and invalid result givenRoomInfo(aRoomInfo(joinedMembersCount = 2)) diff --git a/features/roomdetailsedit/api/src/main/kotlin/io/element/android/features/roomdetailsedit/api/RoomDetailsEditPermissions.kt b/features/roomdetailsedit/api/src/main/kotlin/io/element/android/features/roomdetailsedit/api/RoomDetailsEditPermissions.kt index f95f4466f2e..c400ec12207 100644 --- a/features/roomdetailsedit/api/src/main/kotlin/io/element/android/features/roomdetailsedit/api/RoomDetailsEditPermissions.kt +++ b/features/roomdetailsedit/api/src/main/kotlin/io/element/android/features/roomdetailsedit/api/RoomDetailsEditPermissions.kt @@ -14,7 +14,7 @@ data class RoomDetailsEditPermissions( val canEditName: Boolean, val canEditTopic: Boolean, val canEditAvatar: Boolean, -){ +) { val hasAny = canEditName || canEditTopic || canEditAvatar diff --git a/features/roomdetailsedit/impl/src/main/kotlin/io/element/android/features/roomdetailsedit/impl/RoomDetailsEditPresenter.kt b/features/roomdetailsedit/impl/src/main/kotlin/io/element/android/features/roomdetailsedit/impl/RoomDetailsEditPresenter.kt index ed729928d87..24f8f49e816 100644 --- a/features/roomdetailsedit/impl/src/main/kotlin/io/element/android/features/roomdetailsedit/impl/RoomDetailsEditPresenter.kt +++ b/features/roomdetailsedit/impl/src/main/kotlin/io/element/android/features/roomdetailsedit/impl/RoomDetailsEditPresenter.kt @@ -59,7 +59,6 @@ class RoomDetailsEditPresenter( @Composable override fun present(): RoomDetailsEditState { val cameraPermissionState = cameraPermissionPresenter.present() - val roomSyncUpdateFlow = room.syncUpdateFlow.collectAsState() val roomInfo by room.roomInfoFlow.collectAsState() val roomAvatarUri = roomInfo.avatarUrl var roomAvatarUriEdited by rememberSaveable { mutableStateOf(null) } @@ -94,7 +93,7 @@ class RoomDetailsEditPresenter( } } - val permissions by room.permissionsAsState(RoomDetailsEditPermissions.DEFAULT){perms -> + val permissions by room.permissionsAsState(RoomDetailsEditPermissions.DEFAULT) { perms -> perms.roomDetailsEditPermissions() } diff --git a/features/roomdetailsedit/impl/src/test/kotlin/io/element/android/features/roomdetailsedit/impl/RoomDetailsEditPresenterTest.kt b/features/roomdetailsedit/impl/src/test/kotlin/io/element/android/features/roomdetailsedit/impl/RoomDetailsEditPresenterTest.kt index e438cecf503..9c7f840af25 100644 --- a/features/roomdetailsedit/impl/src/test/kotlin/io/element/android/features/roomdetailsedit/impl/RoomDetailsEditPresenterTest.kt +++ b/features/roomdetailsedit/impl/src/test/kotlin/io/element/android/features/roomdetailsedit/impl/RoomDetailsEditPresenterTest.kt @@ -13,7 +13,6 @@ import com.google.common.truth.Truth.assertThat import io.element.android.libraries.androidutils.file.TemporaryUriDeleter import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.core.mimetype.MimeTypes -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.StateEventType import io.element.android.libraries.matrix.test.AN_AVATAR_URL @@ -23,6 +22,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_TOPIC import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediaupload.api.MediaUploadInfo @@ -102,7 +102,6 @@ class RoomDetailsEditPresenterTest { avatarUrl = AN_AVATAR_URL, displayName = A_ROOM_NAME, rawName = A_ROOM_RAW_NAME, - canSendStateResult = { _, _ -> Result.success(true) } ) val deleteCallback = lambdaRecorder {} val presenter = createRoomDetailsEditPresenter( @@ -127,17 +126,15 @@ class RoomDetailsEditPresenterTest { @Test fun `present - sets canChangeName if user has permission`() = runTest { - val room = FakeJoinedRoom( - FakeBaseRoom( - canSendStateResult = { _, stateEventType -> - when (stateEventType) { - StateEventType.ROOM_NAME -> Result.success(true) - StateEventType.ROOM_AVATAR -> Result.success(false) - StateEventType.ROOM_TOPIC -> Result.failure(RuntimeException("Oops")) - else -> lambdaError() - } - }, - ) + val room = aJoinedRoom( + canSendState = { stateEventType -> + when (stateEventType) { + StateEventType.ROOM_NAME -> true + StateEventType.ROOM_AVATAR -> false + StateEventType.ROOM_TOPIC -> false + else -> lambdaError() + } + } ) val deleteCallback = lambdaRecorder {} val presenter = createRoomDetailsEditPresenter( @@ -163,11 +160,11 @@ class RoomDetailsEditPresenterTest { fun `present - sets canChangeAvatar if user has permission`() = runTest { val room = aJoinedRoom( avatarUrl = AN_AVATAR_URL, - canSendStateResult = { _, stateEventType -> + canSendState = { stateEventType -> when (stateEventType) { - StateEventType.ROOM_NAME -> Result.success(false) - StateEventType.ROOM_AVATAR -> Result.success(true) - StateEventType.ROOM_TOPIC -> Result.failure(RuntimeException("Oops")) + StateEventType.ROOM_NAME -> false + StateEventType.ROOM_AVATAR -> true + StateEventType.ROOM_TOPIC -> false else -> lambdaError() } } @@ -195,11 +192,11 @@ class RoomDetailsEditPresenterTest { fun `present - sets canChangeTopic if user has permission`() = runTest { val room = aJoinedRoom( avatarUrl = AN_AVATAR_URL, - canSendStateResult = { _, stateEventType -> + canSendState = { stateEventType -> when (stateEventType) { - StateEventType.ROOM_NAME -> Result.success(false) - StateEventType.ROOM_AVATAR -> Result.failure(RuntimeException("Oops")) - StateEventType.ROOM_TOPIC -> Result.success(true) + StateEventType.ROOM_NAME -> false + StateEventType.ROOM_AVATAR -> false + StateEventType.ROOM_TOPIC -> true else -> lambdaError() } } @@ -229,7 +226,6 @@ class RoomDetailsEditPresenterTest { topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL, - canSendStateResult = { _, _ -> Result.success(true) } ) val deleteCallback = lambdaRecorder {} val presenter = createRoomDetailsEditPresenter( @@ -274,7 +270,6 @@ class RoomDetailsEditPresenterTest { topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL, - canSendStateResult = { _, _ -> Result.success(true) } ) fakePickerProvider.givenResult(anotherAvatarUri) val deleteCallback = lambdaRecorder {} @@ -298,7 +293,6 @@ class RoomDetailsEditPresenterTest { topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL, - canSendStateResult = { _, _ -> Result.success(true) } ) fakePickerProvider.givenResult(anotherAvatarUri) val fakePermissionsPresenter = FakePermissionsPresenter() @@ -339,7 +333,6 @@ class RoomDetailsEditPresenterTest { topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL, - canSendStateResult = { _, _ -> Result.success(true) } ) fakePickerProvider.givenResult(roomAvatarUri) val deleteCallback = lambdaRecorder {} @@ -389,7 +382,6 @@ class RoomDetailsEditPresenterTest { topic = null, displayName = "fallback", avatarUrl = null, - canSendStateResult = { _, _ -> Result.success(true) } ) fakePickerProvider.givenResult(roomAvatarUri) val deleteCallback = lambdaRecorder {} @@ -445,7 +437,6 @@ class RoomDetailsEditPresenterTest { setNameResult = setNameResult, setTopicResult = setTopicResult, removeAvatarResult = removeAvatarResult, - canSendStateResult = { _, _ -> Result.success(true) } ) val deleteCallback = lambdaRecorder {} val presenter = createRoomDetailsEditPresenter( @@ -471,7 +462,6 @@ class RoomDetailsEditPresenterTest { topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL, - canSendStateResult = { _, _ -> Result.success(true) } ) val deleteCallback = lambdaRecorder {} val presenter = createRoomDetailsEditPresenter( @@ -493,7 +483,6 @@ class RoomDetailsEditPresenterTest { topic = null, displayName = "Name", avatarUrl = AN_AVATAR_URL, - canSendStateResult = { _, _ -> Result.success(true) } ) val deleteCallback = lambdaRecorder {} val presenter = createRoomDetailsEditPresenter( @@ -515,7 +504,6 @@ class RoomDetailsEditPresenterTest { topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL, - canSendStateResult = { _, _ -> Result.success(true) } ) val deleteCallback = lambdaRecorder {} val presenter = createRoomDetailsEditPresenter( @@ -539,7 +527,6 @@ class RoomDetailsEditPresenterTest { displayName = "Name", avatarUrl = AN_AVATAR_URL, updateAvatarResult = updateAvatarResult, - canSendStateResult = { _, _ -> Result.success(true) } ) givenPickerReturnsFile() val deleteCallback = lambdaRecorder {} @@ -566,7 +553,6 @@ class RoomDetailsEditPresenterTest { topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL, - canSendStateResult = { _, _ -> Result.success(true) } ) fakePickerProvider.givenResult(anotherAvatarUri) fakeMediaPreProcessor.givenResult(Result.failure(RuntimeException("Oh no"))) @@ -591,7 +577,6 @@ class RoomDetailsEditPresenterTest { displayName = "Name", avatarUrl = AN_AVATAR_URL, setNameResult = { Result.failure(RuntimeException("!")) }, - canSendStateResult = { _, _ -> Result.success(true) } ) saveAndAssertFailure(room, RoomDetailsEditEvent.UpdateRoomName("New name"), deleteCallbackNumberOfInvocation = 1) } @@ -603,7 +588,6 @@ class RoomDetailsEditPresenterTest { displayName = "Name", avatarUrl = AN_AVATAR_URL, setTopicResult = { Result.failure(RuntimeException("!")) }, - canSendStateResult = { _, _ -> Result.success(true) } ) saveAndAssertFailure(room, RoomDetailsEditEvent.UpdateRoomTopic("New topic"), deleteCallbackNumberOfInvocation = 1) } @@ -615,7 +599,6 @@ class RoomDetailsEditPresenterTest { displayName = "Name", avatarUrl = AN_AVATAR_URL, removeAvatarResult = { Result.failure(RuntimeException("!")) }, - canSendStateResult = { _, _ -> Result.success(true) } ) saveAndAssertFailure(room, RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.Remove), deleteCallbackNumberOfInvocation = 2) } @@ -628,7 +611,6 @@ class RoomDetailsEditPresenterTest { displayName = "Name", avatarUrl = AN_AVATAR_URL, updateAvatarResult = { _, _ -> Result.failure(RuntimeException("!")) }, - canSendStateResult = { _, _ -> Result.success(true) } ) saveAndAssertFailure(room, RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.ChoosePhoto), deleteCallbackNumberOfInvocation = 2) } @@ -641,7 +623,6 @@ class RoomDetailsEditPresenterTest { displayName = "Name", avatarUrl = AN_AVATAR_URL, setTopicResult = { Result.failure(RuntimeException("!")) }, - canSendStateResult = { _, _ -> Result.success(true) } ) val deleteCallback = lambdaRecorder {} val presenter = createRoomDetailsEditPresenter( @@ -663,7 +644,6 @@ class RoomDetailsEditPresenterTest { fun `present - leave without saving - cancel`() = runTest { val room = aJoinedRoom( displayName = "Name", - canSendStateResult = { _, _ -> Result.success(true) } ) val deleteCallback = lambdaRecorder {} val presenter = createRoomDetailsEditPresenter( @@ -693,7 +673,6 @@ class RoomDetailsEditPresenterTest { fun `present - leave no changes, no confirmation`() = runTest { val room = aJoinedRoom( displayName = "Name", - canSendStateResult = { _, _ -> Result.success(true) } ) val presenter = createRoomDetailsEditPresenter( room = room, @@ -711,7 +690,7 @@ class RoomDetailsEditPresenterTest { fun `present - leave without saving - confirm`() = runTest { val room = aJoinedRoom( displayName = "Name", - canSendStateResult = { _, _ -> Result.success(true) } + canSendState = { _ -> true } ) val presenter = createRoomDetailsEditPresenter( room = room, @@ -782,11 +761,13 @@ class RoomDetailsEditPresenterTest { setTopicResult: (String) -> Result = { Result.success(Unit) }, updateAvatarResult: (String, ByteArray) -> Result = { _, _ -> Result.success(Unit) }, removeAvatarResult: () -> Result = { Result.success(Unit) }, - canSendStateResult: (UserId, StateEventType) -> Result, + canSendState: (StateEventType) -> Boolean = { true }, ): JoinedRoom { return FakeJoinedRoom( baseRoom = FakeBaseRoom( - canSendStateResult = canSendStateResult, + roomPermissions = FakeRoomPermissions( + canSendState = canSendState, + ), initialRoomInfo = aRoomInfo( name = displayName, topic = topic, diff --git a/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenterTest.kt b/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenterTest.kt index 2b3f71e7706..4ce50a24882 100644 --- a/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenterTest.kt +++ b/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenterTest.kt @@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.aRoomMember +import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule @@ -355,8 +356,10 @@ class RoomMemberModerationPresenterTest { banUserResult = { _, _ -> banUserResult }, unBanUserResult = { _, _ -> unBanUserResult }, baseRoom = FakeBaseRoom( - canBanResult = { _ -> Result.success(canBan) }, - canKickResult = { _ -> Result.success(canKick) }, + roomPermissions = FakeRoomPermissions( + canBan = canBan, + canKick = canKick + ), userRoleResult = { Result.success(myUserRole) }, updateMembersResult = { Result.success(Unit) } ), diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt index af0f44be38b..81bfe226976 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt @@ -67,7 +67,6 @@ class SecurityAndPrivacyPresenter( }.collectAsState(false) val saveAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } val homeserverName = remember { matrixClient.userIdServerName() } - val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val roomInfo by room.roomInfoFlow.collectAsState() val savedIsVisibleInRoomDirectory = remember { mutableStateOf>(AsyncData.Uninitialized) } diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyPresenterTest.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyPresenterTest.kt index 7d88706996c..a4542b02a1e 100644 --- a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyPresenterTest.kt +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyPresenterTest.kt @@ -18,15 +18,19 @@ import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.libraries.matrix.api.room.StateEventType 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.powerlevels.RoomPermissions import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility import io.element.android.libraries.matrix.test.A_ROOM_ALIAS import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions import io.element.android.tests.testutils.lambda.assert +import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.test import kotlinx.coroutines.test.runTest @@ -66,7 +70,7 @@ class SecurityAndPrivacyPresenterTest { fun `present - room info change updates saved and edited settings`() = runTest { val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canSendStateResult = { _, _ -> Result.success(true) }, + roomPermissions = roomPermissions(), initialRoomInfo = aRoomInfo( joinRule = JoinRule.Public, historyVisibility = RoomHistoryVisibility.WorldReadable, @@ -173,7 +177,7 @@ class SecurityAndPrivacyPresenterTest { fun `present - room visibility loading and change`() = runTest { val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canSendStateResult = { _, _ -> Result.success(true) }, + roomPermissions = roomPermissions(), getRoomVisibilityResult = { Result.success(RoomVisibility.Private) }, initialRoomInfo = aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared) ) @@ -222,7 +226,7 @@ class SecurityAndPrivacyPresenterTest { val updateRoomHistoryVisibilityLambda = lambdaRecorder> { Result.success(Unit) } val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canSendStateResult = { _, _ -> Result.success(true) }, + roomPermissions = roomPermissions(), getRoomVisibilityResult = { Result.success(RoomVisibility.Private) }, initialRoomInfo = aRoomInfo(joinRule = JoinRule.Invite, historyVisibility = RoomHistoryVisibility.Shared) ), @@ -272,8 +276,8 @@ class SecurityAndPrivacyPresenterTest { isEncrypted = true, ) ) - // Saved settings are updated 3 times to match the edited settings - skipItems(3) + // Saved settings are updated 2 times to match the edited settings + skipItems(2) with(awaitItem()) { assertThat(saveAction).isEqualTo(AsyncAction.Success(Unit)) assertThat(savedSettings).isEqualTo(editedSettings) @@ -297,7 +301,7 @@ class SecurityAndPrivacyPresenterTest { val updateRoomHistoryVisibilityLambda = lambdaRecorder> { Result.success(Unit) } val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canSendStateResult = { _, _ -> Result.success(true) }, + roomPermissions = roomPermissions(), getRoomVisibilityResult = { Result.success(RoomVisibility.Private) }, initialRoomInfo = aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared, joinRule = JoinRule.Private) ), @@ -340,7 +344,7 @@ class SecurityAndPrivacyPresenterTest { ) ) // Saved settings are updated 2 times to match the edited settings - skipItems(3) + skipItems(2) val state = awaitItem() with(state) { assertThat(saveAction).isInstanceOf(AsyncAction.Failure::class.java) @@ -374,11 +378,30 @@ class SecurityAndPrivacyPresenterTest { } } + private fun roomPermissions( + canChangeRoomAccess: Boolean = true, + canChangeHistoryVisibility: Boolean = true, + canChangeEncryption: Boolean = true, + canChangeRoomVisibility: Boolean = true, + ): RoomPermissions { + return FakeRoomPermissions( + canSendState = { eventType -> + when (eventType) { + StateEventType.ROOM_JOIN_RULES -> canChangeRoomAccess + StateEventType.ROOM_HISTORY_VISIBILITY -> canChangeHistoryVisibility + StateEventType.ROOM_ENCRYPTION -> canChangeEncryption + StateEventType.ROOM_CANONICAL_ALIAS -> canChangeRoomVisibility + else -> lambdaError() + } + } + ) + } + private fun createSecurityAndPrivacyPresenter( serverName: String = "matrix.org", room: FakeJoinedRoom = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canSendStateResult = { _, _ -> Result.success(true) }, + roomPermissions = roomPermissions(), getRoomVisibilityResult = { Result.success(RoomVisibility.Private) }, initialRoomInfo = aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared, joinRule = JoinRule.Private) ), diff --git a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt index 8b7eddca547..7e09a03ec39 100644 --- a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt +++ b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt @@ -76,7 +76,7 @@ class UserProfilePresenter( roomId ?.let { client.getRoom(it) } ?.use { room -> - room.roomPermissions().use(false){ perms -> perms.canCall()} + room.roomPermissions().use(false) { perms -> perms.canCall() } } .orFalse() } diff --git a/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt b/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt index aa984f00661..b10cf3e48d4 100644 --- a/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt +++ b/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt @@ -28,6 +28,7 @@ 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.UserId import io.element.android.libraries.matrix.api.encryption.identity.IdentityState +import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_ROOM_ID @@ -36,9 +37,11 @@ import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.any +import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.test @@ -89,15 +92,7 @@ class UserProfilePresenterTest { @Test fun `present - canCall is false when canUserJoinCall returns false`() { testCanCall( - canUserJoinCallResult = Result.success(false), - expectedResult = false, - ) - } - - @Test - fun `present - canCall is false when canUserJoinCall fails`() { - testCanCall( - canUserJoinCallResult = Result.failure(AN_EXCEPTION), + canUserJoinCall = false, expectedResult = false, ) } @@ -128,7 +123,7 @@ class UserProfilePresenterTest { private fun testCanCall( isElementCallAvailable: Boolean = true, - canUserJoinCallResult: Result = Result.success(true), + canUserJoinCall: Boolean = true, dmRoom: RoomId? = A_ROOM_ID, canFindRoom: Boolean = true, expectedResult: Boolean, @@ -136,7 +131,14 @@ class UserProfilePresenterTest { checkThatRoomIsDestroyed: Boolean = false, ) = runTest { val room = FakeBaseRoom( - canUserJoinCallResult = { canUserJoinCallResult }, + roomPermissions = FakeRoomPermissions( + canSendState = { type -> + when (type) { + StateEventType.CALL_MEMBER -> canUserJoinCall + else -> lambdaError() + } + } + ), ) val client = createFakeMatrixClient().apply { if (canFindRoom) { diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPermissions.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPermissions.kt index 735627e10ec..638bf5449ca 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPermissions.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPermissions.kt @@ -18,7 +18,6 @@ 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. @@ -150,7 +149,15 @@ fun Result.use(default: T, block: (RoomPermissions) -> T): fun BaseRoom.permissionsFlow(default: T, block: (RoomPermissions) -> T): Flow { return roomInfoFlow - .map { info -> info.roomPowerLevels } + .map { info -> + // If the user is a privileged creator, we return a constant hashcode to avoid recomputing permissions + // each time the power levels change (as they have all permissions). + if (info.privilegedCreatorRole && info.creators.contains(sessionId)) { + Long.MAX_VALUE + } else { + info.roomPowerLevels?.hashCode() ?: 0L + } + } .distinctUntilChanged() .map { roomPermissions().use(default, block) @@ -160,7 +167,6 @@ fun BaseRoom.permissionsFlow(default: T, block: (RoomPermissions) -> T): Flo @Composable fun BaseRoom.permissionsAsState(default: T, block: (RoomPermissions) -> T): State { return remember(this, default, block) { - Timber.d("Computing permissionsAsState for room $roomId with default=$default") permissionsFlow(default, block) }.collectAsState(default) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt index 141957d1eda..1aeb8f445d6 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt @@ -18,12 +18,10 @@ import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.ThreadId 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.RoomInfo 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.RoomMembershipObserver -import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.draft.ComposerDraft import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt index b05eff77e24..b56066af73a 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt @@ -15,11 +15,9 @@ import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.ThreadId 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.RoomInfo 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.StateEventType import io.element.android.libraries.matrix.api.room.draft.ComposerDraft import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues @@ -42,6 +40,7 @@ class FakeBaseRoom( override val sessionId: SessionId = A_SESSION_ID, override val roomId: RoomId = A_ROOM_ID, initialRoomInfo: RoomInfo = aRoomInfo(), + private val roomPermissions: RoomPermissions = FakeRoomPermissions(), override val roomCoroutineScope: CoroutineScope = TestScope(), private var roomPermalinkResult: () -> Result = { lambdaError() }, private var eventPermalinkResult: (EventId) -> Result = { lambdaError() }, @@ -50,17 +49,6 @@ class FakeBaseRoom( private val userRoleResult: () -> Result = { lambdaError() }, private val getUpdatedMemberResult: (UserId) -> Result = { lambdaError() }, private val joinRoomResult: () -> Result = { lambdaError() }, - private val roomPermissionsResult: () -> Result = { Result.success(FakeRoomPermissions()) }, - private val canInviteResult: (UserId) -> Result = { lambdaError() }, - private val canKickResult: (UserId) -> Result = { lambdaError() }, - private val canBanResult: (UserId) -> Result = { lambdaError() }, - private val canRedactOwnResult: (UserId) -> Result = { lambdaError() }, - private val canRedactOtherResult: (UserId) -> Result = { lambdaError() }, - private val canSendStateResult: (UserId, StateEventType) -> Result = { _, _ -> lambdaError() }, - private val canUserSendMessageResult: (UserId, MessageEventType) -> Result = { _, _ -> lambdaError() }, - private val canUserTriggerRoomNotificationResult: (UserId) -> Result = { lambdaError() }, - private val canUserJoinCallResult: (UserId) -> Result = { lambdaError() }, - private val canUserPinUnpinResult: (UserId) -> Result = { lambdaError() }, private val setIsFavoriteResult: (Boolean) -> Result = { lambdaError() }, private val markAsReadResult: (ReceiptType) -> Result = { Result.success(Unit) }, private val powerLevelsResult: () -> Result = { lambdaError() }, @@ -133,7 +121,7 @@ class FakeBaseRoom( } override suspend fun roomPermissions(): Result { - return roomPermissionsResult() + return Result.success(roomPermissions) } override suspend fun getPermalink(): Result { @@ -160,46 +148,6 @@ class FakeBaseRoom( return forgetResult() } - override suspend fun canUserBan(userId: UserId): Result { - return canBanResult(userId) - } - - override suspend fun canUserKick(userId: UserId): Result { - return canKickResult(userId) - } - - override suspend fun canUserInvite(userId: UserId): Result { - return canInviteResult(userId) - } - - override suspend fun canUserRedactOwn(userId: UserId): Result { - return canRedactOwnResult(userId) - } - - override suspend fun canUserRedactOther(userId: UserId): Result { - return canRedactOtherResult(userId) - } - - override suspend fun canUserSendState(userId: UserId, type: StateEventType): Result { - return canSendStateResult(userId, type) - } - - override suspend fun canUserSendMessage(userId: UserId, type: MessageEventType): Result { - return canUserSendMessageResult(userId, type) - } - - override suspend fun canUserTriggerRoomNotification(userId: UserId): Result { - return canUserTriggerRoomNotificationResult(userId) - } - - override suspend fun canUserJoinCall(userId: UserId): Result { - return canUserJoinCallResult(userId) - } - - override suspend fun canUserPinUnpin(userId: UserId): Result { - return canUserPinUnpinResult(userId) - } - override suspend fun setIsFavorite(isFavorite: Boolean): Result { return setIsFavoriteResult(isFavorite) } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/powerlevels/FakeRoomPermissions.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/powerlevels/FakeRoomPermissions.kt index 04ecb1f60b9..b78dc9ba1b3 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/powerlevels/FakeRoomPermissions.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/powerlevels/FakeRoomPermissions.kt @@ -13,44 +13,44 @@ import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions data class FakeRoomPermissions( - val ownerCanBan: Boolean = false, - val ownerCanInvite: Boolean = false, - val ownerCanKick: Boolean = false, - val ownerCanPinUnpin: Boolean = false, - val ownerCanRedactOther: Boolean = false, - val ownerCanRedactOwn: Boolean = false, - val ownerCanTriggerRoomNotification: Boolean = false, - val ownerCanSendMessage: (MessageEventType) -> Boolean = { false }, - val ownerCanSendState: (StateEventType) -> Boolean = { false }, - val userCanBan: (UserId) -> Boolean = { false }, - val userCanInvite: (UserId) -> Boolean = { false }, - val userCanKick: (UserId) -> Boolean = { false }, - val userCanPinUnpin: (UserId) -> Boolean = { false }, - val userCanRedactOther: (UserId) -> Boolean = { false }, - val userCanRedactOwn: (UserId) -> Boolean = { false }, - val userCanTriggerRoomNotification: (UserId) -> Boolean = { false }, - val userCanSendMessage: (UserId, MessageEventType) -> Boolean = { _, _ -> false }, - val userCanSendState: (UserId, StateEventType) -> Boolean = { _, _ -> false }, + private val canBan: Boolean = false, + private val canInvite: Boolean = false, + private val canKick: Boolean = false, + private val canPinUnpin: Boolean = false, + private val canRedactOther: Boolean = false, + private val canRedactOwn: Boolean = false, + private val canTriggerRoomNotification: Boolean = false, + private val canSendMessage: (MessageEventType) -> Boolean = { false }, + private val canSendState: (StateEventType) -> Boolean = { false }, + private val canUserBan: (UserId) -> Boolean = { false }, + private val canUserInvite: (UserId) -> Boolean = { false }, + private val canUserKick: (UserId) -> Boolean = { false }, + private val canUserPinUnpin: (UserId) -> Boolean = { false }, + private val canUserRedactOther: (UserId) -> Boolean = { false }, + private val canUserRedactOwn: (UserId) -> Boolean = { false }, + private val canUserTriggerRoomNotification: (UserId) -> Boolean = { false }, + private val canUserSendMessage: (UserId, MessageEventType) -> Boolean = { _, _ -> false }, + private val canUserSendState: (UserId, StateEventType) -> Boolean = { _, _ -> false }, ) : RoomPermissions { + override fun canOwnUserBan(): Boolean = canBan + override fun canOwnUserInvite(): Boolean = canInvite + override fun canOwnUserKick(): Boolean = canKick + override fun canOwnUserPinUnpin(): Boolean = canPinUnpin + override fun canOwnUserRedactOther(): Boolean = canRedactOther + override fun canOwnUserRedactOwn(): Boolean = canRedactOwn + override fun canOwnUserSendMessage(message: MessageEventType): Boolean = canSendMessage(message) + override fun canOwnUserSendState(stateEvent: StateEventType): Boolean = canSendState(stateEvent) - override fun canOwnUserBan(): Boolean = ownerCanBan - override fun canOwnUserInvite(): Boolean = ownerCanInvite - override fun canOwnUserKick(): Boolean = ownerCanKick - override fun canOwnUserPinUnpin(): Boolean = ownerCanPinUnpin - override fun canOwnUserRedactOther(): Boolean = ownerCanRedactOther - override fun canOwnUserRedactOwn(): Boolean = ownerCanRedactOwn - override fun canOwnUserSendMessage(message: MessageEventType): Boolean = ownerCanSendMessage(message) - override fun canOwnUserSendState(stateEvent: StateEventType): Boolean = ownerCanSendState(stateEvent) - override fun canOwnUserTriggerRoomNotification(): Boolean = ownerCanTriggerRoomNotification - override fun canUserBan(userId: UserId): Boolean = userCanBan(userId) - override fun canUserInvite(userId: UserId): Boolean = userCanInvite(userId) - override fun canUserKick(userId: UserId): Boolean = userCanKick(userId) - override fun canUserPinUnpin(userId: UserId): Boolean = userCanPinUnpin(userId) - override fun canUserRedactOther(userId: UserId): Boolean = userCanRedactOther(userId) - override fun canUserRedactOwn(userId: UserId): Boolean = userCanRedactOwn(userId) - override fun canUserSendMessage(userId: UserId, message: MessageEventType): Boolean = userCanSendMessage(userId, message) - override fun canUserSendState(userId: UserId, stateEvent: StateEventType): Boolean = userCanSendState(userId, stateEvent) - override fun canUserTriggerRoomNotification(userId: UserId): Boolean = userCanTriggerRoomNotification(userId) + override fun canOwnUserTriggerRoomNotification(): Boolean = canTriggerRoomNotification + override fun canUserBan(userId: UserId): Boolean = canUserBan(userId) + override fun canUserInvite(userId: UserId): Boolean = canUserInvite(userId) + override fun canUserKick(userId: UserId): Boolean = canUserKick(userId) + override fun canUserPinUnpin(userId: UserId): Boolean = canUserPinUnpin(userId) + override fun canUserRedactOther(userId: UserId): Boolean = canUserRedactOther(userId) + override fun canUserRedactOwn(userId: UserId): Boolean = canUserRedactOwn(userId) + override fun canUserSendMessage(userId: UserId, message: MessageEventType): Boolean = canUserSendMessage(userId, message) + override fun canUserSendState(userId: UserId, stateEvent: StateEventType): Boolean = canUserSendState(userId, stateEvent) + override fun canUserTriggerRoomNotification(userId: UserId): Boolean = canUserTriggerRoomNotification(userId) override fun close() { // no-op for the fake diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt index e5e493ee63b..cc26e69c33a 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt @@ -207,7 +207,6 @@ class MediaGalleryPresenter( CommonStrings.error_unknown } } - } private fun GroupedMediaItems?.find(eventId: EventId?): MediaItem.Event? { diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt index e890b7f0368..3069a4fd9d4 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt @@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.mediaviewer.impl.datasource.FakeMediaGalleryDataSource import io.element.android.libraries.mediaviewer.impl.datasource.MediaGalleryDataSource @@ -109,7 +110,9 @@ class MediaGalleryPresenterTest { baseRoom = FakeBaseRoom( sessionId = A_USER_ID, initialRoomInfo = aRoomInfo(name = A_ROOM_NAME), - canRedactOwnResult = { Result.success(canDeleteOwn) } + roomPermissions = FakeRoomPermissions( + canRedactOwn = canDeleteOwn + ), ), ) ) @@ -153,7 +156,9 @@ class MediaGalleryPresenterTest { baseRoom = FakeBaseRoom( sessionId = A_USER_ID, initialRoomInfo = aRoomInfo(name = A_ROOM_NAME), - canRedactOtherResult = { Result.success(canDeleteOther) }, + roomPermissions = FakeRoomPermissions( + canRedactOther = canDeleteOther + ), ), createTimelineResult = { Result.success(FakeTimeline()) } ) @@ -355,7 +360,9 @@ class MediaGalleryPresenterTest { room = FakeJoinedRoom( createTimelineResult = { Result.success(FakeTimeline()) }, baseRoom = FakeBaseRoom( - canRedactOwnResult = { Result.success(true) }, + roomPermissions = FakeRoomPermissions( + canRedactOwn = true + ), ), ), navigator = navigator, @@ -386,7 +393,9 @@ class MediaGalleryPresenterTest { room = FakeJoinedRoom( createTimelineResult = { Result.success(FakeTimeline()) }, baseRoom = FakeBaseRoom( - canRedactOwnResult = { Result.success(true) }, + roomPermissions = FakeRoomPermissions( + canRedactOwn = true + ), ), ), navigator = navigator, diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt index d9769aac13a..a9d1704bdcf 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt @@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader import io.element.android.libraries.matrix.test.media.aMediaSource import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint import io.element.android.libraries.mediaviewer.api.anApkMediaInfo @@ -83,7 +84,9 @@ class MediaViewerPresenterTest { localMediaFactory = localMediaFactory, room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canRedactOwnResult = { Result.success(true) }, + roomPermissions = FakeRoomPermissions( + canRedactOwn = true + ), ) ) ) @@ -104,7 +107,9 @@ class MediaViewerPresenterTest { canShowInfo = false, room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canRedactOwnResult = { Result.success(true) }, + roomPermissions = FakeRoomPermissions( + canRedactOwn = true + ), ) ) ) @@ -125,7 +130,9 @@ class MediaViewerPresenterTest { eventId = AN_EVENT_ID, room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canRedactOwnResult = { Result.success(true) }, + roomPermissions = FakeRoomPermissions( + canRedactOwn = true + ), ) ) ) @@ -147,7 +154,9 @@ class MediaViewerPresenterTest { room = FakeJoinedRoom( baseRoom = FakeBaseRoom( sessionId = A_SESSION_ID_2, - canRedactOtherResult = { Result.success(false) }, + roomPermissions = FakeRoomPermissions( + canRedactOther = false + ), ) ) ) @@ -236,7 +245,9 @@ class MediaViewerPresenterTest { mediaGalleryDataSource = mediaGalleryDataSource, room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canRedactOwnResult = { Result.success(true) }, + roomPermissions = FakeRoomPermissions( + canRedactOwn = true + ), ) ) ) @@ -460,7 +471,11 @@ class MediaViewerPresenterTest { localMediaFactory = localMediaFactory, room = FakeJoinedRoom( liveTimeline = timeline, - baseRoom = FakeBaseRoom(canRedactOwnResult = { Result.success(true) }), + baseRoom = FakeBaseRoom( + roomPermissions = FakeRoomPermissions( + canRedactOwn = true + ), + ), ), mediaGalleryDataSource = mediaGalleryDataSource, mediaViewerNavigator = FakeMediaViewerNavigator( @@ -769,7 +784,11 @@ class MediaViewerPresenterTest { localMediaFactory = localMediaFactory, mediaViewerNavigator = navigator, room = FakeJoinedRoom( - baseRoom = FakeBaseRoom(canRedactOwnResult = { Result.success(true) }), + baseRoom = FakeBaseRoom( + roomPermissions = FakeRoomPermissions( + canRedactOwn = true + ), + ), ) ) presenter.test { @@ -794,7 +813,11 @@ class MediaViewerPresenterTest { localMediaFactory = localMediaFactory, mediaViewerNavigator = navigator, room = FakeJoinedRoom( - baseRoom = FakeBaseRoom(canRedactOwnResult = { Result.success(true) }), + baseRoom = FakeBaseRoom( + roomPermissions = FakeRoomPermissions( + canRedactOwn = true + ), + ), ), ) presenter.test { @@ -821,7 +844,11 @@ class MediaViewerPresenterTest { localMediaFactory = localMediaFactory, mediaViewerNavigator = navigator, room = FakeJoinedRoom( - baseRoom = FakeBaseRoom(canRedactOwnResult = { Result.success(true) }), + baseRoom = FakeBaseRoom( + roomPermissions = FakeRoomPermissions( + canRedactOwn = true + ), + ), ), ) presenter.test { From 03ac70b662f6e84cb6d5f4016a8e2bc3ebc5050b Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 12 Dec 2025 18:09:06 +0100 Subject: [PATCH 5/5] misc(power level) : expose RoomMemberModerationPermissions after PR review --- .../messages/impl/MessagesStateProvider.kt | 7 ++--- .../impl/members/RoomMemberListState.kt | 2 +- .../members/RoomMemberListStateProvider.kt | 7 +++-- .../api/RoomMemberModerationPermissions.kt | 29 +++++++++++++++++++ .../api/RoomMemberModerationState.kt | 3 +- .../impl/InternalRoomMemberModerationState.kt | 4 +-- ...ternalRoomMemberModerationStateProvider.kt | 7 ++--- .../impl/RoomMemberModerationPresenter.kt | 19 ++++-------- .../impl/RoomMemberModerationPresenterTest.kt | 4 +-- 9 files changed, 52 insertions(+), 30 deletions(-) create mode 100644 features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationPermissions.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index 7831265fd08..cec9f68b454 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -41,6 +41,7 @@ import io.element.android.features.messages.impl.timeline.protection.aTimelinePr import io.element.android.features.roomcall.api.RoomCallState import io.element.android.features.roomcall.api.aStandByCallState import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationPermissions import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.avatar.AvatarData @@ -164,11 +165,9 @@ fun aMessagesState( ) fun aRoomMemberModerationState( - canKick: Boolean = false, - canBan: Boolean = false, + permissions: RoomMemberModerationPermissions = RoomMemberModerationPermissions.DEFAULT, ) = object : RoomMemberModerationState { - override val canKick: Boolean = canKick - override val canBan: Boolean = canBan + override val permissions: RoomMemberModerationPermissions = permissions override val eventSink: (RoomMemberModerationEvents) -> Unit = {} } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt index 007d276dd3a..7c928fb27ab 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt @@ -26,7 +26,7 @@ data class RoomMemberListState( val moderationState: RoomMemberModerationState, val eventSink: (RoomMemberListEvents) -> Unit, ) { - val showBannedSection: Boolean = moderationState.canBan && roomMembers.dataOrNull()?.banned?.isNotEmpty() == true + val showBannedSection: Boolean = moderationState.permissions.canBan && roomMembers.dataOrNull()?.banned?.isNotEmpty() == true } enum class SelectedSection { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt index 580db7667ab..b37738c30f9 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt @@ -10,6 +10,7 @@ package io.element.android.features.roomdetails.impl.members import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationPermissions import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.map @@ -99,8 +100,10 @@ fun aRoomMemberModerationState( canKick: Boolean = false, ): RoomMemberModerationState { return object : RoomMemberModerationState { - override val canKick: Boolean = canKick - override val canBan: Boolean = canBan + override val permissions: RoomMemberModerationPermissions = RoomMemberModerationPermissions( + canBan = canBan, + canKick = canKick, + ) override val eventSink: (RoomMemberModerationEvents) -> Unit = {} } } diff --git a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationPermissions.kt b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationPermissions.kt new file mode 100644 index 00000000000..223456de695 --- /dev/null +++ b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationPermissions.kt @@ -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.features.roommembermoderation.api + +import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions + +data class RoomMemberModerationPermissions( + val canKick: Boolean, + val canBan: Boolean, +) { + companion object { + val DEFAULT = RoomMemberModerationPermissions( + canKick = false, + canBan = false, + ) + } +} + +fun RoomPermissions.roomMemberModerationPermissions(): RoomMemberModerationPermissions { + return RoomMemberModerationPermissions( + canKick = canOwnUserKick(), + canBan = canOwnUserBan(), + ) +} diff --git a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt index 85f3e8ec19e..c9ee958f6f3 100644 --- a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt +++ b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt @@ -12,8 +12,7 @@ import androidx.compose.runtime.Immutable @Immutable interface RoomMemberModerationState { - val canKick: Boolean - val canBan: Boolean + val permissions: RoomMemberModerationPermissions val eventSink: (RoomMemberModerationEvents) -> Unit } diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt index fe48ece1fd2..f45a2779922 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt @@ -10,14 +10,14 @@ package io.element.android.features.roommembermoderation.impl import io.element.android.features.roommembermoderation.api.ModerationActionState import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationPermissions import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.user.MatrixUser import kotlinx.collections.immutable.ImmutableList data class InternalRoomMemberModerationState( - override val canKick: Boolean, - override val canBan: Boolean, + override val permissions: RoomMemberModerationPermissions, val selectedUser: MatrixUser?, val actions: ImmutableList, val kickUserAsyncAction: AsyncAction, diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationStateProvider.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationStateProvider.kt index d90f352cfa4..120a299a7d4 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationStateProvider.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationStateProvider.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.roommembermoderation.api.ModerationAction import io.element.android.features.roommembermoderation.api.ModerationActionState import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationPermissions import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.user.MatrixUser @@ -83,8 +84,7 @@ fun anAlice() = MatrixUser( ) fun aRoomMembersModerationState( - canKick: Boolean = false, - canBan: Boolean = false, + permissions: RoomMemberModerationPermissions = RoomMemberModerationPermissions.DEFAULT, selectedUser: MatrixUser? = null, actions: List = emptyList(), kickUserAsyncAction: AsyncAction = AsyncAction.Uninitialized, @@ -92,8 +92,7 @@ fun aRoomMembersModerationState( unbanUserAsyncAction: AsyncAction = AsyncAction.Uninitialized, eventSink: (RoomMemberModerationEvents) -> Unit = {}, ) = InternalRoomMemberModerationState( - canKick = canKick, - canBan = canBan, + permissions = permissions, selectedUser = selectedUser, actions = actions.toImmutableList(), kickUserAsyncAction = kickUserAsyncAction, diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt index 96749ba5ac3..16a11aee96b 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt @@ -21,7 +21,9 @@ import im.vector.app.features.analytics.plan.RoomModeration import io.element.android.features.roommembermoderation.api.ModerationAction import io.element.android.features.roommembermoderation.api.ModerationActionState import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationPermissions import io.element.android.features.roommembermoderation.api.RoomMemberModerationState +import io.element.android.features.roommembermoderation.api.roomMemberModerationPermissions import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runUpdatingState @@ -55,11 +57,8 @@ class RoomMemberModerationPresenter( override fun present(): RoomMemberModerationState { val coroutineScope = rememberCoroutineScope() val syncUpdateFlow = room.syncUpdateFlow.collectAsState() - val permissions by room.permissionsAsState(Permissions()) { perms -> - Permissions( - canKick = perms.canOwnUserKick(), - canBan = perms.canOwnUserBan(), - ) + val permissions by room.permissionsAsState(RoomMemberModerationPermissions.DEFAULT) { perms -> + perms.roomMemberModerationPermissions() } val currentUserMemberPowerLevel = room.userPowerLevelAsState(syncUpdateFlow.value) @@ -136,8 +135,7 @@ class RoomMemberModerationPresenter( } return InternalRoomMemberModerationState( - canKick = permissions.canKick, - canBan = permissions.canBan, + permissions = permissions, selectedUser = selectedUser, actions = moderationActions.value, kickUserAsyncAction = kickUserAsyncAction.value, @@ -149,7 +147,7 @@ class RoomMemberModerationPresenter( private fun computeModerationActions( member: RoomMember?, - permissions: Permissions, + permissions: RoomMemberModerationPermissions, currentUserMemberPowerLevel: Long, ): ImmutableList { return buildList { @@ -209,11 +207,6 @@ class RoomMemberModerationPresenter( ) } - private data class Permissions( - val canKick: Boolean = false, - val canBan: Boolean = false, - ) - private fun CoroutineScope.runActionAndWaitForMembershipChange( action: MutableState>, block: suspend () -> Result diff --git a/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenterTest.kt b/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenterTest.kt index 4ce50a24882..3f59151eeaf 100644 --- a/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenterTest.kt +++ b/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenterTest.kt @@ -13,6 +13,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.roommembermoderation.api.ModerationAction import io.element.android.features.roommembermoderation.api.ModerationActionState import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationPermissions import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.core.coroutine.CoroutineDispatchers @@ -49,8 +50,7 @@ class RoomMemberModerationPresenterTest { val room = aJoinedRoom() createRoomMemberModerationPresenter(room = room).test { val initialState = awaitState() - assertThat(initialState.canKick).isFalse() - assertThat(initialState.canBan).isFalse() + assertThat(initialState.permissions).isEqualTo(RoomMemberModerationPermissions.DEFAULT) assertThat(initialState.selectedUser).isNull() assertThat(initialState.banUserAsyncAction).isEqualTo(AsyncAction.Uninitialized) assertThat(initialState.kickUserAsyncAction).isEqualTo(AsyncAction.Uninitialized)