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
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e5c71623f182b0e2aac51c276d3b61f039997c3dadf6c7bd9de6c4f2dfa352b8
|
||||
size 49837
|
||||
oid sha256:a1a33eba9c38eb8e518401f19536be0dedeb25e1c6fa805006d25b2354eea9ad
|
||||
size 50289
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:01a7f9e4be6f064593c299e84f9bf3926dbeb5be1c243b7293889c178c593672
|
||||
size 53305
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:326e774bb0420b4d55cca0014bcb8aac69f2351e58e756c7d63de3905f5bb6d0
|
||||
size 66470
|
||||
oid sha256:f002866b7076f6541d2842009e7f0f2df1ab32981ca3d2bca0c8dddb260a3991
|
||||
size 65349
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ba30b7ffc1096fd5190fbf7e1881217dce20a5e0865800d49b26d82fd0aac0dd
|
||||
size 60733
|
||||
oid sha256:95e9f4fb1fdce076c41091c8e629fb5e4540d6c820271464a0df62ec9c86d04b
|
||||
size 60279
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0fa5719b287ebb77a79900ffdec8793d9a86f2a07920f32c50924872609b09d5
|
||||
size 60682
|
||||
oid sha256:9e24c761af34c2256845387f58fa09bd0200a5a325a7f263b1544db1f8109d55
|
||||
size 60230
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:46cd3b9a7be00e4f41fbaab11e1cb6aa6528d6db578a84512f7abedf5d4b5492
|
||||
size 55610
|
||||
oid sha256:ebe33703e823b36249c9ce77c1bc40cd1bc76a7a184947b2b8dd01d04add1126
|
||||
size 55151
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:13c8b71ef7331e86d15200b8b1979688b21337237bcccd309ce70d866f9304f4
|
||||
size 12932
|
||||
oid sha256:128c073395c100b5fa4d61ce8549fac6db753dd1d8ab33475652b5a051130f6e
|
||||
size 12485
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2d992de8f70372d03ca2e701310ab261c1f5ddb5864810071a84f6b17ce5f6a4
|
||||
size 57531
|
||||
oid sha256:57509ab85d4f7feda8287a9a39353c8213633b0675a638b286523bb9cdc177fe
|
||||
size 58002
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0b34ca16979d6e496ec13cec78250eace85e17616cfa162d131d2c8f797c6b1f
|
||||
size 59946
|
||||
oid sha256:0ddf77c1776754186a16520834dd32458777cefd02b63e810eae46b85788e825
|
||||
size 60434
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3dbd48f7c11b0b5881eedf939654e50b37bfc349c28c3c0e93d5ca5bda0717da
|
||||
size 51691
|
||||
oid sha256:0ef2a7ada57b834c8cc062096e5793027ba0e5b86d5d43bef88d3a5c448d5bdb
|
||||
size 51789
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:83a9e031d5b928895778ac073d4374863a5e8a6b0ac9d99fee0a95879a0421e9
|
||||
size 64502
|
||||
oid sha256:699f12cc41c0d766c61db13ea980fb150b7027457526892da636fa020d3e6a2f
|
||||
size 64046
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9a00224f7aea8db03d01c0dbf244b90eb5d2d3f4289ced45cda874dc655fbac9
|
||||
size 48320
|
||||
oid sha256:0b2908f2612fde0ac68aa872b039e39614004e9207119f34fd480bf9ab558e5f
|
||||
size 48502
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0907cb2655a7a02dd4cc00ffaabf564274eb54ccb739ac61d0b3c6f13a7dd86c
|
||||
size 54081
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bbca019f7bb7555bcfea4645ad3ec00445ec55a54cc98611b1e0d7f99bebeea4
|
||||
size 67131
|
||||
oid sha256:d05b4a8db64921114da702cbbbf3480037a5e95299b6be61b64882063bf3a8d6
|
||||
size 66013
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:aa22afe652bae97bbe9e3504610f520e6fd7337f2757f79c39e9e0f6115902fe
|
||||
size 61461
|
||||
oid sha256:def295811ae2872a5c882de15ced9613e6699d52d07a7263f2bee4f1cbaabbe8
|
||||
size 61093
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bbb5c63809d556bcc60b11b9c23940e26bfac377ed4a5a0d97971e060365c6f3
|
||||
size 61346
|
||||
oid sha256:f87e5c5ee46d0d722d468c20b7403393a34eda7c555fc331e5d936fab6bbc5f6
|
||||
size 60981
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:17a15796338eaa5a8b7a5c7e5252fe2d9c392f2d0dd78ce9e0976dfd68fa6ad5
|
||||
size 55975
|
||||
oid sha256:e9721797718883f2130ba5a90037f1edf8d813a02b5b8329fd7c677a517b6657
|
||||
size 55601
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:562dc9be79006376346ab1b2635890615eecc381e82c15b16a9ebf1855741e24
|
||||
size 12817
|
||||
oid sha256:f9a601e883469969a5c585e44d2c3f5da53abf307a9334fa19c27f685d3c7731
|
||||
size 12436
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2023b53d3a7099f55bc36a738506c6fab2910f3c83ca24a1647b6ba35c29f351
|
||||
size 55807
|
||||
oid sha256:ca1a7ecdceabde82b8283bba218211a7a86c33a01887a6b1d021cdd0460fdf47
|
||||
size 55937
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:41c3b6a09bf3899a0a28ac6b9fc96b773687adeb40a4b11ab0f80dadc1139e1e
|
||||
size 58176
|
||||
oid sha256:2865975bc3f091eead3fdab2870622aa8d8336d2bd0bde7110249e14d1bf5527
|
||||
size 58289
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:24389189d4955f584fb1ed4b390e6ec829622d457ee7f3205b0104f2bcd90bdd
|
||||
size 50948
|
||||
oid sha256:7f15f8c3e7b04e73ef0af6b013e8b817946016e81c4cc788f3c9fd1bc5e8667e
|
||||
size 50836
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:971a027f6aaaa21b7d62af3f08b50eb7cf8369199076226ea92e1b4b30f74342
|
||||
size 64234
|
||||
oid sha256:f184247e1ed5f006dca11426d94498c12724475011b4509d7f8fd13dd95fe0f9
|
||||
size 63882
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:67a08774600869ff76540d518c0d5f80c38f1f155a65af56374ccf362d5109d4
|
||||
size 40618
|
||||
oid sha256:8116875aca8d06c8450168555a8e591cb96b23aaa9f194f62456b6b369cfbc50
|
||||
size 42699
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bc1246fbe273fafe61dfacfe4131aeb166a81a4c4befc35fbeb68cf85c0f355f
|
||||
size 57070
|
||||
oid sha256:68af7df886052c7db027225c978b8c10f5bf69beab65834add7456500f2d5cc0
|
||||
size 59115
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f632403bc5aad7990c8f2a81eec9977db7bbf0fd8db33bbda61af5d25329ac7f
|
||||
size 37776
|
||||
oid sha256:168e8ccfc1be8927e7bab6526e71044786aefc5f8d6f5594b971e75926196481
|
||||
size 39424
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:60df11189f82a42a51fddfcece2d0fdd88c55d06f2a1ee1e2069bae74e9c1378
|
||||
size 36410
|
||||
oid sha256:3e2fce3603a20cdda8ac969e15cbd0b636185f0ff63930d67795074e740cf807
|
||||
size 38175
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4cff66b4102160aeeb3db4bb5df1fef1428b5fadc7a12a62170d2335f506ad3b
|
||||
size 46662
|
||||
oid sha256:79d359483e7bc03c3d5facf61bdc854711f685c1826fd909e8d599892eb28de3
|
||||
size 48333
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f632403bc5aad7990c8f2a81eec9977db7bbf0fd8db33bbda61af5d25329ac7f
|
||||
size 37776
|
||||
oid sha256:168e8ccfc1be8927e7bab6526e71044786aefc5f8d6f5594b971e75926196481
|
||||
size 39424
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:554d8d41ed47b34e71f6fff1689ee2c9488580d012d4ad2b867814399899c6e7
|
||||
size 36812
|
||||
oid sha256:e9be92dc8fbaebaa5c108754180eba18fdab8a31c5a239091b2779480bf89848
|
||||
size 38620
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:620f0bc7825ebf1d0abcbc824c990162bf6a98afe9d3566b476c16dabaa17935
|
||||
size 39106
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:036944c74e2a5150d54e9ad4d64fe6b624fe8e889e411c226816398c65110f55
|
||||
size 39321
|
||||
oid sha256:6cd19bb1be2581e0f1bb50624111bc13717a60778d5141f06857e1edaa34ab69
|
||||
size 41325
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:24df22e86f0fe16ba9d3ce944105c0e2450bed08c8a9556f0d3da0cec32067b5
|
||||
size 54642
|
||||
oid sha256:79770d1b10c85dc12bf57f0a4cf5cb147aca1dc91841bf680349a4603b266310
|
||||
size 56490
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ca39d5d1f2a24584a695f7e3741a82109870a41a289b72882b9cf476e0526de6
|
||||
size 35948
|
||||
oid sha256:33bda0d03e1f14113201df4187a0af99e5e415d2cf2cb0e7c24803d5685dbee8
|
||||
size 37586
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4381d361278286530bbf89b79c56ae37edb722ec0a829ad20f98ebe1205aa1b9
|
||||
size 33990
|
||||
oid sha256:e7625469fb3ad71c44d84e3b0f55cb952f07c15a57117a5c5b369952c6dab089
|
||||
size 35681
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:29863c9616f9ff6eedc59d9c8a78304e061f68448c51c217f8c43d7bb2b5bf69
|
||||
size 43568
|
||||
oid sha256:4c7bfeaa1a068be4ed2c21f659dedf996442ac4d0dbd27d8fa9c0d49944cc38f
|
||||
size 45256
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ca39d5d1f2a24584a695f7e3741a82109870a41a289b72882b9cf476e0526de6
|
||||
size 35948
|
||||
oid sha256:33bda0d03e1f14113201df4187a0af99e5e415d2cf2cb0e7c24803d5685dbee8
|
||||
size 37586
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dea918985f1cb9870d5da1e84ac2c7a43227f03aae4e4b7462bac5d6ba7343c2
|
||||
size 34480
|
||||
oid sha256:f65d83a0f0b106cd9738b94b99c1fe8ea75fcadb443377da65d8847e72adfd72
|
||||
size 36169
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bd8ce7b96a775a029920f5274d4dbc7ed03eb34ed446299ba0043a57ee20c7eb
|
||||
size 37978
|
||||
Loading…
Add table
Add a link
Reference in a new issue