Adapt 'change roles' screens to the new creator/owner role (#5076)
* Replace `RoomMember.Role.CREATOR` with `RoomMember.Role.Owner` - Make `RoomMember.Role` a sealed interface instead
* Adapt room member role mapping to include the power level to distinguish between admins and owners
* Use new `RoomMember.Role` sealed interface through the app
* Change how `MembersByRole` groups members to add owners to the admins section
* Adapt the `ChangeRoles` screen to the new roles:
- Owners can't modify other owner's roles.
- They can modify the roles of any other user, without confirmation.
* Adapt 'roles and permissions' screen:
- Owners can't demote themselves.
- The admin count also counts owners.
* Add more tests and screenshots
* Add owners to its own section in the 'change roles' screen
* Update screenshots
---------
Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
parent
4534229e84
commit
51f67741ae
77 changed files with 663 additions and 301 deletions
|
|
@ -85,7 +85,7 @@ data class RoomInfo(
|
|||
* Returns the list of users with the given [role] in this room.
|
||||
*/
|
||||
fun usersWithRole(role: RoomMember.Role): List<UserId> {
|
||||
return if (role == RoomMember.Role.CREATOR) {
|
||||
return if (role is RoomMember.Role.Owner && role.isCreator) {
|
||||
this.creators
|
||||
} else {
|
||||
this.roomPowerLevels?.usersWithRole(role).orEmpty().toList()
|
||||
|
|
|
|||
|
|
@ -25,21 +25,34 @@ data class RoomMember(
|
|||
/**
|
||||
* Role of the RoomMember, based on its [powerLevel].
|
||||
*/
|
||||
enum class Role(val powerLevel: Long) {
|
||||
CREATOR(Long.MAX_VALUE),
|
||||
ADMIN(100L),
|
||||
MODERATOR(50L),
|
||||
USER(0L);
|
||||
sealed interface Role {
|
||||
data class Owner(val isCreator: Boolean) : Role
|
||||
data object Admin : Role
|
||||
data object Moderator : Role
|
||||
data object User : Role
|
||||
|
||||
val powerLevel: Long
|
||||
get() = when (this) {
|
||||
is Owner -> if (isCreator) CREATOR_POWERLEVEL else SUPERADMIN_POWERLEVEL
|
||||
Admin -> ADMIN_POWERLEVEL
|
||||
Moderator -> MODERATOR_POWERLEVEL
|
||||
User -> USER_POWERLEVEL
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SUPER_ADMIN_LEVEL = 150L
|
||||
private const val CREATOR_POWERLEVEL = Long.MAX_VALUE
|
||||
private const val SUPERADMIN_POWERLEVEL = 150L
|
||||
private const val ADMIN_POWERLEVEL = 100L
|
||||
private const val MODERATOR_POWERLEVEL = 50L
|
||||
private const val USER_POWERLEVEL = 0L
|
||||
|
||||
fun forPowerLevel(powerLevel: Long): Role {
|
||||
return when {
|
||||
powerLevel > SUPER_ADMIN_LEVEL -> CREATOR
|
||||
powerLevel >= ADMIN.powerLevel -> ADMIN
|
||||
powerLevel >= MODERATOR.powerLevel -> MODERATOR
|
||||
else -> USER
|
||||
powerLevel == CREATOR_POWERLEVEL -> Owner(isCreator = true)
|
||||
powerLevel >= SUPERADMIN_POWERLEVEL -> Owner(isCreator = false)
|
||||
powerLevel >= ADMIN_POWERLEVEL -> Admin
|
||||
powerLevel >= MODERATOR_POWERLEVEL -> Moderator
|
||||
else -> User
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -87,11 +100,3 @@ fun RoomMember.toMatrixUser() = MatrixUser(
|
|||
displayName = displayName,
|
||||
avatarUrl = avatarUrl,
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns `true` if the [RoomMember] is an owner of the room.
|
||||
* Owners are defined as members with either the [RoomMember.Role.CREATOR] role or a power level greater than or equal to [RoomMember.Role.SUPER_ADMIN_LEVEL].
|
||||
*/
|
||||
fun RoomMember.isOwner(): Boolean {
|
||||
return role == RoomMember.Role.CREATOR || powerLevel >= RoomMember.Role.SUPER_ADMIN_LEVEL
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,14 +25,23 @@ data class RoomPowerLevels(
|
|||
val values: RoomPowerLevelsValues,
|
||||
private val users: ImmutableMap<UserId, Long>,
|
||||
) {
|
||||
/**
|
||||
* Returns the power level of the user in the room.
|
||||
*
|
||||
* If the user is not found, returns 0.
|
||||
*/
|
||||
fun powerLevelOf(userId: UserId): Long {
|
||||
return users[userId] ?: 0L
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the set of [UserId]s that have the given role in the room.
|
||||
*
|
||||
* **WARNING**: This method must not be used with the [RoomMember.Role.CREATOR] role. It'll result in a runtime error.
|
||||
* **WARNING**: This method must not be used with a creator role. It'll result in a runtime error.
|
||||
*/
|
||||
fun usersWithRole(role: RoomMember.Role): Set<UserId> {
|
||||
return if (role == RoomMember.Role.CREATOR) {
|
||||
error("RoomPowerLevels.usersWithRole should not be used with CREATOR role, use roomInfo.creators instead")
|
||||
return if (role is RoomMember.Role.Owner && role.isCreator) {
|
||||
error("RoomPowerLevels.usersWithRole should not be used with a creator role, use roomInfo.creators instead")
|
||||
} else {
|
||||
users.filterValues { RoomMember.Role.forPowerLevel(it) == role }.keys
|
||||
}
|
||||
|
|
@ -42,7 +51,7 @@ data class RoomPowerLevels(
|
|||
* Returns the role of the user in the room based on their power level.
|
||||
* If the user is not found, returns null.
|
||||
*
|
||||
* **WARNING**: This method must not be used with the [RoomMember.Role.CREATOR] role, as it won't return any results.
|
||||
* **WARNING**: This method must not be used with a creator role, as it won't return any results.
|
||||
*/
|
||||
fun roleOf(userId: UserId): RoomMember.Role? {
|
||||
return users[userId]?.let(RoomMember.Role::forPowerLevel)
|
||||
|
|
|
|||
|
|
@ -343,7 +343,7 @@ class RustMatrixClient(
|
|||
powerLevelContentOverride = defaultRoomCreationPowerLevels.copy(
|
||||
invite = if (createRoomParams.joinRuleOverride == JoinRule.Knock) {
|
||||
// override the invite power level so it's the same as kick.
|
||||
RoomMember.Role.MODERATOR.powerLevel.toInt()
|
||||
RoomMember.Role.Moderator.powerLevel.toInt()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,7 +128,11 @@ class RustBaseRoom(
|
|||
|
||||
override suspend fun userRole(userId: UserId): Result<RoomMember.Role> = withContext(roomDispatcher) {
|
||||
runCatchingExceptions {
|
||||
RoomMemberMapper.mapRole(innerRoom.suggestedRoleForUser(userId.value))
|
||||
val powerLevel = roomInfoFlow.value.roomPowerLevels?.powerLevelOf(userId) ?: 0L
|
||||
RoomMemberMapper.mapRole(
|
||||
role = innerRoom.suggestedRoleForUser(userId.value),
|
||||
powerLevel = powerLevel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,25 +16,36 @@ import org.matrix.rustcomponents.sdk.MembershipState as RustMembershipState
|
|||
import org.matrix.rustcomponents.sdk.RoomMember as RustRoomMember
|
||||
|
||||
object RoomMemberMapper {
|
||||
fun map(roomMember: RustRoomMember): RoomMember = RoomMember(
|
||||
userId = UserId(roomMember.userId),
|
||||
displayName = roomMember.displayName,
|
||||
avatarUrl = roomMember.avatarUrl,
|
||||
membership = mapMembership(roomMember.membership),
|
||||
isNameAmbiguous = roomMember.isNameAmbiguous,
|
||||
powerLevel = roomMember.powerLevel.into(),
|
||||
normalizedPowerLevel = roomMember.normalizedPowerLevel.into(),
|
||||
isIgnored = roomMember.isIgnored,
|
||||
role = mapRole(roomMember.suggestedRoleForPowerLevel),
|
||||
membershipChangeReason = roomMember.membershipChangeReason
|
||||
)
|
||||
fun map(roomMember: RustRoomMember): RoomMember {
|
||||
val powerLevel = roomMember.powerLevel.into()
|
||||
return RoomMember(
|
||||
userId = UserId(roomMember.userId),
|
||||
displayName = roomMember.displayName,
|
||||
avatarUrl = roomMember.avatarUrl,
|
||||
membership = mapMembership(roomMember.membership),
|
||||
isNameAmbiguous = roomMember.isNameAmbiguous,
|
||||
powerLevel = powerLevel,
|
||||
normalizedPowerLevel = roomMember.normalizedPowerLevel.into(),
|
||||
isIgnored = roomMember.isIgnored,
|
||||
role = mapRole(roomMember.suggestedRoleForPowerLevel, powerLevel),
|
||||
membershipChangeReason = roomMember.membershipChangeReason
|
||||
)
|
||||
}
|
||||
|
||||
fun mapRole(role: RoomMemberRole): RoomMember.Role =
|
||||
fun mapRole(role: RoomMemberRole, powerLevel: Long?): RoomMember.Role =
|
||||
when (role) {
|
||||
RoomMemberRole.CREATOR -> RoomMember.Role.CREATOR
|
||||
RoomMemberRole.ADMINISTRATOR -> RoomMember.Role.ADMIN
|
||||
RoomMemberRole.MODERATOR -> RoomMember.Role.MODERATOR
|
||||
RoomMemberRole.USER -> RoomMember.Role.USER
|
||||
RoomMemberRole.CREATOR -> RoomMember.Role.Owner(isCreator = true)
|
||||
RoomMemberRole.ADMINISTRATOR -> {
|
||||
val superAdmin = RoomMember.Role.Owner(isCreator = false)
|
||||
val powerLevelOrDefault = powerLevel ?: 0L
|
||||
if (powerLevelOrDefault >= superAdmin.powerLevel) {
|
||||
superAdmin
|
||||
} else {
|
||||
RoomMember.Role.Admin
|
||||
}
|
||||
}
|
||||
RoomMemberRole.MODERATOR -> RoomMember.Role.Moderator
|
||||
RoomMemberRole.USER -> RoomMember.Role.User
|
||||
}
|
||||
|
||||
fun mapMembership(membershipState: RustMembershipState): RoomMembershipState =
|
||||
|
|
|
|||
|
|
@ -28,6 +28,6 @@ object RoomPowerLevelsValuesMapper {
|
|||
}
|
||||
|
||||
fun PowerLevel.into(): Long = when (this) {
|
||||
PowerLevel.Infinite -> RoomMember.Role.CREATOR.powerLevel
|
||||
PowerLevel.Infinite -> RoomMember.Role.Owner(isCreator = true).powerLevel
|
||||
is PowerLevel.Value -> this.value
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import org.matrix.rustcomponents.sdk.NoPointer
|
|||
import org.matrix.rustcomponents.sdk.Room
|
||||
import org.matrix.rustcomponents.sdk.RoomInfo
|
||||
import org.matrix.rustcomponents.sdk.RoomMembersIterator
|
||||
import uniffi.matrix_sdk.RoomMemberRole
|
||||
|
||||
class FakeFfiRoom(
|
||||
private val roomId: RoomId = A_ROOM_ID,
|
||||
|
|
@ -23,6 +24,7 @@ class FakeFfiRoom(
|
|||
private val getMembersNoSync: () -> RoomMembersIterator = { lambdaError() },
|
||||
private val leaveLambda: () -> Unit = { lambdaError() },
|
||||
private val latestEventLambda: () -> EventTimelineItem? = { lambdaError() },
|
||||
private val suggestedRoleForUserLambda: (String) -> RoomMemberRole = { lambdaError() },
|
||||
private val roomInfo: RoomInfo = aRustRoomInfo(id = roomId.value),
|
||||
) : Room(NoPointer) {
|
||||
override fun id(): String {
|
||||
|
|
@ -49,6 +51,10 @@ class FakeFfiRoom(
|
|||
return latestEventLambda()
|
||||
}
|
||||
|
||||
override suspend fun suggestedRoleForUser(userId: String): RoomMemberRole {
|
||||
return suggestedRoleForUserLambda(userId)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
// No-op
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,20 +12,26 @@ import app.cash.turbine.test
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
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.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoom
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomListService
|
||||
import io.element.android.libraries.matrix.test.A_DEVICE_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import uniffi.matrix_sdk.RoomMemberRole
|
||||
|
||||
class RustBaseRoomTest {
|
||||
@Test
|
||||
|
|
@ -111,6 +117,29 @@ class RustBaseRoomTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `userRole loads and maps the role`() = runTest {
|
||||
val rustBaseRoom = createRustBaseRoom(
|
||||
initialRoomInfo = aRoomInfo(
|
||||
roomPowerLevels = RoomPowerLevels(
|
||||
values = RoomPowerLevelsValues(50, 50, 50, 50, 50, 50, 50, 50),
|
||||
users = persistentMapOf(A_USER_ID to 100L)
|
||||
)
|
||||
),
|
||||
innerRoom = FakeFfiRoom(
|
||||
suggestedRoleForUserLambda = { userId ->
|
||||
// Simulate the role suggestion based on power level
|
||||
if (userId == A_USER_ID.value) RoomMemberRole.ADMINISTRATOR else RoomMemberRole.USER
|
||||
}
|
||||
),
|
||||
)
|
||||
val result = rustBaseRoom.userRole(A_USER_ID).getOrNull()
|
||||
assertThat(result).isNotNull()
|
||||
assertThat(result).isEqualTo(RoomMember.Role.Admin)
|
||||
|
||||
rustBaseRoom.destroy()
|
||||
}
|
||||
|
||||
private suspend fun TestScope.leaveRoomAndObserveMembershipChange(
|
||||
roomMembershipObserver: RoomMembershipObserver,
|
||||
rustBaseRoom: RustBaseRoom,
|
||||
|
|
|
|||
|
|
@ -17,9 +17,19 @@ import org.matrix.rustcomponents.sdk.MembershipState as RustMembershipState
|
|||
class RoomMemberMapperTest {
|
||||
@Test
|
||||
fun mapRole() {
|
||||
assertThat(RoomMemberMapper.mapRole(RoomMemberRole.USER)).isEqualTo(RoomMember.Role.USER)
|
||||
assertThat(RoomMemberMapper.mapRole(RoomMemberRole.MODERATOR)).isEqualTo(RoomMember.Role.MODERATOR)
|
||||
assertThat(RoomMemberMapper.mapRole(RoomMemberRole.ADMINISTRATOR)).isEqualTo(RoomMember.Role.ADMIN)
|
||||
assertThat(RoomMemberMapper.mapRole(RoomMemberRole.USER, 0L)).isEqualTo(RoomMember.Role.User)
|
||||
assertThat(RoomMemberMapper.mapRole(RoomMemberRole.MODERATOR, 50L)).isEqualTo(RoomMember.Role.Moderator)
|
||||
assertThat(RoomMemberMapper.mapRole(RoomMemberRole.ADMINISTRATOR, 100L)).isEqualTo(RoomMember.Role.Admin)
|
||||
assertThat(RoomMemberMapper.mapRole(RoomMemberRole.ADMINISTRATOR, 150L)).isEqualTo(RoomMember.Role.Owner(isCreator = false))
|
||||
assertThat(RoomMemberMapper.mapRole(RoomMemberRole.CREATOR, Long.MAX_VALUE)).isEqualTo(RoomMember.Role.Owner(isCreator = true))
|
||||
|
||||
// `null` power level defaults to USER role
|
||||
assertThat(RoomMemberMapper.mapRole(RoomMemberRole.ADMINISTRATOR, null)).isEqualTo(RoomMember.Role.Admin)
|
||||
|
||||
// Power level is only taken into account for ADMINISTRATOR role
|
||||
assertThat(RoomMemberMapper.mapRole(RoomMemberRole.USER, 123L)).isEqualTo(RoomMember.Role.User)
|
||||
assertThat(RoomMemberMapper.mapRole(RoomMemberRole.MODERATOR, 1L)).isEqualTo(RoomMember.Role.Moderator)
|
||||
assertThat(RoomMemberMapper.mapRole(RoomMemberRole.CREATOR, 0L)).isEqualTo(RoomMember.Role.Owner(isCreator = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ fun aRoomMember(
|
|||
powerLevel: Long = 0L,
|
||||
normalizedPowerLevel: Long = 0L,
|
||||
isIgnored: Boolean = false,
|
||||
role: RoomMember.Role = RoomMember.Role.USER,
|
||||
role: RoomMember.Role = RoomMember.Role.User,
|
||||
membershipChangeReason: String? = null,
|
||||
) = RoomMember(
|
||||
userId = userId,
|
||||
|
|
|
|||
|
|
@ -22,14 +22,14 @@ fun RoomInfo.getAvatarData(size: AvatarSize) = AvatarData(
|
|||
|
||||
/**
|
||||
* Returns the role of the user in the room.
|
||||
* If the user is a creator, returns [RoomMember.Role.CREATOR].
|
||||
* If the user is a creator, returns [RoomMember.Role.Owner].
|
||||
* Otherwise, checks the power levels and returns the corresponding role.
|
||||
* If no specific power level is set for the user, defaults to [RoomMember.Role.USER].
|
||||
* If no specific power level is set for the user, defaults to [RoomMember.Role.User].
|
||||
*/
|
||||
fun RoomInfo.roleOf(userId: UserId): RoomMember.Role {
|
||||
return if (creators.contains(userId)) {
|
||||
RoomMember.Role.CREATOR
|
||||
RoomMember.Role.Owner(isCreator = true)
|
||||
} else {
|
||||
roomPowerLevels?.roleOf(userId) ?: RoomMember.Role.USER
|
||||
roomPowerLevels?.roleOf(userId) ?: RoomMember.Role.User
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ fun BaseRoom.canHandleKnockRequestsAsState(updateKey: Long): State<Boolean> {
|
|||
fun BaseRoom.userPowerLevelAsState(updateKey: Long): State<Long> {
|
||||
return produceState(initialValue = 0, key1 = updateKey) {
|
||||
value = userRole(sessionId)
|
||||
.getOrDefault(RoomMember.Role.USER)
|
||||
.getOrDefault(RoomMember.Role.User)
|
||||
.powerLevel
|
||||
}
|
||||
}
|
||||
|
|
@ -108,7 +108,7 @@ fun BaseRoom.userPowerLevelAsState(updateKey: Long): State<Long> {
|
|||
fun BaseRoom.isOwnUserAdmin(): Boolean {
|
||||
val roomInfo by roomInfoFlow.collectAsState()
|
||||
val role = roomInfo.roleOf(sessionId)
|
||||
return role == RoomMember.Role.ADMIN || role == RoomMember.Role.CREATOR
|
||||
return role == RoomMember.Role.Admin || role is RoomMember.Role.Owner
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue