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:
committed by
GitHub
parent
9509ad5170
commit
8298404630
@@ -157,7 +157,7 @@ internal fun SuggestionsPickerViewPreview() {
|
||||
powerLevel = 0L,
|
||||
normalizedPowerLevel = 0L,
|
||||
isIgnored = false,
|
||||
role = RoomMember.Role.USER,
|
||||
role = RoomMember.Role.User,
|
||||
membershipChangeReason = null,
|
||||
)
|
||||
val anAlias = remember { RoomAlias("#room:domain.org") }
|
||||
|
||||
@@ -69,7 +69,7 @@ fun aDmRoomMember(
|
||||
powerLevel: Long = 0,
|
||||
normalizedPowerLevel: Long = powerLevel,
|
||||
isIgnored: Boolean = false,
|
||||
role: RoomMember.Role = RoomMember.Role.USER,
|
||||
role: RoomMember.Role = RoomMember.Role.User,
|
||||
membershipChangeReason: String? = null,
|
||||
) = RoomMember(
|
||||
userId = userId,
|
||||
|
||||
@@ -13,10 +13,10 @@ import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsV
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
|
||||
internal fun RoomMember.Role.toAnalyticsMemberRole(): RoomModeration.Role = when (this) {
|
||||
RoomMember.Role.CREATOR -> RoomModeration.Role.Administrator // TODO - distinguish creator from admin
|
||||
RoomMember.Role.ADMIN -> RoomModeration.Role.Administrator
|
||||
RoomMember.Role.MODERATOR -> RoomModeration.Role.Moderator
|
||||
RoomMember.Role.USER -> RoomModeration.Role.User
|
||||
is RoomMember.Role.Owner -> RoomModeration.Role.Administrator // TODO - distinguish creator from admin
|
||||
RoomMember.Role.Admin -> RoomModeration.Role.Administrator
|
||||
RoomMember.Role.Moderator -> RoomModeration.Role.Moderator
|
||||
RoomMember.Role.User -> RoomModeration.Role.User
|
||||
}
|
||||
|
||||
internal fun analyticsMemberRoleForPowerLevel(powerLevel: Long): RoomModeration.Role {
|
||||
|
||||
@@ -150,7 +150,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,
|
||||
@@ -178,8 +178,8 @@ fun aRoomMemberList() = persistentListOf(
|
||||
aWalter(),
|
||||
)
|
||||
|
||||
fun anAlice() = aRoomMember(UserId("@alice:server.org"), "Alice", role = RoomMember.Role.ADMIN)
|
||||
fun aBob() = aRoomMember(UserId("@bob:server.org"), "Bob", role = RoomMember.Role.MODERATOR)
|
||||
fun anAlice() = aRoomMember(UserId("@alice:server.org"), "Alice", role = RoomMember.Role.Admin)
|
||||
fun aBob() = aRoomMember(UserId("@bob:server.org"), "Bob", role = RoomMember.Role.Moderator)
|
||||
|
||||
fun aVictor() = aRoomMember(UserId("@victor:server.org"), "Victor", membership = RoomMembershipState.INVITE)
|
||||
|
||||
|
||||
@@ -61,7 +61,6 @@ import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.getBestName
|
||||
import io.element.android.libraries.matrix.api.room.isOwner
|
||||
import io.element.android.libraries.matrix.api.room.toMatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
@@ -295,14 +294,11 @@ private fun RoomMemberListItem(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val member = roomMemberWithIdentity.roomMember
|
||||
val roleText = if (member.isOwner()) {
|
||||
stringResource(R.string.screen_room_member_list_role_owner)
|
||||
} else {
|
||||
when (member.role) {
|
||||
RoomMember.Role.ADMIN -> stringResource(R.string.screen_room_member_list_role_administrator)
|
||||
RoomMember.Role.MODERATOR -> stringResource(R.string.screen_room_member_list_role_moderator)
|
||||
else -> null
|
||||
}
|
||||
val roleText = when (member.role) {
|
||||
RoomMember.Role.Admin -> stringResource(R.string.screen_room_member_list_role_administrator)
|
||||
RoomMember.Role.Moderator -> stringResource(R.string.screen_room_member_list_role_moderator)
|
||||
is RoomMember.Role.Owner -> stringResource(R.string.screen_room_member_list_role_owner)
|
||||
else -> null
|
||||
}
|
||||
|
||||
MatrixUserRow(
|
||||
|
||||
@@ -61,7 +61,7 @@ class RolesAndPermissionsNode @AssistedInject constructor(
|
||||
room.roomInfoFlow
|
||||
.filter { info ->
|
||||
val role = info.roleOf(room.sessionId)
|
||||
role != RoomMember.Role.ADMIN && role != RoomMember.Role.CREATOR
|
||||
role != RoomMember.Role.Admin && role !is RoomMember.Role.Owner
|
||||
}
|
||||
.take(1)
|
||||
.onEach { navigateUp() }
|
||||
|
||||
@@ -26,6 +26,7 @@ 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.activeRoomMembers
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
|
||||
import io.element.android.libraries.matrix.ui.model.roleOf
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -50,14 +51,23 @@ class RolesAndPermissionsPresenter @Inject constructor(
|
||||
}
|
||||
val moderatorCount by remember {
|
||||
derivedStateOf {
|
||||
roomInfo.userCountWithRole(activeRoomMemberIds, RoomMember.Role.MODERATOR)
|
||||
roomInfo.userCountWithRole(activeRoomMemberIds, RoomMember.Role.Moderator)
|
||||
}
|
||||
}
|
||||
val adminCount by remember {
|
||||
derivedStateOf {
|
||||
roomInfo.userCountWithRole(activeRoomMemberIds, RoomMember.Role.ADMIN)
|
||||
val admins = roomInfo.userCountWithRole(activeRoomMemberIds, RoomMember.Role.Admin)
|
||||
val ownersCount = if (roomInfo.privilegedCreatorRole) {
|
||||
val superAdmins = roomInfo.userCountWithRole(activeRoomMemberIds, RoomMember.Role.Owner(isCreator = false))
|
||||
val creators = roomInfo.userCountWithRole(activeRoomMemberIds, RoomMember.Role.Owner(isCreator = true))
|
||||
superAdmins + creators
|
||||
} else {
|
||||
0
|
||||
}
|
||||
admins + ownersCount
|
||||
}
|
||||
}
|
||||
val canDemoteSelf = remember { derivedStateOf { roomInfo.roleOf(room.sessionId) !is RoomMember.Role.Owner } }
|
||||
val changeOwnRoleAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||
val resetPermissionsAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||
|
||||
@@ -83,8 +93,10 @@ class RolesAndPermissionsPresenter @Inject constructor(
|
||||
}
|
||||
|
||||
return RolesAndPermissionsState(
|
||||
roomSupportsOwnerRole = roomInfo.privilegedCreatorRole,
|
||||
adminCount = adminCount,
|
||||
moderatorCount = moderatorCount,
|
||||
canDemoteSelf = canDemoteSelf.value,
|
||||
changeOwnRoleAction = changeOwnRoleAction.value,
|
||||
resetPermissionsAction = resetPermissionsAction.value,
|
||||
eventSink = { handleEvent(it) },
|
||||
|
||||
@@ -10,8 +10,10 @@ package io.element.android.features.roomdetails.impl.rolesandpermissions
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
|
||||
data class RolesAndPermissionsState(
|
||||
val roomSupportsOwnerRole: Boolean,
|
||||
val adminCount: Int,
|
||||
val moderatorCount: Int,
|
||||
val canDemoteSelf: Boolean,
|
||||
val changeOwnRoleAction: AsyncAction<Unit>,
|
||||
val resetPermissionsAction: AsyncAction<Unit>,
|
||||
val eventSink: (RolesAndPermissionsEvents) -> Unit,
|
||||
|
||||
@@ -13,7 +13,7 @@ import io.element.android.libraries.architecture.AsyncAction
|
||||
class RolesAndPermissionsStateProvider : PreviewParameterProvider<RolesAndPermissionsState> {
|
||||
override val values: Sequence<RolesAndPermissionsState>
|
||||
get() = sequenceOf(
|
||||
aRolesAndPermissionsState(),
|
||||
aRolesAndPermissionsState(roomSupportsOwners = false),
|
||||
aRolesAndPermissionsState(adminCount = 1, moderatorCount = 2),
|
||||
aRolesAndPermissionsState(
|
||||
adminCount = 1,
|
||||
@@ -45,17 +45,22 @@ class RolesAndPermissionsStateProvider : PreviewParameterProvider<RolesAndPermis
|
||||
moderatorCount = 2,
|
||||
resetPermissionsAction = AsyncAction.Failure(IllegalStateException("Failed to reset permissions")),
|
||||
),
|
||||
aRolesAndPermissionsState(canDemoteSelf = false),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aRolesAndPermissionsState(
|
||||
roomSupportsOwners: Boolean = true,
|
||||
adminCount: Int = 0,
|
||||
moderatorCount: Int = 0,
|
||||
canDemoteSelf: Boolean = true,
|
||||
changeOwnRoleAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
resetPermissionsAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
eventSink: (RolesAndPermissionsEvents) -> Unit = {},
|
||||
) = RolesAndPermissionsState(
|
||||
roomSupportsOwnerRole = roomSupportsOwners,
|
||||
adminCount = adminCount,
|
||||
canDemoteSelf = canDemoteSelf,
|
||||
moderatorCount = moderatorCount,
|
||||
changeOwnRoleAction = changeOwnRoleAction,
|
||||
resetPermissionsAction = resetPermissionsAction,
|
||||
|
||||
@@ -55,8 +55,14 @@ fun RolesAndPermissionsView(
|
||||
onBackClick = rolesAndPermissionsNavigator::onBackClick,
|
||||
) {
|
||||
ListSectionHeader(title = stringResource(R.string.screen_room_roles_and_permissions_roles_header), hasDivider = false)
|
||||
|
||||
val adminsTitle = if (state.roomSupportsOwnerRole) {
|
||||
stringResource(R.string.screen_room_roles_and_permissions_admins_and_owners)
|
||||
} else {
|
||||
stringResource(R.string.screen_room_roles_and_permissions_admins)
|
||||
}
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_admins)) },
|
||||
headlineContent = { Text(adminsTitle) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Admin())),
|
||||
trailingContent = ListItemContent.Text("${state.adminCount}"),
|
||||
onClick = { rolesAndPermissionsNavigator.openAdminList() },
|
||||
@@ -67,11 +73,13 @@ fun RolesAndPermissionsView(
|
||||
trailingContent = ListItemContent.Text("${state.moderatorCount}"),
|
||||
onClick = { rolesAndPermissionsNavigator.openModeratorList() },
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_change_my_role)) },
|
||||
onClick = { state.eventSink(RolesAndPermissionsEvents.ChangeOwnRole) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Edit()))
|
||||
)
|
||||
if (state.canDemoteSelf) {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_change_my_role)) },
|
||||
onClick = { state.eventSink(RolesAndPermissionsEvents.ChangeOwnRole) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Edit()))
|
||||
)
|
||||
}
|
||||
ListSectionHeader(title = stringResource(R.string.screen_room_roles_and_permissions_permissions_header), hasDivider = true)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_room_details)) },
|
||||
@@ -170,7 +178,7 @@ private fun ChangeOwnRoleBottomSheet(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_change_role_demote_to_moderator)) },
|
||||
onClick = {
|
||||
sheetState.hide(coroutineScope) {
|
||||
eventSink(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.MODERATOR))
|
||||
eventSink(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.Moderator))
|
||||
}
|
||||
},
|
||||
style = ListItemStyle.Destructive,
|
||||
@@ -179,7 +187,7 @@ private fun ChangeOwnRoleBottomSheet(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_change_role_demote_to_member)) },
|
||||
onClick = {
|
||||
sheetState.hide(coroutineScope) {
|
||||
eventSink(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.USER))
|
||||
eventSink(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.User))
|
||||
}
|
||||
},
|
||||
style = ListItemStyle.Destructive,
|
||||
|
||||
@@ -44,8 +44,8 @@ class ChangeRolesNode @AssistedInject constructor(
|
||||
|
||||
private val presenter = presenterFactory.run {
|
||||
val role = when (inputs.listType) {
|
||||
is ListType.Admins -> RoomMember.Role.ADMIN
|
||||
is ListType.Moderators -> RoomMember.Role.MODERATOR
|
||||
is ListType.Admins -> RoomMember.Role.Admin
|
||||
is ListType.Moderators -> RoomMember.Role.Moderator
|
||||
}
|
||||
create(role)
|
||||
}
|
||||
|
||||
@@ -75,18 +75,17 @@ class ChangeRolesPresenter @AssistedInject constructor(
|
||||
val exitState: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
val saveState: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
val usersWithRole = produceState(initialValue = persistentListOf()) {
|
||||
room.usersWithRole(role)
|
||||
.map { members -> members.map { it.toMatrixUser() } }
|
||||
.onEach { users ->
|
||||
val previous: PersistentList<MatrixUser> = value
|
||||
value = users.toPersistentList()
|
||||
// Users who were selected but didn't have the role, so their role change was pending
|
||||
val toAdd = selectedUsers.value.filter { user -> users.none { it.userId == user.userId } && previous.none { it.userId == user.userId } }
|
||||
// Users who no longer have the role
|
||||
val toRemove = previous.filter { user -> users.none { it.userId == user.userId } }.toSet()
|
||||
selectedUsers.value = (users + toAdd - toRemove).toImmutableList()
|
||||
}
|
||||
.launchIn(this)
|
||||
room.usersWithRole(role).map { members -> members.map { it.toMatrixUser() } }
|
||||
.onEach { users ->
|
||||
val previous: PersistentList<MatrixUser> = value
|
||||
value = users.toPersistentList()
|
||||
// Users who were selected but didn't have the role, so their role change was pending
|
||||
val toAdd = selectedUsers.value.filter { user -> users.none { it.userId == user.userId } && previous.none { it.userId == user.userId } }
|
||||
// Users who no longer have the role
|
||||
val toRemove = previous.filter { user -> users.none { it.userId == user.userId } }.toSet()
|
||||
selectedUsers.value = (users + toAdd - toRemove).toImmutableList()
|
||||
}
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
val roomMemberState by room.membersStateFlow.collectAsState()
|
||||
@@ -97,7 +96,6 @@ class ChangeRolesPresenter @AssistedInject constructor(
|
||||
.search(query.orEmpty())
|
||||
.groupedByRole()
|
||||
|
||||
println(results)
|
||||
searchResults = if (results.isEmpty()) {
|
||||
SearchBarResultState.NoResultsFound()
|
||||
} else {
|
||||
@@ -109,9 +107,10 @@ class ChangeRolesPresenter @AssistedInject constructor(
|
||||
|
||||
val roomInfo by room.roomInfoFlow.collectAsState()
|
||||
fun canChangeMemberRole(userId: UserId): Boolean {
|
||||
// An admin can't remove or demote another admin
|
||||
val role = roomInfo.roleOf(userId)
|
||||
return role !in listOf(RoomMember.Role.ADMIN, RoomMember.Role.CREATOR)
|
||||
// This is used to group the
|
||||
val currentUserRole = roomInfo.roleOf(room.sessionId)
|
||||
val otherUserRole = roomInfo.roleOf(userId)
|
||||
return currentUserRole.powerLevel > otherUserRole.powerLevel
|
||||
}
|
||||
|
||||
fun handleEvent(event: ChangeRolesEvent) {
|
||||
@@ -133,11 +132,21 @@ class ChangeRolesPresenter @AssistedInject constructor(
|
||||
selectedUsers.value = newList.toImmutableList()
|
||||
}
|
||||
is ChangeRolesEvent.Save -> {
|
||||
if (role == RoomMember.Role.ADMIN && selectedUsers != usersWithRole && !saveState.value.isConfirming()) {
|
||||
// Confirm adding admin
|
||||
saveState.value = AsyncAction.ConfirmingNoParams
|
||||
} else if (!saveState.value.isLoading()) {
|
||||
coroutineScope.save(usersWithRole.value, selectedUsers, saveState)
|
||||
val currentUserIsAdmin = roomInfo.roleOf(room.sessionId) == RoomMember.Role.Admin
|
||||
val isModifyingAdmins = role == RoomMember.Role.Admin
|
||||
val hasChanges = selectedUsers != usersWithRole
|
||||
val isConfirming = saveState.value.isConfirming()
|
||||
|
||||
val needsConfirmation = currentUserIsAdmin && isModifyingAdmins && hasChanges && !isConfirming
|
||||
|
||||
when {
|
||||
needsConfirmation -> {
|
||||
// Confirm modifying users
|
||||
saveState.value = AsyncAction.ConfirmingNoParams
|
||||
}
|
||||
!saveState.value.isLoading() -> {
|
||||
coroutineScope.save(usersWithRole.value, selectedUsers, saveState)
|
||||
}
|
||||
}
|
||||
}
|
||||
is ChangeRolesEvent.ClearError -> {
|
||||
@@ -175,10 +184,12 @@ class ChangeRolesPresenter @AssistedInject constructor(
|
||||
}
|
||||
|
||||
private fun List<RoomMember>.groupedByRole(): MembersByRole {
|
||||
val groupedMembers = MembersByRole(this)
|
||||
return MembersByRole(
|
||||
admins = filter { it.role == RoomMember.Role.ADMIN }.sorted(),
|
||||
moderators = filter { it.role == RoomMember.Role.MODERATOR }.sorted(),
|
||||
members = filter { it.role == RoomMember.Role.USER }.sorted(),
|
||||
owners = groupedMembers.owners.sorted(),
|
||||
admins = groupedMembers.admins.sorted(),
|
||||
moderators = groupedMembers.moderators.sorted(),
|
||||
members = groupedMembers.members.sorted(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -203,7 +214,7 @@ class ChangeRolesPresenter @AssistedInject constructor(
|
||||
}
|
||||
for (selectedUser in toRemove) {
|
||||
analyticsService.capture(RoomModeration(RoomModeration.Action.ChangeMemberRole, RoomModeration.Role.User))
|
||||
add(UserRoleChange(selectedUser.userId, RoomMember.Role.USER))
|
||||
add(UserRoleChange(selectedUser.userId, RoomMember.Role.User))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,17 +30,19 @@ data class ChangeRolesState(
|
||||
)
|
||||
|
||||
data class MembersByRole(
|
||||
val owners: ImmutableList<RoomMember>,
|
||||
val admins: ImmutableList<RoomMember>,
|
||||
val moderators: ImmutableList<RoomMember>,
|
||||
val members: ImmutableList<RoomMember>,
|
||||
) {
|
||||
constructor(members: List<RoomMember>) : this(
|
||||
admins = members.filter { it.role == RoomMember.Role.ADMIN }.sorted(),
|
||||
moderators = members.filter { it.role == RoomMember.Role.MODERATOR }.sorted(),
|
||||
members = members.filter { it.role == RoomMember.Role.USER }.sorted(),
|
||||
owners = members.filter { it.role is RoomMember.Role.Owner }.sorted(),
|
||||
admins = members.filter { it.role == RoomMember.Role.Admin }.sorted(),
|
||||
moderators = members.filter { it.role == RoomMember.Role.Moderator }.sorted(),
|
||||
members = members.filter { it.role == RoomMember.Role.User }.sorted(),
|
||||
)
|
||||
|
||||
fun isEmpty() = admins.isEmpty() && moderators.isEmpty() && members.isEmpty()
|
||||
fun isEmpty() = owners.isEmpty() && admins.isEmpty() && moderators.isEmpty() && members.isEmpty()
|
||||
}
|
||||
|
||||
private fun Iterable<RoomMember>.sorted(): ImmutableList<RoomMember> {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
package io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||
import io.element.android.features.roomdetails.impl.members.aRoomMemberList
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
@@ -15,6 +16,7 @@ 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.RoomMembershipState
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
@@ -24,7 +26,7 @@ class ChangeRolesStateProvider : PreviewParameterProvider<ChangeRolesState> {
|
||||
override val values: Sequence<ChangeRolesState>
|
||||
get() = sequenceOf(
|
||||
aChangeRolesState(),
|
||||
aChangeRolesStateWithSelectedUsers().copy(role = RoomMember.Role.MODERATOR),
|
||||
aChangeRolesStateWithSelectedUsers().copy(role = RoomMember.Role.Moderator),
|
||||
aChangeRolesStateWithSelectedUsers().copy(hasPendingChanges = false),
|
||||
aChangeRolesStateWithSelectedUsers(),
|
||||
aChangeRolesStateWithSelectedUsers().copy(
|
||||
@@ -41,11 +43,12 @@ class ChangeRolesStateProvider : PreviewParameterProvider<ChangeRolesState> {
|
||||
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Loading),
|
||||
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Success(Unit)),
|
||||
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Failure(Exception("boom"))),
|
||||
aChangeRolesStateWithOwners(),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aChangeRolesState(
|
||||
role: RoomMember.Role = RoomMember.Role.ADMIN,
|
||||
role: RoomMember.Role = RoomMember.Role.Admin,
|
||||
query: String? = null,
|
||||
isSearchActive: Boolean = false,
|
||||
searchResults: SearchBarResultState<MembersByRole> = SearchBarResultState.NoResultsFound(),
|
||||
@@ -84,3 +87,47 @@ internal fun aChangeRolesStateWithSelectedUsers() = aChangeRolesState(
|
||||
hasPendingChanges = true,
|
||||
canRemoveMember = { it != UserId("@alice:server.org") },
|
||||
)
|
||||
|
||||
internal fun aChangeRolesStateWithOwners() = aChangeRolesState(
|
||||
role = RoomMember.Role.Admin,
|
||||
searchResults = SearchBarResultState.Results(
|
||||
MembersByRole(
|
||||
members = persistentListOf(
|
||||
aRoomMember(
|
||||
userId = UserId("@alice:server.org"),
|
||||
displayName = "Alice",
|
||||
role = RoomMember.Role.Owner(isCreator = true),
|
||||
),
|
||||
aRoomMember(
|
||||
userId = UserId("@bob:server.org"),
|
||||
displayName = "Bob",
|
||||
role = RoomMember.Role.Owner(isCreator = false),
|
||||
),
|
||||
aRoomMember(
|
||||
userId = UserId("@carol:server.org"),
|
||||
displayName = "Carol",
|
||||
role = RoomMember.Role.Admin,
|
||||
),
|
||||
aRoomMember(
|
||||
userId = UserId("@david:server.org"),
|
||||
displayName = "David",
|
||||
role = RoomMember.Role.User,
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
canRemoveMember = { userId ->
|
||||
when (userId) {
|
||||
UserId("@alice:server.org") -> false // Owner - creator
|
||||
UserId("@bob:server.org") -> false // Owner - super admin
|
||||
UserId("@carol:server.org") -> true // Admin
|
||||
UserId("@david:server.org") -> true // User
|
||||
else -> false
|
||||
}
|
||||
},
|
||||
selectedUsers = persistentListOf(
|
||||
aMatrixUser(id = "@alice:server.org", displayName = "Alice"),
|
||||
aMatrixUser(id = "@bob:server.org", displayName = "Bob"),
|
||||
aMatrixUser(id = "@carol:server.org", displayName = "Carol"),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -57,6 +57,7 @@ import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Checkbox
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBar
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
@@ -96,9 +97,9 @@ fun ChangeRolesView(
|
||||
AnimatedVisibility(visible = !state.isSearchActive) {
|
||||
TopAppBar(
|
||||
titleStr = when (state.role) {
|
||||
RoomMember.Role.ADMIN -> stringResource(R.string.screen_room_change_role_administrators_title)
|
||||
RoomMember.Role.MODERATOR -> stringResource(R.string.screen_room_change_role_moderators_title)
|
||||
RoomMember.Role.CREATOR, RoomMember.Role.USER -> error("This should never be reached")
|
||||
RoomMember.Role.Admin -> stringResource(R.string.screen_room_change_role_administrators_title)
|
||||
RoomMember.Role.Moderator -> stringResource(R.string.screen_room_change_role_moderators_title)
|
||||
is RoomMember.Role.Owner, RoomMember.Role.User -> error("This should never be reached")
|
||||
},
|
||||
navigationIcon = {
|
||||
BackButton(onClick = { state.eventSink(ChangeRolesEvent.Exit) })
|
||||
@@ -187,7 +188,7 @@ fun ChangeRolesView(
|
||||
|
||||
when (state.savingState) {
|
||||
is AsyncAction.Confirming -> {
|
||||
if (state.role == RoomMember.Role.ADMIN) {
|
||||
if (state.role == RoomMember.Role.Admin) {
|
||||
// Confirm adding new admins dialogs
|
||||
ConfirmationDialog(
|
||||
title = stringResource(R.string.screen_room_change_role_confirm_add_admin_title),
|
||||
@@ -234,10 +235,30 @@ private fun SearchResultsList(
|
||||
item {
|
||||
selectedUsersList(selectedUsers)
|
||||
}
|
||||
if (searchResults.owners.isNotEmpty()) {
|
||||
stickyHeader { ListSectionHeader(text = stringResource(R.string.screen_room_roles_and_permissions_owners)) }
|
||||
item {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 8.dp),
|
||||
text = stringResource(R.string.screen_room_change_role_moderators_owner_section_footer),
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
)
|
||||
}
|
||||
items(searchResults.owners, key = { it.userId }) { roomMember ->
|
||||
ListMemberItem(
|
||||
roomMember = roomMember,
|
||||
canRemoveMember = canRemoveMember,
|
||||
onToggleSelection = onToggleSelection,
|
||||
selectedUsers = selectedUsers
|
||||
)
|
||||
}
|
||||
}
|
||||
if (searchResults.admins.isNotEmpty()) {
|
||||
stickyHeader { ListSectionHeader(text = stringResource(R.string.screen_room_roles_and_permissions_admins)) }
|
||||
// Add a footer for the admin section in change role to moderator screen
|
||||
if (currentRole == RoomMember.Role.MODERATOR) {
|
||||
if (currentRole == RoomMember.Role.Moderator) {
|
||||
item {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
@@ -303,20 +324,24 @@ private fun ListMemberItem(
|
||||
) {
|
||||
val canToggle = canRemoveMember(roomMember.userId)
|
||||
val trailingContent: @Composable (() -> Unit) = {
|
||||
Checkbox(
|
||||
checked = selectedUsers.any { it.userId == roomMember.userId },
|
||||
onCheckedChange = { onToggleSelection(roomMember) },
|
||||
enabled = canToggle,
|
||||
)
|
||||
if (canToggle) {
|
||||
Checkbox(
|
||||
checked = selectedUsers.any { it.userId == roomMember.userId },
|
||||
onCheckedChange = { onToggleSelection(roomMember) },
|
||||
)
|
||||
}
|
||||
}
|
||||
Column {
|
||||
MemberRow(
|
||||
modifier = Modifier.clickable(enabled = canToggle, onClick = { onToggleSelection(roomMember) }),
|
||||
avatarData = roomMember.getAvatarData(size = AvatarSize.UserListItem),
|
||||
name = roomMember.getBestName(),
|
||||
userId = roomMember.userId.value.takeIf { roomMember.displayName?.isNotBlank() == true },
|
||||
isPending = roomMember.membership == RoomMembershipState.INVITE,
|
||||
trailingContent = trailingContent,
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
MemberRow(
|
||||
modifier = Modifier.clickable(enabled = canToggle, onClick = { onToggleSelection(roomMember) }),
|
||||
avatarData = roomMember.getAvatarData(size = AvatarSize.UserListItem),
|
||||
name = roomMember.getBestName(),
|
||||
userId = roomMember.userId.value.takeIf { roomMember.displayName?.isNotBlank() == true },
|
||||
isPending = roomMember.membership == RoomMembershipState.INVITE,
|
||||
trailingContent = trailingContent,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -55,15 +55,15 @@ internal fun aChangeRoomPermissionsState(
|
||||
private fun previewPermissions(): RoomPowerLevelsValues {
|
||||
return RoomPowerLevelsValues(
|
||||
// MembershipModeration section
|
||||
invite = RoomMember.Role.ADMIN.powerLevel,
|
||||
kick = RoomMember.Role.MODERATOR.powerLevel,
|
||||
ban = RoomMember.Role.USER.powerLevel,
|
||||
invite = RoomMember.Role.Admin.powerLevel,
|
||||
kick = RoomMember.Role.Moderator.powerLevel,
|
||||
ban = RoomMember.Role.User.powerLevel,
|
||||
// MessagesAndContent section
|
||||
redactEvents = RoomMember.Role.MODERATOR.powerLevel,
|
||||
sendEvents = RoomMember.Role.ADMIN.powerLevel,
|
||||
redactEvents = RoomMember.Role.Moderator.powerLevel,
|
||||
sendEvents = RoomMember.Role.Admin.powerLevel,
|
||||
// RoomDetails section
|
||||
roomName = RoomMember.Role.ADMIN.powerLevel,
|
||||
roomAvatar = RoomMember.Role.MODERATOR.powerLevel,
|
||||
roomTopic = RoomMember.Role.USER.powerLevel,
|
||||
roomName = RoomMember.Role.Admin.powerLevel,
|
||||
roomAvatar = RoomMember.Role.Moderator.powerLevel,
|
||||
roomTopic = RoomMember.Role.User.powerLevel,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -80,21 +80,21 @@ fun ChangeRoomPermissionsView(
|
||||
ListSectionHeader(titleForSection(item = permissionItem), hasDivider = index > 0)
|
||||
SelectRoleItem(
|
||||
permissionsItem = permissionItem,
|
||||
role = RoomMember.Role.ADMIN,
|
||||
role = RoomMember.Role.Admin,
|
||||
currentPermissions = state.currentPermissions
|
||||
) { item, role ->
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(item, role))
|
||||
}
|
||||
SelectRoleItem(
|
||||
permissionsItem = permissionItem,
|
||||
role = RoomMember.Role.MODERATOR,
|
||||
role = RoomMember.Role.Moderator,
|
||||
currentPermissions = state.currentPermissions
|
||||
) { item, role ->
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(item, role))
|
||||
}
|
||||
SelectRoleItem(
|
||||
permissionsItem = permissionItem,
|
||||
role = RoomMember.Role.USER,
|
||||
role = RoomMember.Role.User,
|
||||
currentPermissions = state.currentPermissions
|
||||
) { item, role ->
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(item, role))
|
||||
@@ -135,9 +135,10 @@ private fun SelectRoleItem(
|
||||
onClick: (RoomPermissionType, RoomMember.Role) -> Unit
|
||||
) {
|
||||
val title = when (role) {
|
||||
RoomMember.Role.ADMIN, RoomMember.Role.CREATOR -> stringResource(R.string.screen_room_change_permissions_administrators)
|
||||
RoomMember.Role.MODERATOR -> stringResource(R.string.screen_room_change_permissions_moderators)
|
||||
RoomMember.Role.USER -> stringResource(R.string.screen_room_change_permissions_everyone)
|
||||
RoomMember.Role.Admin -> stringResource(R.string.screen_room_change_permissions_administrators)
|
||||
RoomMember.Role.Moderator -> stringResource(R.string.screen_room_change_permissions_moderators)
|
||||
RoomMember.Role.User -> stringResource(R.string.screen_room_change_permissions_everyone)
|
||||
else -> error("Unsupported role selected: $role")
|
||||
}
|
||||
ListItem(
|
||||
headlineContent = { Text(text = title) },
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
<string name="screen_room_change_role_invited_member_name">"%1$s (Pending)"</string>
|
||||
<string name="screen_room_change_role_invited_member_name_android">"(Pending)"</string>
|
||||
<string name="screen_room_change_role_moderators_admin_section_footer">"Admins automatically have moderator privileges"</string>
|
||||
<string name="screen_room_change_role_moderators_owner_section_footer">"Owners automatically have admin privileges."</string>
|
||||
<string name="screen_room_change_role_moderators_title">"Edit Moderators"</string>
|
||||
<string name="screen_room_change_role_section_administrators">"Admins"</string>
|
||||
<string name="screen_room_change_role_section_moderators">"Moderators"</string>
|
||||
@@ -99,12 +100,14 @@
|
||||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Mentions and Keywords only"</string>
|
||||
<string name="screen_room_notification_settings_room_custom_settings_title">"In this room, notify me for"</string>
|
||||
<string name="screen_room_roles_and_permissions_admins">"Admins"</string>
|
||||
<string name="screen_room_roles_and_permissions_admins_and_owners">"Admins and owners"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_my_role">"Change my role"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_member">"Demote to member"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_moderator">"Demote to moderator"</string>
|
||||
<string name="screen_room_roles_and_permissions_member_moderation">"Member moderation"</string>
|
||||
<string name="screen_room_roles_and_permissions_messages_and_content">"Messages and content"</string>
|
||||
<string name="screen_room_roles_and_permissions_moderators">"Moderators"</string>
|
||||
<string name="screen_room_roles_and_permissions_owners">"Owners"</string>
|
||||
<string name="screen_room_roles_and_permissions_permissions_header">"Permissions"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset">"Reset permissions"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_description">"Once you reset permissions, you will lose the current settings."</string>
|
||||
|
||||
@@ -66,7 +66,7 @@ class RolesAndPermissionPresenterTest {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.MODERATOR))
|
||||
initialState.eventSink(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.Moderator))
|
||||
|
||||
runCurrent()
|
||||
assertThat(awaitItem().changeOwnRoleAction).isEqualTo(AsyncAction.Loading)
|
||||
@@ -87,7 +87,7 @@ class RolesAndPermissionPresenterTest {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.MODERATOR))
|
||||
initialState.eventSink(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.Moderator))
|
||||
|
||||
runCurrent()
|
||||
assertThat(awaitItem().changeOwnRoleAction).isEqualTo(AsyncAction.Loading)
|
||||
|
||||
@@ -47,12 +47,30 @@ class RolesAndPermissionsViewTest {
|
||||
fun `tapping on Admins opens admin list`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setRolesAndPermissionsView(
|
||||
aRolesAndPermissionsState(
|
||||
roomSupportsOwners = false,
|
||||
eventSink = EventsRecorder(expectEvents = false)
|
||||
),
|
||||
openAdminList = callback,
|
||||
)
|
||||
rule.clickOn(R.string.screen_room_roles_and_permissions_admins)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `tapping on Admins and Owners opens admin list`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setRolesAndPermissionsView(
|
||||
aRolesAndPermissionsState(
|
||||
roomSupportsOwners = true,
|
||||
eventSink = EventsRecorder(expectEvents = false)
|
||||
),
|
||||
openAdminList = callback,
|
||||
)
|
||||
rule.clickOn(R.string.screen_room_roles_and_permissions_admins_and_owners)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `tapping on Moderators opens moderators list`() {
|
||||
ensureCalledOnce { callback ->
|
||||
@@ -126,7 +144,7 @@ class RolesAndPermissionsViewTest {
|
||||
)
|
||||
rule.clickOn(R.string.screen_room_roles_and_permissions_change_role_demote_to_moderator)
|
||||
rule.mainClock.advanceTimeBy(1_000L)
|
||||
recorder.assertSingle(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.MODERATOR))
|
||||
recorder.assertSingle(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.Moderator))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -140,7 +158,7 @@ class RolesAndPermissionsViewTest {
|
||||
)
|
||||
rule.clickOn(R.string.screen_room_roles_and_permissions_change_role_demote_to_member)
|
||||
rule.mainClock.advanceTimeBy(1_000L)
|
||||
recorder.assertSingle(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.USER))
|
||||
recorder.assertSingle(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.User))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -160,6 +178,7 @@ class RolesAndPermissionsViewTest {
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRolesAndPermissionsView(
|
||||
state: RolesAndPermissionsState = aRolesAndPermissionsState(
|
||||
roomSupportsOwners = false,
|
||||
eventSink = EventsRecorder(expectEvents = false),
|
||||
),
|
||||
goBack: () -> Unit = EnsureNeverCalled(),
|
||||
|
||||
@@ -12,6 +12,7 @@ import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.RoomModeration
|
||||
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||
import io.element.android.features.roomdetails.impl.members.aRoomMemberList
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
@@ -23,6 +24,7 @@ import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_3
|
||||
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
|
||||
@@ -43,7 +45,7 @@ class ChangeRolesPresenterTest {
|
||||
presenter.present()
|
||||
}.test {
|
||||
with(awaitItem()) {
|
||||
assertThat(role).isEqualTo(RoomMember.Role.ADMIN)
|
||||
assertThat(role).isEqualTo(RoomMember.Role.Admin)
|
||||
assertThat(query).isNull()
|
||||
assertThat(isSearchActive).isFalse()
|
||||
assertThat(searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
|
||||
@@ -70,6 +72,76 @@ class ChangeRolesPresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - canChangeRole of users with lower power level unless they are owners`() = runTest {
|
||||
val creatorUserId = UserId("@creator:matrix.org")
|
||||
val superAdminUserId = UserId("@super_admin:matrix.org")
|
||||
|
||||
val room = FakeJoinedRoom().apply {
|
||||
// User is a creator, so they can change roles of other members. So is `creatorUserId`.
|
||||
givenRoomInfo(
|
||||
aRoomInfo(
|
||||
roomCreators = listOf(sessionId, creatorUserId),
|
||||
roomPowerLevels = RoomPowerLevels(
|
||||
defaultRoomPowerLevelValues(),
|
||||
users = persistentMapOf(
|
||||
// bob is Admin
|
||||
A_USER_ID_2 to RoomMember.Role.Admin.powerLevel,
|
||||
// carol is Moderator
|
||||
A_USER_ID_3 to RoomMember.Role.Moderator.powerLevel,
|
||||
// super_admin is Owner - Superadmin
|
||||
superAdminUserId to RoomMember.Role.Owner(isCreator = false).powerLevel,
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val roomMemberList = aRoomMemberList() + listOf(
|
||||
// Owner - superadmin
|
||||
aRoomMember(userId = superAdminUserId, role = RoomMember.Role.Owner(isCreator = true)),
|
||||
// Owner - creator
|
||||
aRoomMember(userId = creatorUserId, role = RoomMember.Role.Owner(isCreator = true))
|
||||
)
|
||||
givenRoomMembersState(RoomMembersState.Ready(roomMemberList.toPersistentList()))
|
||||
}
|
||||
val presenter = createChangeRolesPresenter(room = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
awaitItem().run {
|
||||
assertThat(canChangeMemberRole(A_USER_ID_2)).isTrue() // Admin
|
||||
assertThat(canChangeMemberRole(A_USER_ID_3)).isTrue() // Moderator
|
||||
assertThat(canChangeMemberRole(creatorUserId)).isFalse() // Owner
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - when modifying admins, creators are displayed too`() = runTest {
|
||||
val room = FakeJoinedRoom().apply {
|
||||
val creatorUserId = UserId("@creator:matrix.org")
|
||||
val memberList = aRoomMemberList()
|
||||
.plus(aRoomMember(displayName = "CREATOR", role = RoomMember.Role.Owner(isCreator = true), userId = creatorUserId))
|
||||
.toPersistentList()
|
||||
givenRoomInfo(aRoomInfo(roomCreators = listOf(creatorUserId)))
|
||||
givenRoomMembersState(RoomMembersState.Ready(memberList))
|
||||
}
|
||||
val presenter = createChangeRolesPresenter(room = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
awaitItem().searchResults.run {
|
||||
assertThat(this).isInstanceOf(SearchBarResultState.Results::class.java)
|
||||
val results = (this as SearchBarResultState.Results).results
|
||||
assertThat(results.admins).isNotEmpty()
|
||||
assertThat(results.owners).isNotEmpty()
|
||||
assertThat(results.owners.last().role).isEqualTo(RoomMember.Role.Owner(isCreator = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - ToggleSearchActive changes the value`() = runTest {
|
||||
val room = FakeJoinedRoom().apply {
|
||||
@@ -145,7 +217,7 @@ class ChangeRolesPresenterTest {
|
||||
fun `present - UserSelectionToggle adds and removes users from the selected user list`() = runTest {
|
||||
val room = FakeJoinedRoom().apply {
|
||||
givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
|
||||
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.ADMIN)))
|
||||
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.Admin)))
|
||||
}
|
||||
val presenter = createChangeRolesPresenter(room = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
@@ -167,7 +239,7 @@ class ChangeRolesPresenterTest {
|
||||
fun `present - hasPendingChanges is true when the initial selected users don't match the new ones`() = runTest {
|
||||
val room = FakeJoinedRoom().apply {
|
||||
givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
|
||||
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.ADMIN)))
|
||||
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.Admin)))
|
||||
}
|
||||
val presenter = createChangeRolesPresenter(room = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
@@ -196,7 +268,7 @@ class ChangeRolesPresenterTest {
|
||||
fun `present - Exit will display success if no pending changes`() = runTest {
|
||||
val room = FakeJoinedRoom().apply {
|
||||
givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
|
||||
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.ADMIN)))
|
||||
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.Admin)))
|
||||
}
|
||||
val presenter = createChangeRolesPresenter(room = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
@@ -216,7 +288,7 @@ class ChangeRolesPresenterTest {
|
||||
fun `present - CancelExit will remove exit confirmation`() = runTest {
|
||||
val room = FakeJoinedRoom().apply {
|
||||
givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
|
||||
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.ADMIN)))
|
||||
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.Admin)))
|
||||
}
|
||||
val presenter = createChangeRolesPresenter(room = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
@@ -242,7 +314,7 @@ class ChangeRolesPresenterTest {
|
||||
fun `present - Exit will display a confirmation dialog if there are pending changes, calling it again will actually exit`() = runTest {
|
||||
val room = FakeJoinedRoom().apply {
|
||||
givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
|
||||
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.ADMIN)))
|
||||
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.Admin)))
|
||||
}
|
||||
val presenter = createChangeRolesPresenter(room = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
@@ -273,9 +345,9 @@ class ChangeRolesPresenterTest {
|
||||
baseRoom = FakeBaseRoom(updateMembersResult = { Result.success(Unit) }),
|
||||
).apply {
|
||||
givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
|
||||
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.ADMIN)))
|
||||
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.Admin)))
|
||||
}
|
||||
val presenter = createChangeRolesPresenter(role = RoomMember.Role.ADMIN, room = room)
|
||||
val presenter = createChangeRolesPresenter(role = RoomMember.Role.Admin, room = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
@@ -302,9 +374,9 @@ class ChangeRolesPresenterTest {
|
||||
fun `present - CancelSave will remove the confirmation dialog`() = runTest {
|
||||
val room = FakeJoinedRoom().apply {
|
||||
givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
|
||||
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.ADMIN)))
|
||||
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.Admin)))
|
||||
}
|
||||
val presenter = createChangeRolesPresenter(role = RoomMember.Role.ADMIN, room = room)
|
||||
val presenter = createChangeRolesPresenter(role = RoomMember.Role.Admin, room = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
@@ -331,10 +403,10 @@ class ChangeRolesPresenterTest {
|
||||
baseRoom = FakeBaseRoom(updateMembersResult = { Result.success(Unit) }),
|
||||
).apply {
|
||||
givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
|
||||
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.MODERATOR)))
|
||||
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.Moderator)))
|
||||
}
|
||||
val presenter = createChangeRolesPresenter(
|
||||
role = RoomMember.Role.MODERATOR,
|
||||
role = RoomMember.Role.Moderator,
|
||||
room = room,
|
||||
analyticsService = analyticsService
|
||||
)
|
||||
@@ -358,15 +430,55 @@ class ChangeRolesPresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - Save will just save the changes if the current user is a room creator and the selected users are not`() = runTest {
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val room = FakeJoinedRoom(
|
||||
updateUserRoleResult = { Result.success(Unit) },
|
||||
baseRoom = FakeBaseRoom(updateMembersResult = { Result.success(Unit) }),
|
||||
).apply {
|
||||
givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
|
||||
givenRoomInfo(
|
||||
aRoomInfo(
|
||||
roomCreators = listOf(sessionId),
|
||||
roomPowerLevels = roomPowerLevelsWithRole(role = RoomMember.Role.Admin, userId = A_USER_ID_2)
|
||||
)
|
||||
)
|
||||
}
|
||||
val presenter = createChangeRolesPresenter(
|
||||
role = RoomMember.Role.Admin,
|
||||
room = room,
|
||||
analyticsService = analyticsService
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.selectedUsers).hasSize(1)
|
||||
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(MatrixUser(A_USER_ID_2)))
|
||||
|
||||
awaitItem().eventSink(ChangeRolesEvent.Save)
|
||||
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.savingState).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
skipItems(1)
|
||||
|
||||
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Success(Unit))
|
||||
assertThat(analyticsService.capturedEvents.last()).isEqualTo(RoomModeration(RoomModeration.Action.ChangeMemberRole, RoomModeration.Role.User))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - Save can handle failures and ClearError clears them`() = runTest {
|
||||
val room = FakeJoinedRoom(
|
||||
updateUserRoleResult = { Result.failure(IllegalStateException("Failed")) }
|
||||
).apply {
|
||||
givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
|
||||
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.MODERATOR)))
|
||||
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(role = RoomMember.Role.Moderator, userId = A_USER_ID)))
|
||||
}
|
||||
val presenter = createChangeRolesPresenter(role = RoomMember.Role.MODERATOR, room = room)
|
||||
val presenter = createChangeRolesPresenter(role = RoomMember.Role.Moderator, room = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
@@ -399,7 +511,7 @@ class ChangeRolesPresenterTest {
|
||||
}
|
||||
|
||||
private fun TestScope.createChangeRolesPresenter(
|
||||
role: RoomMember.Role = RoomMember.Role.ADMIN,
|
||||
role: RoomMember.Role = RoomMember.Role.Admin,
|
||||
room: FakeJoinedRoom = FakeJoinedRoom(),
|
||||
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
||||
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
|
||||
|
||||
@@ -41,11 +41,25 @@ class ChangeRolesViewTest {
|
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `passing a 'USER' role throws an exception`() {
|
||||
fun `passing a 'User' role throws an exception`() {
|
||||
val exception = runCatchingExceptions {
|
||||
rule.setChangeRolesContent(
|
||||
state = aChangeRolesState(
|
||||
role = RoomMember.Role.USER,
|
||||
role = RoomMember.Role.User,
|
||||
eventSink = EnsureNeverCalledWithParam(),
|
||||
),
|
||||
)
|
||||
}.exceptionOrNull()
|
||||
|
||||
assertThat(exception).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `passing an 'Owner' role throws an exception`() {
|
||||
val exception = runCatchingExceptions {
|
||||
rule.setChangeRolesContent(
|
||||
state = aChangeRolesState(
|
||||
role = RoomMember.Role.Owner(isCreator = true),
|
||||
eventSink = EnsureNeverCalledWithParam(),
|
||||
),
|
||||
)
|
||||
@@ -166,7 +180,7 @@ class ChangeRolesViewTest {
|
||||
val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
|
||||
rule.setChangeRolesContent(
|
||||
state = aChangeRolesState(
|
||||
role = RoomMember.Role.ADMIN,
|
||||
role = RoomMember.Role.Admin,
|
||||
isSearchActive = true,
|
||||
savingState = AsyncAction.ConfirmingNoParams,
|
||||
eventSink = eventsRecorder,
|
||||
@@ -183,7 +197,7 @@ class ChangeRolesViewTest {
|
||||
val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
|
||||
rule.setChangeRolesContent(
|
||||
state = aChangeRolesState(
|
||||
role = RoomMember.Role.ADMIN,
|
||||
role = RoomMember.Role.Admin,
|
||||
isSearchActive = true,
|
||||
savingState = AsyncAction.ConfirmingNoParams,
|
||||
eventSink = eventsRecorder,
|
||||
|
||||
@@ -14,6 +14,8 @@ import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_3
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_4
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_5
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_6
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_7
|
||||
import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.junit.Test
|
||||
@@ -22,22 +24,28 @@ class MembersByRoleTest {
|
||||
@Test
|
||||
fun `constructor - with single member list categorizes and sorts members`() {
|
||||
val members = listOf(
|
||||
aRoomMember(A_USER_ID_2, displayName = "Bob", role = RoomMember.Role.ADMIN),
|
||||
aRoomMember(A_USER_ID, displayName = "Alice", role = RoomMember.Role.ADMIN),
|
||||
aRoomMember(A_USER_ID_3, displayName = "Carol", role = RoomMember.Role.USER),
|
||||
aRoomMember(A_USER_ID_5, displayName = "Eve", role = RoomMember.Role.USER),
|
||||
aRoomMember(A_USER_ID_4, displayName = "David", role = RoomMember.Role.USER),
|
||||
aRoomMember(A_USER_ID_2, displayName = "Bob", role = RoomMember.Role.Admin),
|
||||
aRoomMember(A_USER_ID, displayName = "Alice", role = RoomMember.Role.Admin),
|
||||
aRoomMember(A_USER_ID_3, displayName = "Carol", role = RoomMember.Role.User),
|
||||
aRoomMember(A_USER_ID_5, displayName = "Eve", role = RoomMember.Role.User),
|
||||
aRoomMember(A_USER_ID_4, displayName = "David", role = RoomMember.Role.User),
|
||||
aRoomMember(A_USER_ID_6, displayName = "Justin", role = RoomMember.Role.Owner(isCreator = true)),
|
||||
aRoomMember(A_USER_ID_7, displayName = "Mallory", role = RoomMember.Role.Owner(isCreator = false)),
|
||||
)
|
||||
val membersByRole = MembersByRole(members = members)
|
||||
assertThat(membersByRole.owners).containsExactly(
|
||||
aRoomMember(A_USER_ID_6, displayName = "Justin", role = RoomMember.Role.Owner(isCreator = true)),
|
||||
aRoomMember(A_USER_ID_7, displayName = "Mallory", role = RoomMember.Role.Owner(isCreator = false)),
|
||||
)
|
||||
assertThat(membersByRole.admins).containsExactly(
|
||||
aRoomMember(A_USER_ID, displayName = "Alice", role = RoomMember.Role.ADMIN),
|
||||
aRoomMember(A_USER_ID_2, displayName = "Bob", role = RoomMember.Role.ADMIN),
|
||||
aRoomMember(A_USER_ID, displayName = "Alice", role = RoomMember.Role.Admin),
|
||||
aRoomMember(A_USER_ID_2, displayName = "Bob", role = RoomMember.Role.Admin),
|
||||
)
|
||||
assertThat(membersByRole.moderators).isEmpty()
|
||||
assertThat(membersByRole.members).containsExactly(
|
||||
aRoomMember(A_USER_ID_3, displayName = "Carol", role = RoomMember.Role.USER),
|
||||
aRoomMember(A_USER_ID_4, displayName = "David", role = RoomMember.Role.USER),
|
||||
aRoomMember(A_USER_ID_5, displayName = "Eve", role = RoomMember.Role.USER),
|
||||
aRoomMember(A_USER_ID_3, displayName = "Carol", role = RoomMember.Role.User),
|
||||
aRoomMember(A_USER_ID_4, displayName = "David", role = RoomMember.Role.User),
|
||||
aRoomMember(A_USER_ID_5, displayName = "Eve", role = RoomMember.Role.User),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -46,24 +54,35 @@ class MembersByRoleTest {
|
||||
val emptyMembersByRole = MembersByRole(emptyList())
|
||||
assertThat(emptyMembersByRole.isEmpty()).isTrue()
|
||||
|
||||
val membersByRoleWithOwners = MembersByRole(
|
||||
owners = persistentListOf(aRoomMember(A_USER_ID, role = RoomMember.Role.Admin)),
|
||||
admins = persistentListOf(),
|
||||
moderators = persistentListOf(),
|
||||
members = persistentListOf(),
|
||||
)
|
||||
assertThat(membersByRoleWithOwners.isEmpty()).isFalse()
|
||||
|
||||
val membersByRoleWithAdmins = MembersByRole(
|
||||
admins = persistentListOf(aRoomMember(A_USER_ID, role = RoomMember.Role.ADMIN)),
|
||||
owners = persistentListOf(),
|
||||
admins = persistentListOf(aRoomMember(A_USER_ID, role = RoomMember.Role.Admin)),
|
||||
moderators = persistentListOf(),
|
||||
members = persistentListOf(),
|
||||
)
|
||||
assertThat(membersByRoleWithAdmins.isEmpty()).isFalse()
|
||||
|
||||
val membersByRoleWithModerators = MembersByRole(
|
||||
owners = persistentListOf(),
|
||||
admins = persistentListOf(),
|
||||
moderators = persistentListOf(aRoomMember(A_USER_ID, role = RoomMember.Role.MODERATOR)),
|
||||
moderators = persistentListOf(aRoomMember(A_USER_ID, role = RoomMember.Role.Moderator)),
|
||||
members = persistentListOf(),
|
||||
)
|
||||
assertThat(membersByRoleWithModerators.isEmpty()).isFalse()
|
||||
|
||||
val membersByRoleWithMembers = MembersByRole(
|
||||
owners = persistentListOf(),
|
||||
admins = persistentListOf(),
|
||||
moderators = persistentListOf(),
|
||||
members = persistentListOf(aRoomMember(A_USER_ID, role = RoomMember.Role.USER)),
|
||||
members = persistentListOf(aRoomMember(A_USER_ID, role = RoomMember.Role.User)),
|
||||
)
|
||||
assertThat(membersByRoleWithMembers.isEmpty()).isFalse()
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@ import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.RoomModeration
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember.Role.ADMIN
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember.Role.MODERATOR
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember.Role.USER
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember.Role.Admin
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember.Role.Moderator
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember.Role.User
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues
|
||||
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
@@ -100,13 +100,13 @@ class ChangeBaseRoomPermissionsPresenterTest {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val state = awaitUpdatedItem()
|
||||
assertThat(state.currentPermissions?.roomName).isEqualTo(ADMIN.powerLevel)
|
||||
assertThat(state.currentPermissions?.roomName).isEqualTo(Admin.powerLevel)
|
||||
assertThat(state.hasChanges).isFalse()
|
||||
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, MODERATOR))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, Moderator))
|
||||
|
||||
awaitItem().run {
|
||||
assertThat(currentPermissions?.roomName).isEqualTo(MODERATOR.powerLevel)
|
||||
assertThat(currentPermissions?.roomName).isEqualTo(Moderator.powerLevel)
|
||||
assertThat(hasChanges).isTrue()
|
||||
}
|
||||
}
|
||||
@@ -120,28 +120,28 @@ class ChangeBaseRoomPermissionsPresenterTest {
|
||||
}.test {
|
||||
val state = awaitUpdatedItem()
|
||||
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.INVITE, MODERATOR))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.KICK, MODERATOR))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.BAN, MODERATOR))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.SEND_EVENTS, MODERATOR))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.REDACT_EVENTS, MODERATOR))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, MODERATOR))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_AVATAR, MODERATOR))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_TOPIC, MODERATOR))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.INVITE, Moderator))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.KICK, Moderator))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.BAN, Moderator))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.SEND_EVENTS, Moderator))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.REDACT_EVENTS, Moderator))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, Moderator))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_AVATAR, Moderator))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_TOPIC, Moderator))
|
||||
|
||||
val items = cancelAndConsumeRemainingEvents()
|
||||
|
||||
(items.last() as? Event.Item<ChangeRoomPermissionsState>)?.value?.run {
|
||||
assertThat(currentPermissions).isEqualTo(
|
||||
RoomPowerLevelsValues(
|
||||
invite = MODERATOR.powerLevel,
|
||||
kick = MODERATOR.powerLevel,
|
||||
ban = MODERATOR.powerLevel,
|
||||
redactEvents = MODERATOR.powerLevel,
|
||||
sendEvents = MODERATOR.powerLevel,
|
||||
roomName = MODERATOR.powerLevel,
|
||||
roomAvatar = MODERATOR.powerLevel,
|
||||
roomTopic = MODERATOR.powerLevel,
|
||||
invite = Moderator.powerLevel,
|
||||
kick = Moderator.powerLevel,
|
||||
ban = Moderator.powerLevel,
|
||||
redactEvents = Moderator.powerLevel,
|
||||
sendEvents = Moderator.powerLevel,
|
||||
roomName = Moderator.powerLevel,
|
||||
roomAvatar = Moderator.powerLevel,
|
||||
roomTopic = Moderator.powerLevel,
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -162,17 +162,17 @@ class ChangeBaseRoomPermissionsPresenterTest {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val state = awaitUpdatedItem()
|
||||
assertThat(state.currentPermissions?.roomName).isEqualTo(ADMIN.powerLevel)
|
||||
assertThat(state.currentPermissions?.roomName).isEqualTo(Admin.powerLevel)
|
||||
assertThat(state.hasChanges).isFalse()
|
||||
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, MODERATOR))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_AVATAR, MODERATOR))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_TOPIC, MODERATOR))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.SEND_EVENTS, MODERATOR))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.REDACT_EVENTS, USER))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.KICK, ADMIN))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.BAN, ADMIN))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.INVITE, ADMIN))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, Moderator))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_AVATAR, Moderator))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_TOPIC, Moderator))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.SEND_EVENTS, Moderator))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.REDACT_EVENTS, User))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.KICK, Admin))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.BAN, Admin))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.INVITE, Admin))
|
||||
skipItems(7)
|
||||
assertThat(awaitItem().hasChanges).isTrue()
|
||||
|
||||
@@ -181,7 +181,7 @@ class ChangeBaseRoomPermissionsPresenterTest {
|
||||
assertThat(awaitItem().saveAction).isEqualTo(AsyncAction.Loading)
|
||||
assertThat(awaitItem().hasChanges).isFalse()
|
||||
awaitItem().run {
|
||||
assertThat(currentPermissions?.roomName).isEqualTo(MODERATOR.powerLevel)
|
||||
assertThat(currentPermissions?.roomName).isEqualTo(Moderator.powerLevel)
|
||||
assertThat(saveAction).isEqualTo(AsyncAction.Success(Unit))
|
||||
}
|
||||
assertThat(analyticsService.capturedEvents).containsExactlyElementsIn(
|
||||
@@ -227,17 +227,17 @@ class ChangeBaseRoomPermissionsPresenterTest {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val state = awaitUpdatedItem()
|
||||
assertThat(state.currentPermissions?.roomName).isEqualTo(ADMIN.powerLevel)
|
||||
assertThat(state.currentPermissions?.roomName).isEqualTo(Admin.powerLevel)
|
||||
assertThat(state.hasChanges).isFalse()
|
||||
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, MODERATOR))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, Moderator))
|
||||
assertThat(awaitItem().hasChanges).isTrue()
|
||||
|
||||
state.eventSink(ChangeRoomPermissionsEvent.Save)
|
||||
|
||||
assertThat(awaitItem().saveAction).isEqualTo(AsyncAction.Loading)
|
||||
awaitItem().run {
|
||||
assertThat(currentPermissions?.roomName).isEqualTo(MODERATOR.powerLevel)
|
||||
assertThat(currentPermissions?.roomName).isEqualTo(Moderator.powerLevel)
|
||||
// Couldn't save the changes, so they're still pending
|
||||
assertThat(hasChanges).isTrue()
|
||||
assertThat(saveAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
@@ -245,7 +245,7 @@ class ChangeBaseRoomPermissionsPresenterTest {
|
||||
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ResetPendingActions)
|
||||
awaitItem().run {
|
||||
assertThat(currentPermissions?.roomName).isEqualTo(MODERATOR.powerLevel)
|
||||
assertThat(currentPermissions?.roomName).isEqualTo(Moderator.powerLevel)
|
||||
assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
assertThat(hasChanges).isTrue()
|
||||
}
|
||||
@@ -259,7 +259,7 @@ class ChangeBaseRoomPermissionsPresenterTest {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val state = awaitUpdatedItem()
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, MODERATOR))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, Moderator))
|
||||
assertThat(awaitItem().hasChanges).isTrue()
|
||||
|
||||
state.eventSink(ChangeRoomPermissionsEvent.Exit)
|
||||
|
||||
@@ -115,9 +115,9 @@ class ChangeBaseRoomPermissionsViewTest {
|
||||
rule.onAllNodesWithText(users).onFirst().performClick()
|
||||
recorder.assertList(
|
||||
listOf(
|
||||
ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, RoomMember.Role.ADMIN),
|
||||
ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, RoomMember.Role.MODERATOR),
|
||||
ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, RoomMember.Role.USER),
|
||||
ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, RoomMember.Role.Admin),
|
||||
ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, RoomMember.Role.Moderator),
|
||||
ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, RoomMember.Role.User),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -61,8 +61,8 @@ class RoomMemberModerationPresenterTest {
|
||||
val room = aJoinedRoom(
|
||||
canBan = false,
|
||||
canKick = false,
|
||||
myUserRole = RoomMember.Role.USER,
|
||||
targetRoomMember = aRoomMember(userId = A_USER_ID, powerLevel = RoomMember.Role.USER.powerLevel)
|
||||
myUserRole = RoomMember.Role.User,
|
||||
targetRoomMember = aRoomMember(userId = A_USER_ID, powerLevel = RoomMember.Role.User.powerLevel)
|
||||
)
|
||||
createRoomMemberModerationPresenter(room = room).test {
|
||||
val initialState = awaitState()
|
||||
@@ -81,7 +81,7 @@ class RoomMemberModerationPresenterTest {
|
||||
val room = aJoinedRoom(
|
||||
canBan = true,
|
||||
canKick = true,
|
||||
myUserRole = RoomMember.Role.ADMIN,
|
||||
myUserRole = RoomMember.Role.Admin,
|
||||
targetRoomMember = null
|
||||
)
|
||||
createRoomMemberModerationPresenter(room = room).test {
|
||||
@@ -103,8 +103,8 @@ class RoomMemberModerationPresenterTest {
|
||||
val room = aJoinedRoom(
|
||||
canBan = true,
|
||||
canKick = true,
|
||||
myUserRole = RoomMember.Role.ADMIN,
|
||||
targetRoomMember = aRoomMember(userId = A_USER_ID, powerLevel = RoomMember.Role.USER.powerLevel)
|
||||
myUserRole = RoomMember.Role.Admin,
|
||||
targetRoomMember = aRoomMember(userId = A_USER_ID, powerLevel = RoomMember.Role.User.powerLevel)
|
||||
)
|
||||
createRoomMemberModerationPresenter(room = room).test {
|
||||
val initialState = awaitState()
|
||||
@@ -125,8 +125,8 @@ class RoomMemberModerationPresenterTest {
|
||||
val room = aJoinedRoom(
|
||||
canBan = true,
|
||||
canKick = true,
|
||||
myUserRole = RoomMember.Role.MODERATOR,
|
||||
targetRoomMember = aRoomMember(userId = A_USER_ID, powerLevel = RoomMember.Role.ADMIN.powerLevel)
|
||||
myUserRole = RoomMember.Role.Moderator,
|
||||
targetRoomMember = aRoomMember(userId = A_USER_ID, powerLevel = RoomMember.Role.Admin.powerLevel)
|
||||
)
|
||||
createRoomMemberModerationPresenter(room = room).test {
|
||||
val initialState = awaitState()
|
||||
@@ -147,7 +147,7 @@ class RoomMemberModerationPresenterTest {
|
||||
val room = aJoinedRoom(
|
||||
canBan = true,
|
||||
canKick = true,
|
||||
myUserRole = RoomMember.Role.MODERATOR,
|
||||
myUserRole = RoomMember.Role.Moderator,
|
||||
targetRoomMember = aRoomMember(userId = A_USER_ID, membership = RoomMembershipState.BAN)
|
||||
)
|
||||
createRoomMemberModerationPresenter(room = room).test {
|
||||
@@ -321,7 +321,7 @@ class RoomMemberModerationPresenterTest {
|
||||
private fun aJoinedRoom(
|
||||
canKick: Boolean = false,
|
||||
canBan: Boolean = false,
|
||||
myUserRole: RoomMember.Role = RoomMember.Role.USER,
|
||||
myUserRole: RoomMember.Role = RoomMember.Role.User,
|
||||
kickUserResult: Result<Unit> = Result.success(Unit),
|
||||
banUserResult: Result<Unit> = Result.success(Unit),
|
||||
unBanUserResult: Result<Unit> = Result.success(Unit),
|
||||
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user