Merge pull request #5786 from element-hq/feature/bma/addAdminConfirmation

Ensure confirmation dialog is displayed when an admin add other admin to a room
This commit is contained in:
Benoit Marty
2025-11-21 17:06:42 +01:00
committed by GitHub
16 changed files with 260 additions and 124 deletions

View File

@@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
@@ -104,7 +105,11 @@ class ChangeRolesPresenter(
}
}
val hasPendingChanges = usersWithRole.value != selectedUsers.value
val hasPendingChanges by remember {
derivedStateOf {
usersWithRole.value.toSet() != selectedUsers.value.toSet()
}
}
val roomInfo by room.roomInfoFlow.collectAsState()
fun canChangeMemberRole(userId: UserId): Boolean {
@@ -134,16 +139,20 @@ class ChangeRolesPresenter(
is ChangeRolesEvent.Save -> {
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 modifyingOwners = role is RoomMember.Role.Owner
val needsConfirmation = (modifyingOwners || currentUserIsAdmin && isModifyingAdmins) && hasChanges && !isConfirming
val confirmationValue = if (hasPendingChanges && !isConfirming) {
when {
modifyingOwners -> ConfirmingModifyingOwners
currentUserIsAdmin && isModifyingAdmins -> ConfirmingModifyingAdmins
else -> null
}
} else {
null
}
when {
needsConfirmation -> {
// Confirm modifying users
saveState.value = AsyncAction.ConfirmingNoParams
confirmationValue != null -> {
saveState.value = confirmationValue
}
!saveState.value.isLoading() -> {
roomCoroutineScope.save(usersWithRole.value, selectedUsers, saveState)

View File

@@ -46,7 +46,7 @@ class ChangeRolesStateProvider : PreviewParameterProvider<ChangeRolesState> {
selectedUsers = aMatrixUserList().take(1).toImmutableList(),
),
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.ConfirmingCancellation),
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.ConfirmingNoParams),
aChangeRolesStateWithSelectedUsers().copy(savingState = ConfirmingModifyingAdmins),
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Loading),
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Success(true)),
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Failure(Exception("boom"))),
@@ -58,6 +58,8 @@ class ChangeRolesStateProvider : PreviewParameterProvider<ChangeRolesState> {
)
),
aChangeRolesStateWithOwners(role = RoomMember.Role.Owner(isCreator = false)),
aChangeRolesStateWithOwners(role = RoomMember.Role.Owner(isCreator = false))
.copy(savingState = ConfirmingModifyingOwners),
)
}

View File

@@ -178,29 +178,23 @@ fun ChangeRolesView(
onDismiss = { state.eventSink(ChangeRolesEvent.CloseDialog) }
)
}
else -> {
when (state.role) {
is RoomMember.Role.Owner -> {
ConfirmationDialog(
title = stringResource(R.string.screen_room_change_role_confirm_change_owners_title),
content = stringResource(R.string.screen_room_change_role_confirm_change_owners_description),
submitText = stringResource(CommonStrings.action_continue),
onSubmitClick = { state.eventSink(ChangeRolesEvent.Save) },
onDismiss = { state.eventSink(ChangeRolesEvent.CloseDialog) },
destructiveSubmit = true,
)
}
is RoomMember.Role.Admin -> {
ConfirmationDialog(
title = stringResource(R.string.screen_room_change_role_confirm_add_admin_title),
content = stringResource(R.string.screen_room_change_role_confirm_add_admin_description),
onSubmitClick = { state.eventSink(ChangeRolesEvent.Save) },
onDismiss = { state.eventSink(ChangeRolesEvent.CloseDialog) }
)
}
// No confirmation needed for Moderator or User roles
else -> Unit
}
is ConfirmingModifyingOwners -> {
ConfirmationDialog(
title = stringResource(R.string.screen_room_change_role_confirm_change_owners_title),
content = stringResource(R.string.screen_room_change_role_confirm_change_owners_description),
submitText = stringResource(CommonStrings.action_continue),
onSubmitClick = { state.eventSink(ChangeRolesEvent.Save) },
onDismiss = { state.eventSink(ChangeRolesEvent.CloseDialog) },
destructiveSubmit = true,
)
}
is ConfirmingModifyingAdmins -> {
ConfirmationDialog(
title = stringResource(R.string.screen_room_change_role_confirm_add_admin_title),
content = stringResource(R.string.screen_room_change_role_confirm_add_admin_description),
onSubmitClick = { state.eventSink(ChangeRolesEvent.Save) },
onDismiss = { state.eventSink(ChangeRolesEvent.CloseDialog) }
)
}
}
},

View File

@@ -0,0 +1,12 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.rolesandpermissions.impl.roles
import io.element.android.libraries.architecture.AsyncAction
data object ConfirmingModifyingAdmins : AsyncAction.Confirming

View File

@@ -0,0 +1,12 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.rolesandpermissions.impl.roles
import io.element.android.libraries.architecture.AsyncAction
data object ConfirmingModifyingOwners : AsyncAction.Confirming

View File

@@ -8,9 +8,6 @@
package io.element.android.features.rolesandpermissions.impl.roles
import app.cash.molecule.RecompositionMode
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.libraries.architecture.AsyncAction
@@ -31,6 +28,7 @@ import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.matrix.test.room.defaultRoomPowerLevelValues
import io.element.android.libraries.previewutils.room.aRoomMemberList
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.test
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableList
@@ -43,9 +41,7 @@ class ChangeRolesPresenterTest {
@Test
fun `present - initial state`() = runTest {
val presenter = createChangeRolesPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
with(awaitItem()) {
assertThat(role).isEqualTo(RoomMember.Role.Admin)
assertThat(query).isNull()
@@ -65,16 +61,14 @@ class ChangeRolesPresenterTest {
givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
}
val presenter = createChangeRolesPresenter(room = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
skipItems(1)
assertThat(awaitItem().searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
}
}
@Test
fun `present - canChangeRole of users with lower power level unless they are owners`() = runTest {
fun `present - canChangeRole of users with lower power level unless they are owners - privilegedCreatorRole is true`() = runTest {
val creatorUserId = UserId("@creator:matrix.org")
val superAdminUserId = UserId("@super_admin:matrix.org")
@@ -82,6 +76,7 @@ class ChangeRolesPresenterTest {
// User is a creator, so they can change roles of other members. So is `creatorUserId`.
givenRoomInfo(
aRoomInfo(
privilegedCreatorRole = true,
roomCreators = listOf(sessionId, creatorUserId),
roomPowerLevels = RoomPowerLevels(
defaultRoomPowerLevelValues(),
@@ -99,16 +94,14 @@ class ChangeRolesPresenterTest {
val roomMemberList = aRoomMemberList() + listOf(
// Owner - superadmin
aRoomMember(userId = superAdminUserId, role = RoomMember.Role.Owner(isCreator = true)),
aRoomMember(userId = superAdminUserId, role = RoomMember.Role.Owner(isCreator = false)),
// Owner - creator
aRoomMember(userId = creatorUserId, role = RoomMember.Role.Owner(isCreator = true))
)
givenRoomMembersState(RoomMembersState.Ready(roomMemberList.toImmutableList()))
}
val presenter = createChangeRolesPresenter(room = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
skipItems(1)
awaitItem().run {
assertThat(canChangeMemberRole(A_USER_ID_2)).isTrue() // Admin
@@ -118,6 +111,53 @@ class ChangeRolesPresenterTest {
}
}
@Test
fun `present - canChangeRole of users with lower power level unless they are owners - privilegedCreatorRole is false`() = 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(
privilegedCreatorRole = false,
roomCreators = listOf(sessionId, creatorUserId),
roomPowerLevels = RoomPowerLevels(
defaultRoomPowerLevelValues(),
users = persistentMapOf(
// Creator is an admin
sessionId to RoomMember.Role.Admin.powerLevel,
creatorUserId to RoomMember.Role.Admin.powerLevel,
// 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 = false)),
// Owner - creator
aRoomMember(userId = creatorUserId, role = RoomMember.Role.Owner(isCreator = true))
)
givenRoomMembersState(RoomMembersState.Ready(roomMemberList.toImmutableList()))
}
val presenter = createChangeRolesPresenter(room = room)
presenter.test {
skipItems(1)
awaitItem().run {
assertThat(canChangeMemberRole(A_USER_ID_2)).isFalse() // Creator cannot update Admin in this case
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 {
@@ -129,9 +169,7 @@ class ChangeRolesPresenterTest {
givenRoomMembersState(RoomMembersState.Ready(memberList))
}
val presenter = createChangeRolesPresenter(room = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
skipItems(1)
awaitItem().searchResults.run {
assertThat(this).isInstanceOf(SearchBarResultState.Results::class.java)
@@ -149,9 +187,7 @@ class ChangeRolesPresenterTest {
givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
}
val presenter = createChangeRolesPresenter(room = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
val initialState = awaitItem()
initialState.eventSink(ChangeRolesEvent.ToggleSearchActive)
@@ -168,9 +204,7 @@ class ChangeRolesPresenterTest {
givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
}
val presenter = createChangeRolesPresenter(room = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
val initialState = awaitItem()
val initialResults = (awaitItem().searchResults as? SearchBarResultState.Results)?.results
assertThat(initialResults?.members).hasSize(8)
@@ -194,9 +228,7 @@ class ChangeRolesPresenterTest {
givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
}
val presenter = createChangeRolesPresenter(room = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
skipItems(1)
val initialResults = (awaitItem().searchResults as? SearchBarResultState.Results)?.results
assertThat(initialResults?.members).hasSize(8)
@@ -221,9 +253,7 @@ class ChangeRolesPresenterTest {
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.Admin)))
}
val presenter = createChangeRolesPresenter(room = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.selectedUsers).hasSize(1)
@@ -243,9 +273,7 @@ class ChangeRolesPresenterTest {
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.Admin)))
}
val presenter = createChangeRolesPresenter(room = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.hasPendingChanges).isFalse()
@@ -272,9 +300,7 @@ class ChangeRolesPresenterTest {
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.Admin)))
}
val presenter = createChangeRolesPresenter(room = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.hasPendingChanges).isFalse()
@@ -292,9 +318,7 @@ class ChangeRolesPresenterTest {
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.Admin)))
}
val presenter = createChangeRolesPresenter(room = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.hasPendingChanges).isFalse()
@@ -318,9 +342,7 @@ class ChangeRolesPresenterTest {
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.Admin)))
}
val presenter = createChangeRolesPresenter(room = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.hasPendingChanges).isFalse()
@@ -349,16 +371,14 @@ class ChangeRolesPresenterTest {
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.Admin)))
}
val presenter = createChangeRolesPresenter(role = RoomMember.Role.Admin, room = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
skipItems(2)
val initialState = awaitItem()
assertThat(initialState.selectedUsers).hasSize(1)
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(MatrixUser(A_USER_ID_2)))
awaitItem().eventSink(ChangeRolesEvent.Save)
val confirmingState = awaitItem()
assertThat(confirmingState.savingState).isEqualTo(AsyncAction.ConfirmingNoParams)
assertThat(confirmingState.savingState).isEqualTo(ConfirmingModifyingAdmins)
confirmingState.eventSink(ChangeRolesEvent.Save)
assertThat(awaitItem().savingState).isInstanceOf(AsyncAction.Loading::class.java)
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Success(true))
@@ -372,9 +392,7 @@ class ChangeRolesPresenterTest {
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.Admin)))
}
val presenter = createChangeRolesPresenter(role = RoomMember.Role.Admin, room = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.selectedUsers).hasSize(1)
@@ -383,7 +401,7 @@ class ChangeRolesPresenterTest {
awaitItem().eventSink(ChangeRolesEvent.Save)
val confirmingState = awaitItem()
assertThat(confirmingState.savingState).isEqualTo(AsyncAction.ConfirmingNoParams)
assertThat(confirmingState.savingState).isEqualTo(ConfirmingModifyingAdmins)
confirmingState.eventSink(ChangeRolesEvent.CloseDialog)
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Uninitialized)
@@ -405,9 +423,7 @@ class ChangeRolesPresenterTest {
room = room,
analyticsService = analyticsService
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
skipItems(2)
val initialState = awaitItem()
assertThat(initialState.selectedUsers).hasSize(1)
@@ -442,9 +458,7 @@ class ChangeRolesPresenterTest {
room = room,
analyticsService = analyticsService
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.selectedUsers).hasSize(1)
@@ -477,9 +491,7 @@ class ChangeRolesPresenterTest {
room = room,
analyticsService = analyticsService
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
skipItems(2)
val initialState = awaitItem()
assertThat(initialState.selectedUsers).hasSize(1)
@@ -504,9 +516,7 @@ class ChangeRolesPresenterTest {
givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(role = RoomMember.Role.Moderator, userId = A_USER_ID)))
}
val presenter = createChangeRolesPresenter(role = RoomMember.Role.Moderator, room = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
skipItems(2)
val initialState = awaitItem()
assertThat(initialState.selectedUsers).hasSize(1)

View File

@@ -50,7 +50,6 @@ class ChangeRolesViewTest {
),
)
}.exceptionOrNull()
assertThat(exception).isNotNull()
}
@@ -63,9 +62,7 @@ class ChangeRolesViewTest {
eventSink = eventsRecorder,
),
)
rule.pressBackKey()
eventsRecorder.assertSingle(ChangeRolesEvent.ToggleSearchActive)
}
@@ -78,9 +75,7 @@ class ChangeRolesViewTest {
eventSink = eventsRecorder,
),
)
rule.pressBackKey()
eventsRecorder.assertList(listOf(ChangeRolesEvent.QueryChanged(""), ChangeRolesEvent.Exit))
}
@@ -93,9 +88,7 @@ class ChangeRolesViewTest {
eventSink = eventsRecorder,
),
)
rule.pressBack()
eventsRecorder.assertList(listOf(ChangeRolesEvent.QueryChanged(""), ChangeRolesEvent.Exit))
}
@@ -108,9 +101,7 @@ class ChangeRolesViewTest {
eventSink = eventsRecorder,
),
)
rule.clickOn(CommonStrings.action_save)
eventsRecorder.assertList(listOf(ChangeRolesEvent.QueryChanged(""), ChangeRolesEvent.Save))
}
@@ -123,9 +114,7 @@ class ChangeRolesViewTest {
eventSink = eventsRecorder,
),
)
rule.clickOn(CommonStrings.action_save)
eventsRecorder.assertList(listOf(ChangeRolesEvent.QueryChanged("")))
}
@@ -139,9 +128,7 @@ class ChangeRolesViewTest {
eventSink = eventsRecorder,
),
)
rule.clickOn(CommonStrings.action_ok)
eventsRecorder.assertSingle(ChangeRolesEvent.Exit)
}
@@ -155,26 +142,22 @@ class ChangeRolesViewTest {
eventSink = eventsRecorder,
),
)
rule.clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(ChangeRolesEvent.CloseDialog)
}
@Test
fun `save confirmation dialog - submit saves the changes`() {
fun `save admins confirmation dialog - submit saves the changes`() {
val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
rule.setChangeRolesContent(
state = aChangeRolesState(
role = RoomMember.Role.Admin,
isSearchActive = true,
savingState = AsyncAction.ConfirmingNoParams,
savingState = ConfirmingModifyingAdmins,
eventSink = eventsRecorder,
),
)
rule.clickOn(CommonStrings.action_ok)
eventsRecorder.assertSingle(ChangeRolesEvent.Save)
}
@@ -185,30 +168,41 @@ class ChangeRolesViewTest {
state = aChangeRolesState(
role = RoomMember.Role.Owner(isCreator = false),
isSearchActive = true,
savingState = AsyncAction.ConfirmingNoParams,
savingState = ConfirmingModifyingOwners,
eventSink = eventsRecorder,
),
)
rule.clickOn(CommonStrings.action_continue)
eventsRecorder.assertSingle(ChangeRolesEvent.Save)
}
@Test
fun `save confirmation dialog - cancel removes the dialog`() {
fun `save admins confirmation dialog - cancel removes the dialog`() {
val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
rule.setChangeRolesContent(
state = aChangeRolesState(
role = RoomMember.Role.Admin,
isSearchActive = true,
savingState = AsyncAction.ConfirmingNoParams,
savingState = ConfirmingModifyingAdmins,
eventSink = eventsRecorder,
),
)
rule.clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(ChangeRolesEvent.CloseDialog)
}
@Test
fun `save owners confirmation dialog - cancel removes the dialog`() {
val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
rule.setChangeRolesContent(
state = aChangeRolesState(
role = RoomMember.Role.Owner(isCreator = false),
isSearchActive = true,
savingState = ConfirmingModifyingOwners,
eventSink = eventsRecorder,
),
)
rule.clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(ChangeRolesEvent.CloseDialog)
}
@@ -222,9 +216,7 @@ class ChangeRolesViewTest {
eventSink = eventsRecorder,
),
)
rule.clickOn(CommonStrings.action_ok)
eventsRecorder.assertSingle(ChangeRolesEvent.CloseDialog)
}

View File

@@ -205,6 +205,7 @@ class RoomDetailsPresenter(
canReportRoom = canReportRoom,
isTombstoned = roomInfo.successorRoom != null,
showDebugInfo = isDeveloperModeEnabled,
roomVersion = roomInfo.roomVersion,
eventSink = ::handleEvent,
)
}

View File

@@ -49,6 +49,7 @@ data class RoomDetailsState(
val canReportRoom: Boolean,
val isTombstoned: Boolean,
val showDebugInfo: Boolean,
val roomVersion: String?,
val eventSink: (RoomDetailsEvent) -> Unit
) {
val roomBadges = buildList {

View File

@@ -146,6 +146,7 @@ fun aRoomDetailsState(
canReportRoom = canReportRoom,
isTombstoned = isTombstoned,
showDebugInfo = showDebugInfo,
roomVersion = "12",
eventSink = eventSink,
)

View File

@@ -264,6 +264,7 @@ fun RoomDetailsView(
if (state.showDebugInfo) {
DebugInfoSection(
roomId = state.roomId,
roomVersion = state.roomVersion,
)
}
}
@@ -714,7 +715,10 @@ private fun OtherActionsSection(
}
@Composable
private fun DebugInfoSection(roomId: RoomId) {
private fun DebugInfoSection(
roomId: RoomId,
roomVersion: String?,
) {
val context = LocalContext.current
PreferenceCategory(showTopDivider = true) {
ListItem(
@@ -737,6 +741,19 @@ private fun DebugInfoSection(roomId: RoomId) {
)
},
)
ListItem(
headlineContent = {
Text("Room version")
},
supportingContent = {
Text(
text = roomVersion ?: "Unknown",
style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textSecondary,
)
},
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Info())),
)
}
}

View File

@@ -53,7 +53,7 @@ fun aRoomInfo(
notificationCount: Long = 0,
userDefinedNotificationMode: RoomNotificationMode? = null,
hasRoomCall: Boolean = false,
roomPowerLevels: RoomPowerLevels = RoomPowerLevels(
roomPowerLevels: RoomPowerLevels? = RoomPowerLevels(
values = defaultRoomPowerLevelValues(),
users = persistentMapOf(),
),

View File

@@ -23,12 +23,12 @@ fun RoomInfo.getAvatarData(size: AvatarSize) = AvatarData(
/**
* Returns the role of the user in the room.
* If the user is a creator, returns [RoomMember.Role.Owner].
* If the user is a creator and [RoomInfo.privilegedCreatorRole] is true, 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].
*/
fun RoomInfo.roleOf(userId: UserId): RoomMember.Role {
return if (creators.contains(userId)) {
return if (privilegedCreatorRole && creators.contains(userId)) {
RoomMember.Role.Owner(isCreator = true)
} else {
roomPowerLevels?.roleOf(userId) ?: RoomMember.Role.User

View File

@@ -0,0 +1,79 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.ui.model
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels
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.aRoomInfo
import io.element.android.libraries.matrix.test.room.defaultRoomPowerLevelValues
import kotlinx.collections.immutable.toImmutableMap
import org.junit.Test
class RoomInfoExtensionTest {
@Test
fun `roleOf returns Owner for creator with privilegedCreatorRole true`() {
val roomInfo = aRoomInfo(
privilegedCreatorRole = true,
roomCreators = listOf(A_USER_ID),
)
assertThat(roomInfo.roleOf(A_USER_ID)).isEqualTo(RoomMember.Role.Owner(isCreator = true))
}
@Test
fun `roleOf returns User for not creator with privilegedCreatorRole true`() {
val roomInfo = aRoomInfo(
privilegedCreatorRole = true,
roomCreators = listOf(A_USER_ID),
)
assertThat(roomInfo.roleOf(A_USER_ID_2)).isEqualTo(RoomMember.Role.User)
}
@Test
fun `roleOf returns User for creator with privilegedCreatorRole false`() {
val roomInfo = aRoomInfo(
privilegedCreatorRole = false,
roomCreators = listOf(A_USER_ID),
)
assertThat(roomInfo.roleOf(A_USER_ID)).isEqualTo(RoomMember.Role.User)
}
@Test
fun `roleOf returns role from the power level`() {
val roomInfo = aRoomInfo(
privilegedCreatorRole = false,
roomPowerLevels = RoomPowerLevels(
values = defaultRoomPowerLevelValues(),
users = mapOf(
A_USER_ID to 100L, // Admin
A_USER_ID_2 to 50L, // Moderator
A_USER_ID_3 to 0L, // User
).toImmutableMap(),
),
roomCreators = listOf(A_USER_ID),
)
assertThat(roomInfo.roleOf(A_USER_ID)).isEqualTo(RoomMember.Role.Admin)
assertThat(roomInfo.roleOf(A_USER_ID_2)).isEqualTo(RoomMember.Role.Moderator)
assertThat(roomInfo.roleOf(A_USER_ID_3)).isEqualTo(RoomMember.Role.User)
}
@Test
fun `roleOf returns User when the power level is null`() {
val roomInfo = aRoomInfo(
privilegedCreatorRole = false,
roomPowerLevels = null,
roomCreators = listOf(A_USER_ID),
)
assertThat(roomInfo.roleOf(A_USER_ID)).isEqualTo(RoomMember.Role.User)
assertThat(roomInfo.roleOf(A_USER_ID_2)).isEqualTo(RoomMember.Role.User)
assertThat(roomInfo.roleOf(A_USER_ID_3)).isEqualTo(RoomMember.Role.User)
}
}