diff --git a/changelog.d/2259.feature b/changelog.d/2259.feature
new file mode 100644
index 0000000000..24a3b06b3e
--- /dev/null
+++ b/changelog.d/2259.feature
@@ -0,0 +1 @@
+Change a room's permissions power levels.
diff --git a/features/messages/impl/src/main/res/values/localazy.xml b/features/messages/impl/src/main/res/values/localazy.xml
index 61cafa9ac5..daebb8613b 100644
--- a/features/messages/impl/src/main/res/values/localazy.xml
+++ b/features/messages/impl/src/main/res/values/localazy.xml
@@ -36,10 +36,10 @@
"Messages and content"
"Admins and moderators"
"Remove people"
- "Change Room Avatar"
+ "Change room avatar"
"Room details"
- "Change Room Name"
- "Change Room Topic"
+ "Change room name"
+ "Change room topic"
"Send messages"
"Edit Admins"
"You will not be able to undo this action. You are promoting the user to have the same power level as you."
@@ -87,7 +87,7 @@
"Moderators"
"Permissions"
"Reset permissions"
- "Once you reset permissions, you will lose your current settings."
+ "Once you reset permissions, you will lose the current settings."
"Reset permissions?"
"Roles"
"Room details"
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsEvents.kt
index 249d934816..f81fd94fb0 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsEvents.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsEvents.kt
@@ -21,5 +21,6 @@ import io.element.android.libraries.matrix.api.room.RoomMember
sealed interface RolesAndPermissionsEvents {
data object ChangeOwnRole : RolesAndPermissionsEvents
data class DemoteSelfTo(val role: RoomMember.Role) : RolesAndPermissionsEvents
+ data object ResetPermissions : RolesAndPermissionsEvents
data object CancelPendingAction : RolesAndPermissionsEvents
}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsFlowNode.kt
index 6c38551441..319de14309 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsFlowNode.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsFlowNode.kt
@@ -28,6 +28,8 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.ChangeRolesNode
+import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.ChangeRoomPermissionsNode
+import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.ChangeRoomPermissionsSection
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode
@@ -55,6 +57,9 @@ class RolesAndPermissionsFlowNode @AssistedInject constructor(
@Parcelize
data object ModeratorList : NavTarget
+
+ @Parcelize
+ data class ChangeRoomPermissions(val section: ChangeRoomPermissionsSection) : NavTarget
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@@ -68,6 +73,18 @@ class RolesAndPermissionsFlowNode @AssistedInject constructor(
override fun openModeratorList() {
backstack.push(NavTarget.ModeratorList)
}
+
+ override fun openEditRoomDetailsPermissions() {
+ backstack.push(NavTarget.ChangeRoomPermissions(ChangeRoomPermissionsSection.RoomDetails))
+ }
+
+ override fun openMessagesAndContentPermissions() {
+ backstack.push(NavTarget.ChangeRoomPermissions(ChangeRoomPermissionsSection.MessagesAndContent))
+ }
+
+ override fun openModerationPermissions() {
+ backstack.push(NavTarget.ChangeRoomPermissions(ChangeRoomPermissionsSection.MembershipModeration))
+ }
}
createNode(
buildContext = buildContext,
@@ -88,6 +105,13 @@ class RolesAndPermissionsFlowNode @AssistedInject constructor(
plugins = listOf(inputs),
)
}
+ is NavTarget.ChangeRoomPermissions -> {
+ val inputs = ChangeRoomPermissionsNode.Inputs(navTarget.section)
+ createNode(
+ buildContext = buildContext,
+ plugins = listOf(inputs),
+ )
+ }
}
}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsNode.kt
index 1e78302146..0da156e54b 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsNode.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsNode.kt
@@ -17,6 +17,7 @@
package io.element.android.features.roomdetails.impl.rolesandpermissions
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
@@ -46,17 +47,24 @@ class RolesAndPermissionsNode @AssistedInject constructor(
@Assisted plugins: List,
private val presenter: RolesAndPermissionsPresenter,
private val room: MatrixRoom,
-) : Node(buildContext, plugins = plugins), RoomDetailsAdminSettingsNavigator {
- interface Callback : Plugin {
- fun openAdminList()
- fun openModeratorList()
+) : Node(buildContext, plugins = plugins), RolesAndPermissionsNavigator {
+ interface Callback : Plugin, RolesAndPermissionsNavigator {
+ override fun openAdminList()
+ override fun openModeratorList()
+ override fun openEditRoomDetailsPermissions()
+ override fun openMessagesAndContentPermissions()
+ override fun openModerationPermissions()
+ override fun onBackPressed() {}
}
private val callback = plugins().first()
- override fun onBackPressed() = navigateUp()
- override fun openAdminList() = callback.openAdminList()
- override fun openModeratorList() = callback.openModeratorList()
+ @Stable
+ private val navigator = object : RolesAndPermissionsNavigator by callback {
+ override fun onBackPressed() {
+ navigateUp()
+ }
+ }
override fun onBuilt() {
super.onBuilt()
@@ -88,14 +96,17 @@ class RolesAndPermissionsNode @AssistedInject constructor(
val state = presenter.present()
RolesAndPermissionsView(
state = state,
- roomDetailsAdminSettingsNavigator = this,
+ rolesAndPermissionsNavigator = navigator,
modifier = modifier,
)
}
}
-interface RoomDetailsAdminSettingsNavigator {
+interface RolesAndPermissionsNavigator {
fun onBackPressed() {}
fun openAdminList() {}
fun openModeratorList() {}
+ fun openEditRoomDetailsPermissions() {}
+ fun openMessagesAndContentPermissions() {}
+ fun openModerationPermissions() {}
}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsPresenter.kt
index db67de66a4..9e6eb824cc 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsPresenter.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsPresenter.kt
@@ -55,6 +55,7 @@ class RolesAndPermissionsPresenter @Inject constructor(
}
}
val changeOwnRoleAction = remember { mutableStateOf>(AsyncAction.Uninitialized) }
+ val resetPermissionsAction = remember { mutableStateOf>(AsyncAction.Uninitialized) }
fun handleEvent(event: RolesAndPermissionsEvents) {
when (event) {
@@ -63,11 +64,17 @@ class RolesAndPermissionsPresenter @Inject constructor(
}
is RolesAndPermissionsEvents.CancelPendingAction -> {
changeOwnRoleAction.value = AsyncAction.Uninitialized
+ resetPermissionsAction.value = AsyncAction.Uninitialized
}
is RolesAndPermissionsEvents.DemoteSelfTo -> coroutineScope.demoteSelfTo(
role = event.role,
changeOwnRoleAction = changeOwnRoleAction,
)
+ is RolesAndPermissionsEvents.ResetPermissions -> if (resetPermissionsAction.value.isConfirming()) {
+ coroutineScope.resetPermissions(resetPermissionsAction)
+ } else {
+ resetPermissionsAction.value = AsyncAction.Confirming
+ }
}
}
@@ -75,6 +82,7 @@ class RolesAndPermissionsPresenter @Inject constructor(
adminCount = adminCount,
moderatorCount = moderatorCount,
changeOwnRoleAction = changeOwnRoleAction.value,
+ resetPermissionsAction = resetPermissionsAction.value,
eventSink = { handleEvent(it) },
)
}
@@ -88,6 +96,14 @@ class RolesAndPermissionsPresenter @Inject constructor(
}
}
+ private fun CoroutineScope.resetPermissions(
+ resetPermissionsAction: MutableState>,
+ ) = launch(dispatchers.io) {
+ runUpdatingState(resetPermissionsAction) {
+ room.resetPowerLevels().map {}
+ }
+ }
+
private fun MatrixRoomInfo?.userCountWithRole(role: RoomMember.Role): Int {
return if (this != null) {
userPowerLevels.count { (_, level) -> RoomMember.Role.forPowerLevel(level) == role }
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsState.kt
index b1c2905ae8..5fab19fcef 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsState.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsState.kt
@@ -22,5 +22,6 @@ data class RolesAndPermissionsState(
val adminCount: Int,
val moderatorCount: Int,
val changeOwnRoleAction: AsyncAction,
+ val resetPermissionsAction: AsyncAction,
val eventSink: (RolesAndPermissionsEvents) -> Unit,
)
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsStateProvider.kt
index cde21c1e7e..9fa91879ba 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsStateProvider.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsStateProvider.kt
@@ -39,6 +39,21 @@ class RolesAndPermissionsStateProvider : PreviewParameterProvider = AsyncAction.Uninitialized,
+ resetPermissionsAction: AsyncAction = AsyncAction.Uninitialized,
eventSink: (RolesAndPermissionsEvents) -> Unit = {},
) = RolesAndPermissionsState(
adminCount = adminCount,
moderatorCount = moderatorCount,
changeOwnRoleAction = changeOwnRoleAction,
+ resetPermissionsAction = resetPermissionsAction,
eventSink = eventSink,
)
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsView.kt
index 087fa8c862..f97b069215 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsView.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsView.kt
@@ -33,6 +33,8 @@ import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.roomdetails.impl.R
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.components.ProgressDialog
+import io.element.android.libraries.designsystem.components.async.AsyncActionView
+import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
@@ -53,35 +55,72 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun RolesAndPermissionsView(
state: RolesAndPermissionsState,
- roomDetailsAdminSettingsNavigator: RoomDetailsAdminSettingsNavigator,
+ rolesAndPermissionsNavigator: RolesAndPermissionsNavigator,
modifier: Modifier = Modifier,
) {
PreferencePage(
modifier = modifier,
title = stringResource(R.string.screen_room_roles_and_permissions_title),
- onBackPressed = roomDetailsAdminSettingsNavigator::onBackPressed,
+ onBackPressed = rolesAndPermissionsNavigator::onBackPressed,
) {
ListSectionHeader(title = stringResource(R.string.screen_room_roles_and_permissions_roles_header), hasDivider = false)
ListItem(
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_admins)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Admin())),
trailingContent = ListItemContent.Text("${state.adminCount}"),
- onClick = { roomDetailsAdminSettingsNavigator.openAdminList() },
+ onClick = { rolesAndPermissionsNavigator.openAdminList() },
)
ListItem(
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_moderators)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ChatProblem())),
trailingContent = ListItemContent.Text("${state.moderatorCount}"),
- onClick = { roomDetailsAdminSettingsNavigator.openModeratorList() },
+ 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()))
)
+ 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)) },
+ leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Info())),
+ onClick = { rolesAndPermissionsNavigator.openEditRoomDetailsPermissions() },
+ )
+ ListItem(
+ headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_messages_and_content)) },
+ leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Chat())),
+ onClick = { rolesAndPermissionsNavigator.openMessagesAndContentPermissions() },
+ )
+ ListItem(
+ headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_member_moderation)) },
+ leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.User())),
+ onClick = { rolesAndPermissionsNavigator.openModerationPermissions() },
+ )
HorizontalDivider()
+ ListItem(
+ headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_reset)) },
+ onClick = { state.eventSink(RolesAndPermissionsEvents.ResetPermissions) },
+ style = ListItemStyle.Destructive,
+ )
}
+ AsyncActionView(
+ async = state.resetPermissionsAction,
+ confirmationDialog = {
+ ConfirmationDialog(
+ title = stringResource(R.string.screen_room_roles_and_permissions_reset_confirm_title),
+ content = stringResource(R.string.screen_room_roles_and_permissions_reset_confirm_description),
+ submitText = stringResource(CommonStrings.action_reset),
+ destructiveSubmit = true,
+ onSubmitClicked = { state.eventSink(RolesAndPermissionsEvents.ResetPermissions) },
+ onDismiss = { state.eventSink(RolesAndPermissionsEvents.CancelPendingAction) },
+ )
+ },
+ onSuccess = { state.eventSink(RolesAndPermissionsEvents.CancelPendingAction) },
+ onErrorDismiss = { state.eventSink(RolesAndPermissionsEvents.CancelPendingAction) }
+ )
+
when (state.changeOwnRoleAction) {
is AsyncAction.Confirming -> {
ChangeOwnRoleBottomSheet(
@@ -156,11 +195,7 @@ private fun ChangeOwnRoleBottomSheet(
)
ListItem(
headlineContent = { Text(stringResource(CommonStrings.action_cancel)) },
- onClick = {
- sheetState.hide(coroutineScope) {
- eventSink(RolesAndPermissionsEvents.CancelPendingAction)
- }
- },
+ onClick = ::dismiss,
style = ListItemStyle.Primary,
)
}
@@ -168,11 +203,11 @@ private fun ChangeOwnRoleBottomSheet(
@PreviewsDayNight
@Composable
-internal fun RoomDetailsAdminSettingsViewPreview(@PreviewParameter(RolesAndPermissionsStateProvider::class) state: RolesAndPermissionsState) {
+internal fun RolesAndPermissionViewPreview(@PreviewParameter(RolesAndPermissionsStateProvider::class) state: RolesAndPermissionsState) {
ElementPreview {
RolesAndPermissionsView(
state = state,
- roomDetailsAdminSettingsNavigator = object : RoomDetailsAdminSettingsNavigator {},
+ rolesAndPermissionsNavigator = object : RolesAndPermissionsNavigator {},
)
}
}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsEvent.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsEvent.kt
new file mode 100644
index 0000000000..003c4f7233
--- /dev/null
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsEvent.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2024 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.roomdetails.impl.rolesandpermissions.permissions
+
+import io.element.android.libraries.matrix.api.room.RoomMember
+
+interface ChangeRoomPermissionsEvent {
+ data class ChangeMinimumRoleForAction(val action: RoomPermissionType, val role: RoomMember.Role) : ChangeRoomPermissionsEvent
+ data object Save : ChangeRoomPermissionsEvent
+ data object Exit : ChangeRoomPermissionsEvent
+ data object ResetPendingActions : ChangeRoomPermissionsEvent
+}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsNode.kt
new file mode 100644
index 0000000000..18b369a38b
--- /dev/null
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsNode.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2024 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.roomdetails.impl.rolesandpermissions.permissions
+
+import android.os.Parcelable
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.core.plugin.Plugin
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import io.element.android.anvilannotations.ContributesNode
+import io.element.android.libraries.architecture.NodeInputs
+import io.element.android.libraries.architecture.inputs
+import io.element.android.libraries.di.RoomScope
+import kotlinx.parcelize.Parcelize
+
+@ContributesNode(RoomScope::class)
+class ChangeRoomPermissionsNode @AssistedInject constructor(
+ @Assisted buildContext: BuildContext,
+ @Assisted plugins: List,
+ presenterFactory: ChangeRoomPermissionsPresenter.Factory,
+) : Node(buildContext, plugins = plugins) {
+ @Parcelize
+ data class Inputs(
+ val section: ChangeRoomPermissionsSection,
+ ) : NodeInputs, Parcelable
+
+ private val inputs: Inputs = inputs()
+
+ private val presenter = presenterFactory.run {
+ create(inputs.section)
+ }
+
+ @Composable
+ override fun View(modifier: Modifier) {
+ val state = presenter.present()
+ ChangeRoomPermissionsView(
+ modifier = modifier,
+ state = state,
+ onBackPressed = this::navigateUp,
+ )
+ }
+}
+
+@Parcelize
+enum class ChangeRoomPermissionsSection : Parcelable {
+ RoomDetails,
+ MessagesAndContent,
+ MembershipModeration,
+}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsPresenter.kt
new file mode 100644
index 0000000000..37d5d351eb
--- /dev/null
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsPresenter.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright (c) 2024 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.roomdetails.impl.rolesandpermissions.permissions
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import io.element.android.libraries.architecture.AsyncAction
+import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.matrix.api.room.MatrixRoom
+import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+class ChangeRoomPermissionsPresenter @AssistedInject constructor(
+ @Assisted private val section: ChangeRoomPermissionsSection,
+ private val room: MatrixRoom,
+) : Presenter {
+ companion object {
+ internal fun itemsForSection(section: ChangeRoomPermissionsSection) = when (section) {
+ ChangeRoomPermissionsSection.RoomDetails -> persistentListOf(
+ RoomPermissionType.ROOM_NAME,
+ RoomPermissionType.ROOM_AVATAR,
+ RoomPermissionType.ROOM_TOPIC,
+ )
+ ChangeRoomPermissionsSection.MessagesAndContent -> persistentListOf(
+ RoomPermissionType.SEND_EVENTS,
+ RoomPermissionType.REDACT_EVENTS,
+ )
+ ChangeRoomPermissionsSection.MembershipModeration -> persistentListOf(
+ RoomPermissionType.INVITE,
+ RoomPermissionType.KICK,
+ RoomPermissionType.BAN,
+ )
+ }
+ }
+ @AssistedFactory
+ interface Factory {
+ fun create(section: ChangeRoomPermissionsSection): ChangeRoomPermissionsPresenter
+ }
+
+ private val items: ImmutableList = itemsForSection(section)
+
+ private var initialPermissions by mutableStateOf(null)
+ private var currentPermissions by mutableStateOf(null)
+ private var saveAction by mutableStateOf>(AsyncAction.Uninitialized)
+ private var confirmExitAction by mutableStateOf>(AsyncAction.Uninitialized)
+
+ @Composable
+ override fun present(): ChangeRoomPermissionsState {
+ val coroutineScope = rememberCoroutineScope()
+
+ LaunchedEffect(Unit) {
+ updatePermissions()
+ }
+
+ val hasChanges by remember {
+ derivedStateOf { initialPermissions != currentPermissions }
+ }
+
+ fun handleEvent(event: ChangeRoomPermissionsEvent) {
+ when (event) {
+ is ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction -> {
+ currentPermissions = when (event.action) {
+ RoomPermissionType.BAN -> currentPermissions?.copy(ban = event.role.powerLevel)
+ RoomPermissionType.INVITE -> currentPermissions?.copy(invite = event.role.powerLevel)
+ RoomPermissionType.KICK -> currentPermissions?.copy(kick = event.role.powerLevel)
+ RoomPermissionType.SEND_EVENTS -> currentPermissions?.copy(sendEvents = event.role.powerLevel)
+ RoomPermissionType.REDACT_EVENTS -> currentPermissions?.copy(redactEvents = event.role.powerLevel)
+ RoomPermissionType.ROOM_NAME -> currentPermissions?.copy(roomName = event.role.powerLevel)
+ RoomPermissionType.ROOM_AVATAR -> currentPermissions?.copy(roomAvatar = event.role.powerLevel)
+ RoomPermissionType.ROOM_TOPIC -> currentPermissions?.copy(roomTopic = event.role.powerLevel)
+ }
+ }
+ is ChangeRoomPermissionsEvent.Save -> coroutineScope.save()
+ is ChangeRoomPermissionsEvent.Exit -> {
+ confirmExitAction = if (!hasChanges || confirmExitAction.isConfirming()) {
+ AsyncAction.Success(Unit)
+ } else {
+ AsyncAction.Confirming
+ }
+ }
+ is ChangeRoomPermissionsEvent.ResetPendingActions -> {
+ saveAction = AsyncAction.Uninitialized
+ confirmExitAction = AsyncAction.Uninitialized
+ }
+ }
+ }
+ return ChangeRoomPermissionsState(
+ section = section,
+ currentPermissions = currentPermissions,
+ items = items,
+ hasChanges = hasChanges,
+ saveAction = saveAction,
+ confirmExitAction = confirmExitAction,
+ eventSink = { handleEvent(it) }
+ )
+ }
+
+ private suspend fun updatePermissions() {
+ val powerLevels = room.powerLevels().getOrNull() ?: return
+ initialPermissions = powerLevels
+ currentPermissions = initialPermissions
+ }
+
+ private fun CoroutineScope.save() = launch {
+ saveAction = AsyncAction.Loading
+ val updatedRoomPowerLevels = currentPermissions ?: run {
+ saveAction = AsyncAction.Failure(IllegalStateException("Failed to set room power levels"))
+ return@launch
+ }
+ room.updatePowerLevels(updatedRoomPowerLevels)
+ .onSuccess {
+ initialPermissions = currentPermissions
+ saveAction = AsyncAction.Success(Unit)
+ }
+ .onFailure {
+ saveAction = AsyncAction.Failure(it)
+ }
+ }
+}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsState.kt
new file mode 100644
index 0000000000..a80e7bb49f
--- /dev/null
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsState.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2024 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.roomdetails.impl.rolesandpermissions.permissions
+
+import io.element.android.libraries.architecture.AsyncAction
+import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels
+import kotlinx.collections.immutable.ImmutableList
+
+data class ChangeRoomPermissionsState(
+ val section: ChangeRoomPermissionsSection,
+ val currentPermissions: MatrixRoomPowerLevels?,
+ val items: ImmutableList,
+ val hasChanges: Boolean,
+ val saveAction: AsyncAction,
+ val confirmExitAction: AsyncAction,
+ val eventSink: (ChangeRoomPermissionsEvent) -> Unit,
+)
+
+enum class RoomPermissionType {
+ BAN,
+ INVITE,
+ KICK,
+ SEND_EVENTS,
+ REDACT_EVENTS,
+ ROOM_NAME,
+ ROOM_AVATAR,
+ ROOM_TOPIC
+}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsStatePreviewProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsStatePreviewProvider.kt
new file mode 100644
index 0000000000..8bb8073068
--- /dev/null
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsStatePreviewProvider.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2024 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.roomdetails.impl.rolesandpermissions.permissions
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.libraries.architecture.AsyncAction
+import io.element.android.libraries.matrix.api.room.RoomMember
+import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels
+import kotlinx.collections.immutable.toPersistentList
+
+class ChangeRoomPermissionsStatePreviewProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ aChangeRoomPermissionsState(section = ChangeRoomPermissionsSection.RoomDetails),
+ aChangeRoomPermissionsState(section = ChangeRoomPermissionsSection.MessagesAndContent),
+ aChangeRoomPermissionsState(section = ChangeRoomPermissionsSection.MembershipModeration),
+ aChangeRoomPermissionsState(section = ChangeRoomPermissionsSection.RoomDetails, hasChanges = true),
+ aChangeRoomPermissionsState(section = ChangeRoomPermissionsSection.RoomDetails, hasChanges = true, saveAction = AsyncAction.Loading),
+ aChangeRoomPermissionsState(
+ section = ChangeRoomPermissionsSection.RoomDetails,
+ hasChanges = true,
+ saveAction = AsyncAction.Failure(IllegalStateException("Failed to save changes"))
+ ),
+ aChangeRoomPermissionsState(section = ChangeRoomPermissionsSection.RoomDetails, hasChanges = true, confirmExitAction = AsyncAction.Confirming),
+ )
+}
+
+internal fun aChangeRoomPermissionsState(
+ section: ChangeRoomPermissionsSection,
+ currentPermissions: MatrixRoomPowerLevels = previewPermissions(),
+ items: List = ChangeRoomPermissionsPresenter.itemsForSection(section),
+ hasChanges: Boolean = false,
+ saveAction: AsyncAction = AsyncAction.Uninitialized,
+ confirmExitAction: AsyncAction = AsyncAction.Uninitialized,
+ eventSink: (ChangeRoomPermissionsEvent) -> Unit = {},
+) = ChangeRoomPermissionsState(
+ section = section,
+ currentPermissions = currentPermissions,
+ items = items.toPersistentList(),
+ hasChanges = hasChanges,
+ saveAction = saveAction,
+ confirmExitAction = confirmExitAction,
+ eventSink = eventSink,
+)
+
+private fun previewPermissions(): MatrixRoomPowerLevels {
+ return MatrixRoomPowerLevels(
+ // MembershipModeration section
+ 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,
+ // RoomDetails section
+ roomName = RoomMember.Role.ADMIN.powerLevel,
+ roomAvatar = RoomMember.Role.MODERATOR.powerLevel,
+ roomTopic = RoomMember.Role.USER.powerLevel,
+ )
+}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsView.kt
new file mode 100644
index 0000000000..1d57545a0b
--- /dev/null
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsView.kt
@@ -0,0 +1,192 @@
+/*
+ * Copyright (c) 2024 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.roomdetails.impl.rolesandpermissions.permissions
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.compound.tokens.generated.CompoundIcons
+import io.element.android.features.roomdetails.impl.R
+import io.element.android.libraries.core.bool.orFalse
+import io.element.android.libraries.designsystem.components.async.AsyncActionView
+import io.element.android.libraries.designsystem.components.button.BackButton
+import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
+import io.element.android.libraries.designsystem.components.list.ListItemContent
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.designsystem.theme.aliasScreenTitle
+import io.element.android.libraries.designsystem.theme.components.IconSource
+import io.element.android.libraries.designsystem.theme.components.ListItem
+import io.element.android.libraries.designsystem.theme.components.ListItemStyle
+import io.element.android.libraries.designsystem.theme.components.ListSectionHeader
+import io.element.android.libraries.designsystem.theme.components.Scaffold
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.designsystem.theme.components.TextButton
+import io.element.android.libraries.designsystem.theme.components.TopAppBar
+import io.element.android.libraries.matrix.api.room.RoomMember
+import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels
+import io.element.android.libraries.ui.strings.CommonStrings
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ChangeRoomPermissionsView(
+ state: ChangeRoomPermissionsState,
+ onBackPressed: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ BackHandler {
+ state.eventSink(ChangeRoomPermissionsEvent.Exit)
+ }
+ Scaffold(
+ modifier = modifier,
+ topBar = {
+ val title = when (state.section) {
+ ChangeRoomPermissionsSection.RoomDetails -> stringResource(R.string.screen_room_change_permissions_room_details)
+ ChangeRoomPermissionsSection.MessagesAndContent -> stringResource(R.string.screen_room_change_permissions_messages_and_content)
+ ChangeRoomPermissionsSection.MembershipModeration -> stringResource(R.string.screen_room_change_permissions_member_moderation)
+ }
+ TopAppBar(
+ title = { Text(text = title, style = ElementTheme.typography.aliasScreenTitle) },
+ navigationIcon = {
+ BackButton(onClick = { state.eventSink(ChangeRoomPermissionsEvent.Exit) })
+ },
+ actions = {
+ TextButton(
+ text = stringResource(CommonStrings.action_save),
+ onClick = { state.eventSink(ChangeRoomPermissionsEvent.Save) },
+ enabled = state.hasChanges,
+ )
+ }
+ )
+ }
+ ) { padding ->
+ Column(modifier = Modifier.padding(padding)) {
+ for ((index, permissionItem) in state.items.withIndex()) {
+ ListSectionHeader(titleForSection(item = permissionItem), hasDivider = index > 0)
+ SelectRoleItem(
+ permissionsItem = permissionItem,
+ role = RoomMember.Role.ADMIN,
+ currentPermissions = state.currentPermissions
+ ) { item, role ->
+ state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(item, role))
+ }
+ SelectRoleItem(
+ permissionsItem = permissionItem,
+ role = RoomMember.Role.MODERATOR,
+ currentPermissions = state.currentPermissions
+ ) { item, role ->
+ state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(item, role))
+ }
+ SelectRoleItem(
+ permissionsItem = permissionItem,
+ role = RoomMember.Role.USER,
+ currentPermissions = state.currentPermissions
+ ) { item, role ->
+ state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(item, role))
+ }
+ }
+ }
+ }
+
+ AsyncActionView(
+ async = state.saveAction,
+ onSuccess = { onBackPressed() },
+ onErrorDismiss = { state.eventSink(ChangeRoomPermissionsEvent.ResetPendingActions) }
+ )
+
+ AsyncActionView(
+ async = state.confirmExitAction,
+ onSuccess = { onBackPressed() },
+ confirmationDialog = {
+ ConfirmationDialog(
+ title = stringResource(R.string.screen_room_change_role_unsaved_changes_title),
+ content = stringResource(R.string.screen_room_change_role_unsaved_changes_description),
+ submitText = stringResource(CommonStrings.action_save),
+ cancelText = stringResource(CommonStrings.action_discard),
+ onSubmitClicked = { state.eventSink(ChangeRoomPermissionsEvent.Save) },
+ onDismiss = { state.eventSink(ChangeRoomPermissionsEvent.Exit) }
+ )
+ },
+ onErrorDismiss = {},
+ )
+}
+
+@Composable
+private fun SelectRoleItem(
+ permissionsItem: RoomPermissionType,
+ role: RoomMember.Role,
+ currentPermissions: MatrixRoomPowerLevels?,
+ onClick: (RoomPermissionType, RoomMember.Role) -> Unit
+) {
+ val title = when (role) {
+ 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)
+ }
+ ListItem(
+ headlineContent = { Text(text = title) },
+ trailingContent = if (currentPermissions?.isSelected(permissionsItem, role).orFalse()) {
+ ListItemContent.Icon(IconSource.Vector(CompoundIcons.Check()))
+ } else {
+ null
+ },
+ style = ListItemStyle.Primary,
+ onClick = { onClick(permissionsItem, role) },
+ )
+}
+
+private fun MatrixRoomPowerLevels.isSelected(item: RoomPermissionType, role: RoomMember.Role): Boolean {
+ return when (item) {
+ RoomPermissionType.BAN -> RoomMember.Role.forPowerLevel(ban) == role
+ RoomPermissionType.INVITE -> RoomMember.Role.forPowerLevel(invite) == role
+ RoomPermissionType.KICK -> RoomMember.Role.forPowerLevel(kick) == role
+ RoomPermissionType.SEND_EVENTS -> RoomMember.Role.forPowerLevel(sendEvents) == role
+ RoomPermissionType.REDACT_EVENTS -> RoomMember.Role.forPowerLevel(redactEvents) == role
+ RoomPermissionType.ROOM_NAME -> RoomMember.Role.forPowerLevel(roomName) == role
+ RoomPermissionType.ROOM_AVATAR -> RoomMember.Role.forPowerLevel(roomAvatar) == role
+ RoomPermissionType.ROOM_TOPIC -> RoomMember.Role.forPowerLevel(roomTopic) == role
+ }
+}
+
+@Composable
+private fun titleForSection(item: RoomPermissionType): String = when (item) {
+ RoomPermissionType.INVITE -> stringResource(R.string.screen_room_change_permissions_invite_people)
+ RoomPermissionType.KICK -> stringResource(R.string.screen_room_change_permissions_remove_people)
+ RoomPermissionType.BAN -> stringResource(R.string.screen_room_change_permissions_ban_people)
+ RoomPermissionType.SEND_EVENTS -> stringResource(R.string.screen_room_change_permissions_send_messages)
+ RoomPermissionType.REDACT_EVENTS -> stringResource(R.string.screen_room_change_permissions_delete_messages)
+ RoomPermissionType.ROOM_NAME -> stringResource(R.string.screen_room_change_permissions_room_name)
+ RoomPermissionType.ROOM_AVATAR -> stringResource(R.string.screen_room_change_permissions_room_avatar)
+ RoomPermissionType.ROOM_TOPIC -> stringResource(R.string.screen_room_change_permissions_room_topic)
+}
+
+@PreviewsDayNight
+@Composable
+internal fun ChangeRoomPermissionsViewPreview(@PreviewParameter(ChangeRoomPermissionsStatePreviewProvider::class) state: ChangeRoomPermissionsState) {
+ ElementPreview {
+ ChangeRoomPermissionsView(
+ state = state,
+ onBackPressed = {},
+ )
+ }
+}
diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml
index cb5b7e75e2..9fca8935b0 100644
--- a/features/roomdetails/impl/src/main/res/values/localazy.xml
+++ b/features/roomdetails/impl/src/main/res/values/localazy.xml
@@ -18,10 +18,10 @@
"Messages and content"
"Admins and moderators"
"Remove people"
- "Change Room Avatar"
+ "Change room avatar"
"Room details"
- "Change Room Name"
- "Change Room Topic"
+ "Change room name"
+ "Change room topic"
"Send messages"
"Edit Admins"
"You will not be able to undo this action. You are promoting the user to have the same power level as you."
@@ -104,7 +104,7 @@
"Moderators"
"Permissions"
"Reset permissions"
- "Once you reset permissions, you will lose your current settings."
+ "Once you reset permissions, you will lose the current settings."
"Reset permissions?"
"Roles"
"Room details"
diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/RolesAndPermissionPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/RolesAndPermissionPresenterTests.kt
index 0221a6851a..0c81a549b9 100644
--- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/RolesAndPermissionPresenterTests.kt
+++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/RolesAndPermissionPresenterTests.kt
@@ -118,6 +118,36 @@ class RolesAndPermissionPresenterTests {
}
}
+ @Test
+ fun `present - ResetPermissions needs confirmation, then resets permissions`() = runTest {
+ val presenter = createRolesAndPermissionsPresenter()
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ initialState.eventSink(RolesAndPermissionsEvents.ResetPermissions)
+ // Confirmation
+ awaitItem().eventSink(RolesAndPermissionsEvents.ResetPermissions)
+
+ assertThat(awaitItem().resetPermissionsAction).isEqualTo(AsyncAction.Loading)
+ assertThat(awaitItem().resetPermissionsAction).isEqualTo(AsyncAction.Success(Unit))
+ }
+ }
+
+ @Test
+ fun `present - ResetPermissions confirmation can be cancelled`() = runTest {
+ val presenter = createRolesAndPermissionsPresenter()
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ initialState.eventSink(RolesAndPermissionsEvents.ResetPermissions)
+ awaitItem().eventSink(RolesAndPermissionsEvents.CancelPendingAction)
+
+ assertThat(awaitItem().resetPermissionsAction).isEqualTo(AsyncAction.Uninitialized)
+ }
+ }
+
private fun TestScope.createRolesAndPermissionsPresenter(
room: FakeMatrixRoom = FakeMatrixRoom(),
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/RolesAndPermissionsViewTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/RolesAndPermissionsViewTests.kt
index 6eb596ce83..be6411b373 100644
--- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/RolesAndPermissionsViewTests.kt
+++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/RolesAndPermissionsViewTests.kt
@@ -21,19 +21,26 @@ import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.roomdetails.impl.R
+import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsEvents
+import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsNavigator
import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsState
import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsView
-import io.element.android.features.roomdetails.impl.rolesandpermissions.RoomDetailsAdminSettingsNavigator
import io.element.android.features.roomdetails.impl.rolesandpermissions.aRolesAndPermissionsState
+import io.element.android.libraries.architecture.AsyncAction
+import io.element.android.libraries.matrix.api.room.RoomMember
+import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
+import io.element.android.tests.testutils.ensureCalledTimes
import io.element.android.tests.testutils.pressBack
+import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class)
class RolesAndPermissionsViewTests {
@@ -68,6 +75,100 @@ class RolesAndPermissionsViewTests {
rule.clickOn(R.string.screen_room_roles_and_permissions_moderators)
}
}
+
+ @Test
+ @Config(qualifiers = "h640dp")
+ fun `tapping on any of the permission items open the change permissions screen`() {
+ ensureCalledTimes(3) { callback ->
+ rule.setRolesAndPermissionsView(
+ openPermissionScreens = callback,
+ )
+ rule.clickOn(R.string.screen_room_roles_and_permissions_room_details)
+ rule.clickOn(R.string.screen_room_roles_and_permissions_messages_and_content)
+ rule.clickOn(R.string.screen_room_roles_and_permissions_member_moderation)
+ }
+ }
+
+ @Test
+ @Config(qualifiers = "h640dp")
+ fun `tapping on reset permissions triggers ResetPermissions event`() {
+ val recorder = EventsRecorder()
+ rule.setRolesAndPermissionsView(
+ state = aRolesAndPermissionsState(
+ eventSink = recorder,
+ ),
+ )
+ rule.clickOn(R.string.screen_room_roles_and_permissions_reset)
+ recorder.assertSingle(RolesAndPermissionsEvents.ResetPermissions)
+ }
+
+ @Test
+ fun `tapping on Reset in the reset permissions confirmation dialog triggers ResetPermissions event`() {
+ val recorder = EventsRecorder()
+ rule.setRolesAndPermissionsView(
+ state = aRolesAndPermissionsState(
+ resetPermissionsAction = AsyncAction.Confirming,
+ eventSink = recorder,
+ ),
+ )
+ rule.clickOn(CommonStrings.action_reset)
+ recorder.assertSingle(RolesAndPermissionsEvents.ResetPermissions)
+ }
+
+ @Test
+ fun `tapping on Cancel in the reset permissions confirmation dialog triggers CancelPendingAction event`() {
+ val recorder = EventsRecorder()
+ rule.setRolesAndPermissionsView(
+ state = aRolesAndPermissionsState(
+ resetPermissionsAction = AsyncAction.Confirming,
+ eventSink = recorder,
+ ),
+ )
+ rule.clickOn(CommonStrings.action_cancel)
+ recorder.assertSingle(RolesAndPermissionsEvents.CancelPendingAction)
+ }
+
+ @Test
+ fun `tapping on 'Demote to moderator' in the demote self bottom sheet triggers the right event`() {
+ val recorder = EventsRecorder()
+ rule.setRolesAndPermissionsView(
+ state = aRolesAndPermissionsState(
+ changeOwnRoleAction = AsyncAction.Confirming,
+ eventSink = recorder,
+ ),
+ )
+ 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))
+ }
+
+ @Test
+ fun `tapping on 'Demote to member' in the demote self bottom sheet triggers the right event`() = runTest {
+ val recorder = EventsRecorder()
+ rule.setRolesAndPermissionsView(
+ state = aRolesAndPermissionsState(
+ changeOwnRoleAction = AsyncAction.Confirming,
+ eventSink = recorder,
+ ),
+ )
+ 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))
+ }
+
+ @Test
+ fun `tapping on 'Cancel' in the demote self bottom sheet triggers the right event`() {
+ val recorder = EventsRecorder()
+ rule.setRolesAndPermissionsView(
+ state = aRolesAndPermissionsState(
+ changeOwnRoleAction = AsyncAction.Confirming,
+ eventSink = recorder,
+ ),
+ )
+ rule.clickOn(CommonStrings.action_cancel)
+ rule.mainClock.advanceTimeBy(1_000L)
+ recorder.assertSingle(RolesAndPermissionsEvents.CancelPendingAction)
+ }
}
private fun AndroidComposeTestRule.setRolesAndPermissionsView(
@@ -77,14 +178,18 @@ private fun AndroidComposeTestRule.setRoles
goBack: () -> Unit = EnsureNeverCalled(),
openAdminList: () -> Unit = EnsureNeverCalled(),
openModeratorList: () -> Unit = EnsureNeverCalled(),
+ openPermissionScreens: () -> Unit = EnsureNeverCalled(),
) {
setContent {
RolesAndPermissionsView(
state = state,
- roomDetailsAdminSettingsNavigator = object : RoomDetailsAdminSettingsNavigator {
+ rolesAndPermissionsNavigator = object : RolesAndPermissionsNavigator {
override fun onBackPressed() = goBack()
override fun openAdminList() = openAdminList()
override fun openModeratorList() = openModeratorList()
+ override fun openEditRoomDetailsPermissions() = openPermissionScreens()
+ override fun openModerationPermissions() = openPermissionScreens()
+ override fun openMessagesAndContentPermissions() = openPermissionScreens()
}
)
}
diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/permissions/ChangeRoomPermissionsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/permissions/ChangeRoomPermissionsPresenterTests.kt
new file mode 100644
index 0000000000..513438b710
--- /dev/null
+++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/permissions/ChangeRoomPermissionsPresenterTests.kt
@@ -0,0 +1,294 @@
+/*
+ * Copyright (c) 2024 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.roomdetails.rolesandpermissions.permissions
+
+import app.cash.molecule.RecompositionMode
+import app.cash.molecule.moleculeFlow
+import app.cash.turbine.Event
+import app.cash.turbine.TurbineTestContext
+import app.cash.turbine.test
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.ChangeRoomPermissionsEvent
+import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.ChangeRoomPermissionsPresenter
+import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.ChangeRoomPermissionsSection
+import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.ChangeRoomPermissionsState
+import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.RoomPermissionType
+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.powerlevels.MatrixRoomPowerLevels
+import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
+import io.element.android.libraries.matrix.test.room.defaultRoomPowerLevels
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class ChangeRoomPermissionsPresenterTests {
+ @Test
+ fun `present - initial state`() = runTest {
+ val section = ChangeRoomPermissionsSection.RoomDetails
+ val presenter = createChangeRoomPermissionsPresenter(section = section)
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ // Initial state, no permissions loaded
+ awaitItem().run {
+ assertThat(this.section).isEqualTo(section)
+ assertThat(this.currentPermissions).isNull()
+ assertThat(this.items).isNotEmpty()
+ assertThat(this.hasChanges).isFalse()
+ assertThat(this.saveAction).isEqualTo(AsyncAction.Uninitialized)
+ assertThat(this.confirmExitAction).isEqualTo(AsyncAction.Uninitialized)
+ }
+
+ // Updated state, permissions loaded
+ assertThat(awaitItem().currentPermissions).isEqualTo(defaultPermissions())
+ }
+ }
+
+ @Test
+ fun `present - RoomDetails section contains the right items`() = runTest {
+ val section = ChangeRoomPermissionsSection.RoomDetails
+ val presenter = createChangeRoomPermissionsPresenter(section = section)
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ assertThat(awaitUpdatedItem().items).containsExactly(
+ RoomPermissionType.ROOM_NAME,
+ RoomPermissionType.ROOM_AVATAR,
+ RoomPermissionType.ROOM_TOPIC,
+ )
+ }
+ }
+
+ @Test
+ fun `present - MessagesAndContent section contains the right items`() = runTest {
+ val section = ChangeRoomPermissionsSection.MessagesAndContent
+ val presenter = createChangeRoomPermissionsPresenter(section = section)
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ assertThat(awaitUpdatedItem().items).containsExactly(
+ RoomPermissionType.SEND_EVENTS,
+ RoomPermissionType.REDACT_EVENTS,
+ )
+ }
+ }
+
+ @Test
+ fun `present - MembershipModeration section contains the right items`() = runTest {
+ val section = ChangeRoomPermissionsSection.MembershipModeration
+ val presenter = createChangeRoomPermissionsPresenter(section = section)
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ assertThat(awaitUpdatedItem().items).containsExactly(
+ RoomPermissionType.INVITE,
+ RoomPermissionType.KICK,
+ RoomPermissionType.BAN,
+ )
+ }
+ }
+
+ @Test
+ fun `present - ChangeMinimumRoleForAction updates the current permissions and hasChanges`() = runTest {
+ val presenter = createChangeRoomPermissionsPresenter()
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ val state = awaitUpdatedItem()
+ assertThat(state.currentPermissions?.roomName).isEqualTo(ADMIN.powerLevel)
+ assertThat(state.hasChanges).isFalse()
+
+ state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, MODERATOR))
+
+ awaitItem().run {
+ assertThat(currentPermissions?.roomName).isEqualTo(MODERATOR.powerLevel)
+ assertThat(hasChanges).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun `present - ChangeMinimumRoleForAction works for all actions`() = runTest {
+ val presenter = createChangeRoomPermissionsPresenter()
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.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))
+
+ val items = cancelAndConsumeRemainingEvents()
+
+ (items.last() as? Event.Item)?.value?.run {
+ assertThat(currentPermissions).isEqualTo(
+ MatrixRoomPowerLevels(
+ invite = MODERATOR.powerLevel,
+ kick = MODERATOR.powerLevel,
+ ban = MODERATOR.powerLevel,
+ redactEvents = MODERATOR.powerLevel,
+ sendEvents = MODERATOR.powerLevel,
+ roomName = MODERATOR.powerLevel,
+ roomAvatar = MODERATOR.powerLevel,
+ roomTopic = MODERATOR.powerLevel,
+ )
+ )
+ }
+ }
+ }
+
+ @Test
+ fun `present - Save updates the current permissions and resets hasChanges`() = runTest {
+ val presenter = createChangeRoomPermissionsPresenter()
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ val state = awaitUpdatedItem()
+ assertThat(state.currentPermissions?.roomName).isEqualTo(ADMIN.powerLevel)
+ assertThat(state.hasChanges).isFalse()
+
+ state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, MODERATOR))
+ assertThat(awaitItem().hasChanges).isTrue()
+
+ state.eventSink(ChangeRoomPermissionsEvent.Save)
+
+ assertThat(awaitItem().saveAction).isEqualTo(AsyncAction.Loading)
+ assertThat(awaitItem().hasChanges).isFalse()
+ awaitItem().run {
+ assertThat(currentPermissions?.roomName).isEqualTo(MODERATOR.powerLevel)
+ assertThat(saveAction).isEqualTo(AsyncAction.Success(Unit))
+ }
+ }
+ }
+
+ @Test
+ fun `present - Save will fail if there are not current permissions`() = runTest {
+ val room = FakeMatrixRoom().apply {
+ givenPowerLevelsResult(Result.failure(IllegalStateException("Failed to load power levels")))
+ }
+ val presenter = createChangeRoomPermissionsPresenter(room = room)
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ val state = awaitItem()
+ assertThat(state.currentPermissions).isNull()
+
+ state.eventSink(ChangeRoomPermissionsEvent.Save)
+ assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Failure::class.java)
+ }
+ }
+
+ @Test
+ fun `present - Save can handle failures and they can be cleared`() = runTest {
+ val room = FakeMatrixRoom().apply {
+ givenUpdatePowerLevelsResult(Result.failure(IllegalStateException("Failed to update power levels")))
+ }
+ val presenter = createChangeRoomPermissionsPresenter(room = room)
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ val state = awaitUpdatedItem()
+ assertThat(state.currentPermissions?.roomName).isEqualTo(ADMIN.powerLevel)
+ assertThat(state.hasChanges).isFalse()
+
+ 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)
+ // Couldn't save the changes, so they're still pending
+ assertThat(hasChanges).isTrue()
+ assertThat(saveAction).isInstanceOf(AsyncAction.Failure::class.java)
+ }
+
+ state.eventSink(ChangeRoomPermissionsEvent.ResetPendingActions)
+ awaitItem().run {
+ assertThat(currentPermissions?.roomName).isEqualTo(MODERATOR.powerLevel)
+ assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized)
+ assertThat(hasChanges).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun `present - Exit does not need a confirmation when there are no pending changes`() = runTest {
+ val presenter = createChangeRoomPermissionsPresenter()
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ val state = awaitUpdatedItem()
+ state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, MODERATOR))
+ assertThat(awaitItem().hasChanges).isTrue()
+
+ state.eventSink(ChangeRoomPermissionsEvent.Exit)
+ assertThat(awaitItem().confirmExitAction).isEqualTo(AsyncAction.Confirming)
+
+ state.eventSink(ChangeRoomPermissionsEvent.Exit)
+ assertThat(awaitItem().confirmExitAction).isEqualTo(AsyncAction.Success(Unit))
+ }
+ }
+
+ @Test
+ fun `present - Exit needs confirmation when there are pending changes`() = runTest {
+ val presenter = createChangeRoomPermissionsPresenter()
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ val state = awaitUpdatedItem()
+
+ state.eventSink(ChangeRoomPermissionsEvent.Exit)
+
+ assertThat(awaitItem().confirmExitAction).isEqualTo(AsyncAction.Success(Unit))
+ }
+ }
+
+ private fun createChangeRoomPermissionsPresenter(
+ section: ChangeRoomPermissionsSection = ChangeRoomPermissionsSection.RoomDetails,
+ room: FakeMatrixRoom = FakeMatrixRoom(),
+ ) = ChangeRoomPermissionsPresenter(
+ section = section,
+ room = room,
+ )
+
+ private fun defaultPermissions() = defaultRoomPowerLevels().run {
+ MatrixRoomPowerLevels(
+ invite = invite,
+ kick = kick,
+ ban = ban,
+ redactEvents = redactEvents,
+ sendEvents = sendEvents,
+ roomName = roomName,
+ roomAvatar = roomAvatar,
+ roomTopic = roomTopic,
+ )
+ }
+
+ private suspend fun TurbineTestContext.awaitUpdatedItem(): ChangeRoomPermissionsState {
+ skipItems(1)
+ return awaitItem()
+ }
+}
diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/permissions/ChangeRoomPermissionsViewTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/permissions/ChangeRoomPermissionsViewTests.kt
new file mode 100644
index 0000000000..942b40ff7a
--- /dev/null
+++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/permissions/ChangeRoomPermissionsViewTests.kt
@@ -0,0 +1,201 @@
+/*
+ * Copyright (c) 2024 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.roomdetails.rolesandpermissions.permissions
+
+import androidx.activity.ComponentActivity
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onAllNodesWithText
+import androidx.compose.ui.test.onFirst
+import androidx.compose.ui.test.performClick
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import io.element.android.features.roomdetails.impl.R
+import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.ChangeRoomPermissionsEvent
+import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.ChangeRoomPermissionsSection
+import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.ChangeRoomPermissionsState
+import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.ChangeRoomPermissionsView
+import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.RoomPermissionType
+import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.aChangeRoomPermissionsState
+import io.element.android.libraries.architecture.AsyncAction
+import io.element.android.libraries.matrix.api.room.RoomMember
+import io.element.android.libraries.ui.strings.CommonStrings
+import io.element.android.tests.testutils.EnsureNeverCalled
+import io.element.android.tests.testutils.EventsRecorder
+import io.element.android.tests.testutils.clickOn
+import io.element.android.tests.testutils.clickOnFirst
+import io.element.android.tests.testutils.ensureCalledOnce
+import io.element.android.tests.testutils.pressBack
+import io.element.android.tests.testutils.pressBackKey
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestRule
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ChangeRoomPermissionsViewTests {
+ @get:Rule val rule = createAndroidComposeRule()
+
+ @Test
+ fun `click on back icon invokes Exit`() {
+ val recorder = EventsRecorder()
+ rule.setChangeRoomPermissionsRule(
+ eventsRecorder = recorder,
+ )
+ rule.pressBack()
+ recorder.assertSingle(ChangeRoomPermissionsEvent.Exit)
+ }
+
+ @Test
+ fun `click on back key invokes Exit`() {
+ val recorder = EventsRecorder()
+ rule.setChangeRoomPermissionsRule(
+ eventsRecorder = recorder,
+ )
+ rule.pressBackKey()
+ recorder.assertSingle(ChangeRoomPermissionsEvent.Exit)
+ }
+
+ @Test
+ fun `when confirming exit with pending changes, using the back key actually exits`() {
+ val recorder = EventsRecorder()
+ rule.setChangeRoomPermissionsRule(
+ state = aChangeRoomPermissionsState(
+ section = ChangeRoomPermissionsSection.RoomDetails,
+ hasChanges = true,
+ eventSink = recorder,
+ ),
+ eventsRecorder = recorder,
+ )
+ rule.pressBackKey()
+ recorder.assertSingle(ChangeRoomPermissionsEvent.Exit)
+ }
+
+ @Test
+ fun `when confirming exit with pending changes, clicking on 'discard' button in the dialog actually exits`() {
+ val recorder = EventsRecorder()
+ rule.setChangeRoomPermissionsRule(
+ state = aChangeRoomPermissionsState(
+ section = ChangeRoomPermissionsSection.RoomDetails,
+ hasChanges = true,
+ confirmExitAction = AsyncAction.Confirming,
+ eventSink = recorder,
+ ),
+ eventsRecorder = recorder,
+ )
+ rule.clickOn(CommonStrings.action_discard)
+ recorder.assertSingle(ChangeRoomPermissionsEvent.Exit)
+ }
+
+ @Test
+ fun `when confirming exit with pending changes, clicking on 'save' button in the dialog saves the changes`() {
+ val recorder = EventsRecorder()
+ rule.setChangeRoomPermissionsRule(
+ state = aChangeRoomPermissionsState(
+ section = ChangeRoomPermissionsSection.RoomDetails,
+ hasChanges = true,
+ confirmExitAction = AsyncAction.Confirming,
+ eventSink = recorder,
+ ),
+ eventsRecorder = recorder,
+ )
+ rule.clickOnFirst(CommonStrings.action_save)
+ recorder.assertSingle(ChangeRoomPermissionsEvent.Save)
+ }
+
+ @Test
+ fun `click on a role item triggers ChangeRole event`() {
+ val recorder = EventsRecorder()
+ rule.setChangeRoomPermissionsRule(
+ eventsRecorder = recorder,
+ )
+ val admins = rule.activity.getText(R.string.screen_room_change_permissions_administrators).toString()
+ val moderators = rule.activity.getText(R.string.screen_room_change_permissions_moderators).toString()
+ val users = rule.activity.getText(R.string.screen_room_change_permissions_everyone).toString()
+ rule.onAllNodesWithText(admins).onFirst().performClick()
+ rule.onAllNodesWithText(moderators).onFirst().performClick()
+ 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),
+ )
+ )
+ }
+
+ @Test
+ fun `click on the Save menu item triggers Save event`() {
+ val recorder = EventsRecorder()
+ rule.setChangeRoomPermissionsRule(
+ state = aChangeRoomPermissionsState(
+ section = ChangeRoomPermissionsSection.RoomDetails,
+ hasChanges = true,
+ eventSink = recorder,
+ ),
+ eventsRecorder = recorder,
+ )
+ rule.clickOn(CommonStrings.action_save)
+ recorder.assertSingle(ChangeRoomPermissionsEvent.Save)
+ }
+
+ @Test
+ fun `a successful save exits the screen`() {
+ ensureCalledOnce { callback ->
+ rule.setChangeRoomPermissionsRule(
+ state = aChangeRoomPermissionsState(
+ section = ChangeRoomPermissionsSection.RoomDetails,
+ hasChanges = true,
+ saveAction = AsyncAction.Success(Unit),
+ ),
+ onBackPressed = callback
+ )
+ rule.clickOn(CommonStrings.action_save)
+ }
+ }
+
+ @Test
+ fun `click on the Ok option in save error dialog triggers ResetPendingAction event`() {
+ val recorder = EventsRecorder()
+ rule.setChangeRoomPermissionsRule(
+ state = aChangeRoomPermissionsState(
+ section = ChangeRoomPermissionsSection.RoomDetails,
+ hasChanges = true,
+ saveAction = AsyncAction.Failure(IllegalStateException("Failed to set room power levels")),
+ eventSink = recorder,
+ ),
+ eventsRecorder = recorder,
+ )
+ rule.clickOn(CommonStrings.action_ok)
+ recorder.assertSingle(ChangeRoomPermissionsEvent.ResetPendingActions)
+ }
+}
+
+private fun AndroidComposeTestRule.setChangeRoomPermissionsRule(
+ eventsRecorder: EventsRecorder = EventsRecorder(expectEvents = false),
+ state: ChangeRoomPermissionsState = aChangeRoomPermissionsState(
+ section = ChangeRoomPermissionsSection.RoomDetails,
+ eventSink = eventsRecorder,
+ ),
+ onBackPressed: () -> Unit = EnsureNeverCalled(),
+) {
+ setContent {
+ ChangeRoomPermissionsView(
+ state = state,
+ onBackPressed = onBackPressed,
+ )
+ }
+}
diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt
index 857bc0b89e..2e86648f7b 100644
--- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt
+++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt
@@ -170,7 +170,7 @@ class StateContentFormatter @Inject constructor(
"RoomPinnedEvents"
}
}
- is OtherState.RoomPowerLevels -> when (renderingMode) {
+ is OtherState.RoomUserPowerLevels -> when (renderingMode) {
RenderingMode.RoomList -> {
Timber.v("Filtering timeline item for room state change: $content")
null
diff --git a/libraries/eventformatter/impl/src/main/res/values/localazy.xml b/libraries/eventformatter/impl/src/main/res/values/localazy.xml
index 525a9435fe..53589974fe 100644
--- a/libraries/eventformatter/impl/src/main/res/values/localazy.xml
+++ b/libraries/eventformatter/impl/src/main/res/values/localazy.xml
@@ -3,12 +3,16 @@
"(avatar was changed too)"
"%1$s changed their avatar"
"You changed your avatar"
+ "%1$s was demoted to member"
+ "%1$s was demoted to moderator"
"%1$s changed their display name from %2$s to %3$s"
"You changed your display name from %1$s to %2$s"
"%1$s removed their display name (it was %2$s)"
"You removed your display name (it was %1$s)"
"%1$s set their display name to %2$s"
"You set your display name to %1$s"
+ "%1$s was promoted to admin"
+ "%1$s was promoted to moderator"
"%1$s changed the room avatar"
"You changed the room avatar"
"%1$s removed the room avatar"
diff --git a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTest.kt b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTest.kt
index 5165e5dadf..b127911cc0 100644
--- a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTest.kt
+++ b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTest.kt
@@ -650,7 +650,7 @@ class DefaultRoomLastMessageFormatterTest {
OtherState.RoomHistoryVisibility,
OtherState.RoomJoinRules,
OtherState.RoomPinnedEvents,
- OtherState.RoomPowerLevels(emptyMap()),
+ OtherState.RoomUserPowerLevels(emptyMap()),
OtherState.RoomServerAcl,
OtherState.RoomTombstone,
OtherState.SpaceChild,
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt
index 97285a345a..f049bd1deb 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt
@@ -29,6 +29,7 @@ import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.location.AssetType
+import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.ReceiptType
@@ -97,6 +98,12 @@ interface MatrixRoom : Closeable {
suspend fun unsubscribeFromSync()
+ suspend fun powerLevels(): Result
+
+ suspend fun updatePowerLevels(matrixRoomPowerLevels: MatrixRoomPowerLevels): Result
+
+ suspend fun resetPowerLevels(): Result
+
suspend fun userRole(userId: UserId): Result
suspend fun updateUsersRoles(changes: List): Result
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomPowerLevels.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomPowerLevels.kt
index 2950788c68..f2577fcb6b 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomPowerLevels.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomPowerLevels.kt
@@ -20,6 +20,17 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.StateEventType
+data class MatrixRoomPowerLevels(
+ val ban: Long,
+ val invite: Long,
+ val kick: Long,
+ val sendEvents: Long,
+ val redactEvents: Long,
+ val roomName: Long,
+ val roomAvatar: Long,
+ val roomTopic: Long,
+)
+
/**
* Shortcut for calling [MatrixRoom.canUserInvite] with our own user.
*/
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/OtherState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/OtherState.kt
index 90b30cc1f9..d963b71a63 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/OtherState.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/OtherState.kt
@@ -33,7 +33,7 @@ sealed interface OtherState {
data object RoomJoinRules : OtherState
data class RoomName(val name: String?) : OtherState
data object RoomPinnedEvents : OtherState
- data class RoomPowerLevels(val users: Map) : OtherState
+ data class RoomUserPowerLevels(val users: Map) : OtherState
data object RoomServerAcl : OtherState
data class RoomThirdPartyInvite(val displayName: String?) : OtherState
data object RoomTombstone : OtherState
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt
index 26e0036415..9a85f25502 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt
@@ -39,6 +39,7 @@ import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.location.AssetType
+import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
import io.element.android.libraries.matrix.api.room.roomNotificationSettings
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
@@ -54,6 +55,7 @@ import io.element.android.libraries.matrix.impl.poll.toInner
import io.element.android.libraries.matrix.impl.room.location.toInner
import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher
import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
+import io.element.android.libraries.matrix.impl.room.powerlevels.RoomPowerLevelsMapper
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
import io.element.android.libraries.matrix.impl.timeline.toRustReceiptType
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
@@ -86,6 +88,7 @@ import org.matrix.rustcomponents.sdk.messageEventContentFromHtml
import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown
import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
+import uniffi.matrix_sdk.RoomPowerLevelChanges
import java.io.File
import org.matrix.rustcomponents.sdk.Room as InnerRoom
import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline
@@ -253,6 +256,34 @@ class RustMatrixRoom(
}
}
+ override suspend fun powerLevels(): Result = withContext(roomDispatcher) {
+ runCatching {
+ RoomPowerLevelsMapper.map(innerRoom.getPowerLevels())
+ }
+ }
+
+ override suspend fun updatePowerLevels(matrixRoomPowerLevels: MatrixRoomPowerLevels): Result = withContext(roomDispatcher) {
+ runCatching {
+ val changes = RoomPowerLevelChanges(
+ ban = matrixRoomPowerLevels.ban,
+ invite = matrixRoomPowerLevels.invite,
+ kick = matrixRoomPowerLevels.kick,
+ redact = matrixRoomPowerLevels.redactEvents,
+ eventsDefault = matrixRoomPowerLevels.sendEvents,
+ roomName = matrixRoomPowerLevels.roomName,
+ roomAvatar = matrixRoomPowerLevels.roomAvatar,
+ roomTopic = matrixRoomPowerLevels.roomTopic,
+ )
+ innerRoom.applyPowerLevelChanges(changes)
+ }
+ }
+
+ override suspend fun resetPowerLevels(): Result = withContext(roomDispatcher) {
+ runCatching {
+ RoomPowerLevelsMapper.map(innerRoom.resetPowerLevels())
+ }
+ }
+
override suspend fun userAvatarUrl(userId: UserId): Result = withContext(roomDispatcher) {
runCatching {
innerRoom.memberAvatarUrl(userId.value)
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/powerlevels/RoomPowerLevelsMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/powerlevels/RoomPowerLevelsMapper.kt
new file mode 100644
index 0000000000..fdf8cdbf85
--- /dev/null
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/powerlevels/RoomPowerLevelsMapper.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2024 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.matrix.impl.room.powerlevels
+
+import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels
+import org.matrix.rustcomponents.sdk.RoomPowerLevels as RustRoomPowerLevels
+
+object RoomPowerLevelsMapper {
+ fun map(roomPowerLevels: RustRoomPowerLevels): MatrixRoomPowerLevels {
+ return MatrixRoomPowerLevels(
+ ban = roomPowerLevels.ban,
+ invite = roomPowerLevels.invite,
+ kick = roomPowerLevels.kick,
+ sendEvents = roomPowerLevels.eventsDefault,
+ redactEvents = roomPowerLevels.redact,
+ roomName = roomPowerLevels.roomName,
+ roomAvatar = roomPowerLevels.roomAvatar,
+ roomTopic = roomPowerLevels.roomTopic
+ )
+ }
+}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt
index 0bef1744cc..c2f08f778e 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt
@@ -163,7 +163,7 @@ private fun RustOtherState.map(): OtherState {
RustOtherState.RoomJoinRules -> OtherState.RoomJoinRules
is RustOtherState.RoomName -> OtherState.RoomName(name)
RustOtherState.RoomPinnedEvents -> OtherState.RoomPinnedEvents
- is RustOtherState.RoomPowerLevels -> OtherState.RoomPowerLevels(users)
+ is RustOtherState.RoomPowerLevels -> OtherState.RoomUserPowerLevels(users)
RustOtherState.RoomServerAcl -> OtherState.RoomServerAcl
is RustOtherState.RoomThirdPartyInvite -> OtherState.RoomThirdPartyInvite(displayName)
RustOtherState.RoomTombstone -> OtherState.RoomTombstone
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
index a1e51b4f7b..56d63fe1d8 100644
--- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
@@ -40,6 +40,7 @@ import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.location.AssetType
+import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.ReceiptType
@@ -125,6 +126,9 @@ class FakeMatrixRoom(
private var canUserTriggerRoomNotificationResult: Result = Result.success(true)
private var canUserJoinCallResult: Result = Result.success(true)
private var setIsFavoriteResult = Result.success(Unit)
+ private var powerLevelsResult = Result.success(defaultRoomPowerLevels())
+ private var updatePowerLevelsResult = Result.success(Unit)
+ private var resetPowerLevelsResult = Result.success(defaultRoomPowerLevels())
var sendMessageMentions = emptyList()
val editMessageCalls = mutableListOf>()
private val _typingRecord = mutableListOf()
@@ -204,6 +208,17 @@ class FakeMatrixRoom(
override suspend fun subscribeToSync() = Unit
override suspend fun unsubscribeFromSync() = Unit
+ override suspend fun powerLevels(): Result {
+ return powerLevelsResult
+ }
+
+ override suspend fun updatePowerLevels(matrixRoomPowerLevels: MatrixRoomPowerLevels): Result = simulateLongTask {
+ updatePowerLevelsResult
+ }
+
+ override suspend fun resetPowerLevels(): Result = simulateLongTask {
+ resetPowerLevelsResult
+ }
override fun destroy() = Unit
@@ -676,6 +691,18 @@ class FakeMatrixRoom(
fun givenRoomTypingMembers(typingMembers: List) {
_roomTypingMembersFlow.tryEmit(typingMembers)
}
+
+ fun givenPowerLevelsResult(result: Result) {
+ powerLevelsResult = result
+ }
+
+ fun givenUpdatePowerLevelsResult(result: Result) {
+ updatePowerLevelsResult = result
+ }
+
+ fun givenResetPowerLevelsResult(result: Result) {
+ resetPowerLevelsResult = result
+ }
}
data class SendLocationInvocation(
@@ -752,3 +779,14 @@ fun aRoomInfo(
userPowerLevels = userPowerLevels,
activeRoomCallParticipants = activeRoomCallParticipants.toImmutableList(),
)
+
+fun defaultRoomPowerLevels() = MatrixRoomPowerLevels(
+ ban = 50,
+ invite = 0,
+ kick = 50,
+ sendEvents = 0,
+ redactEvents = 50,
+ roomName = 100,
+ roomAvatar = 100,
+ roomTopic = 100
+)
diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml
index c44401618f..c8ee3c3d08 100644
--- a/libraries/ui-strings/src/main/res/values/localazy.xml
+++ b/libraries/ui-strings/src/main/res/values/localazy.xml
@@ -49,6 +49,7 @@
"Decline"
"Delete Poll"
"Disable"
+ "Discard"
"Done"
"Edit"
"Edit poll"
diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureCalledOnce.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureCalledOnce.kt
index 9789aff870..ba0b2dbf88 100644
--- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureCalledOnce.kt
+++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureCalledOnce.kt
@@ -29,12 +29,31 @@ class EnsureCalledOnce : () -> Unit {
}
}
+class EnsureCalledTimes(val times: Int) : () -> Unit {
+ private var counter = 0
+ override fun invoke() {
+ counter++
+ }
+
+ fun assertSuccess() {
+ if (counter != times) {
+ throw AssertionError("Expected to be called $times, but was called $counter times")
+ }
+ }
+}
+
fun ensureCalledOnce(block: (callback: () -> Unit) -> Unit) {
val callback = EnsureCalledOnce()
block(callback)
callback.assertSuccess()
}
+fun ensureCalledTimes(times: Int, block: (callback: () -> Unit) -> Unit) {
+ val callback = EnsureCalledTimes(times)
+ block(callback)
+ callback.assertSuccess()
+}
+
class EnsureCalledOnceWithParam(
private val expectedParam: T,
private val result: R,
diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/SemanticsNodeInteractionsProviderExtensions.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/SemanticsNodeInteractionsProviderExtensions.kt
index 1ebbd80acf..da6e67a91d 100644
--- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/SemanticsNodeInteractionsProviderExtensions.kt
+++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/SemanticsNodeInteractionsProviderExtensions.kt
@@ -24,6 +24,7 @@ import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.performClick
import io.element.android.libraries.ui.strings.CommonStrings
import org.junit.rules.TestRule
@@ -34,6 +35,16 @@ fun AndroidComposeTestRule.clickOn(@StringR
.performClick()
}
+fun AndroidComposeTestRule.clickOnFirst(@StringRes res: Int) {
+ val text = activity.getString(res)
+ onAllNodes(hasText(text) and hasClickAction()).onFirst().performClick()
+}
+
+fun AndroidComposeTestRule.clickOnLast(@StringRes res: Int) {
+ val text = activity.getString(res)
+ onAllNodes(hasText(text) and hasClickAction()).onFirst().performClick()
+}
+
/**
* Press the back button in the app bar.
*/
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..c1fa064495
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cce394023e7c5bac40bcf234f9073196b1eb424893760f29d609eb329f62817f
+size 42587
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..308abc0e92
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:97eaf8930ebc7c127dddb12a1002161aa10b120f87ce88779ea96ff35790e344
+size 38372
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_2,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..8428ee7714
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_2,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2d6818023d37493825ba7b929470152847c397b5f19a5bd8ee862fc900dee690
+size 40755
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_3,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..52135a6c75
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_3,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:86aa47d55841e5041bf56534852dd716b4546cc3657a30133b88c0c87998f16c
+size 42464
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_4,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..d7bdf55419
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_4,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:237125629b6449d6898391235b625248b71629b4d96415b80fb7ef5e773e86b2
+size 40484
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_5,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..6c7a7c2b33
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_5,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:753eecb930bf0659671f2f1ecb2a64ae226aac96c5365ed549c635fd90c8c729
+size 40951
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_6,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..bca446759d
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_6,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d0b37566f62e30fcb4f16364a9ac2c008a98a5429c3fa5724d7ef6455e79cf34
+size 47995
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..35d6c4ad87
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dff4c4a80156b2977da007d6258d1cd6cefc93bc709ec33bff0e1b484604cf0a
+size 38506
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..3e0e7b54fd
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:635c8d6bdaafb07706d95d5a9cb9a4e828ef312c31b33a80f6deff1a1c3b206f
+size 34554
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_2,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..97de47b574
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_2,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:af5a843cc9cd5a51d986ba7dae7d2c6a088f862f46bac961667a1fb1d62b8a20
+size 36966
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_3,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..e7363aa84b
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_3,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:059e598dc973e8c01dba4ef49accf47ea2bd74377dfbfb441c3c213fc77a1cee
+size 38158
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_4,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..7f5fe9dbc4
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_4,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c34415877b279b2018d02667cc98b4857f4ae5bfa68d169f3226a0c99ed4620b
+size 36699
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_5,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..17ca2569d6
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_5,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dc02c16667b78ca5a9985da6816f17bbd0f7bb2307569aacaf3391ccd8431bd5
+size 36567
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_6,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..9d7aa5c1be
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_6,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4faece23a4780f5465abe42587f56933ecded55bf3ae966a9e6b634e7df1e049
+size 42845
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Day-8_9_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Day-8_9_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..6b04a5962e
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Day-8_9_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ee5c36535582ecb91b7315afa70bae38db2c7287760071293dbf5fac2b8000b4
+size 43609
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Day-8_9_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Day-8_9_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..0676dcd329
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Day-8_9_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4c5d2d8a8e672c19e1ed9cd37291a6355f815fcd55157fca7a5966bba0baa830
+size 43303
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-8_9_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Day-8_9_null_2,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-8_9_null_2,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Day-8_9_null_2,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Day-8_9_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Day-8_9_null_3,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..ba495911fc
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Day-8_9_null_3,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3c63e9045666c863d13865f4d40563a57ee7ec2110de023455a16d400eb67d26
+size 41977
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Day-8_9_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Day-8_9_null_4,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..598e940ff8
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Day-8_9_null_4,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:905960605341e3cf5987025bdc560e0141f1b5f1ff5e5b114d3f698712e3428d
+size 39869
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Day-8_9_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Day-8_9_null_5,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..c4eb3d509f
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Day-8_9_null_5,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7cd6b2f0b51fe273ee5479e77a4762d4addb8e59560edbbb4a3ce4520ceb4a75
+size 51493
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Day-8_9_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Day-8_9_null_6,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..ba495911fc
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Day-8_9_null_6,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3c63e9045666c863d13865f4d40563a57ee7ec2110de023455a16d400eb67d26
+size 41977
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Day-8_9_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Day-8_9_null_7,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..d3cc403fb5
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Day-8_9_null_7,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8c25b6b3d2699895a4b34a61864d2b57aad9f44d5488830f5f2e061fddf42e32
+size 40355
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Night-8_10_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Night-8_10_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..3e3bf2effa
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Night-8_10_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:09a844501c3aea35d5ec151415b25c5be8960b98697a99dba450c8a25fb69c8b
+size 39847
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Night-8_10_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Night-8_10_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..9d78e2116d
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Night-8_10_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5d262c2c0681ea12ce6920af395bd56aa58a97448ade03997cd22e921d1e2f77
+size 39564
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-8_10_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Night-8_10_null_2,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-8_10_null_2,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Night-8_10_null_2,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Night-8_10_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Night-8_10_null_3,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..c11f365339
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Night-8_10_null_3,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a1e7414cc8c9412c45e55ea77842b908a1979aa680a66609cbdfa6f5a91d87c9
+size 37925
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Night-8_10_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Night-8_10_null_4,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..bc035365a5
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Night-8_10_null_4,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a08860391a1f9854c2354ede0236e0ea78bd1242b134121e2e6a1903e7b8a6b2
+size 35378
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Night-8_10_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Night-8_10_null_5,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..32e6921e00
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Night-8_10_null_5,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:865c6148937271e08a14f4820652b9c9109b81eca71b715711460c8fe14fcc97
+size 45654
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Night-8_10_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Night-8_10_null_6,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..c11f365339
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Night-8_10_null_6,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a1e7414cc8c9412c45e55ea77842b908a1979aa680a66609cbdfa6f5a91d87c9
+size 37925
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Night-8_10_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Night-8_10_null_7,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..5159dcd623
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Night-8_10_null_7,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9b9d5ca3bccf0fbca3ced8392f2ca42dd448c032d97d81291357dc3a1bb33ea2
+size 35806
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-8_9_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-8_9_null_0,NEXUS_5,1.0,en].png
deleted file mode 100644
index 3c15ca411d..0000000000
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-8_9_null_0,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:5c6410198fc2f7e74279e21013718bf8326bfd871fa7ffa244470725f7bf3ae5
-size 23045
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-8_9_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-8_9_null_1,NEXUS_5,1.0,en].png
deleted file mode 100644
index a4329e47f8..0000000000
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-8_9_null_1,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:3d6fce3b7fa9ac12e25f135ddeabb96dad4bba9422926e5451647255a712e481
-size 22772
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-8_9_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-8_9_null_3,NEXUS_5,1.0,en].png
deleted file mode 100644
index 944ddb606f..0000000000
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-8_9_null_3,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:c788eab0495478d8253cbd11970c57c83670e2b761a675b413642db3c6ee57fa
-size 25095
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-8_9_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-8_9_null_4,NEXUS_5,1.0,en].png
deleted file mode 100644
index e3cefb87a5..0000000000
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-8_9_null_4,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:98f81fcfa9cb5c7c02f8322ca4e444df32e81acb5f0d79f39841f9b71c436594
-size 26376
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-8_10_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-8_10_null_0,NEXUS_5,1.0,en].png
deleted file mode 100644
index f6fa9fb31f..0000000000
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-8_10_null_0,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:dbc67150e8d533fd473204fbf4c9360751b8498831c435a06e9ed1525dfa1d7b
-size 21637
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-8_10_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-8_10_null_1,NEXUS_5,1.0,en].png
deleted file mode 100644
index 16255be86e..0000000000
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-8_10_null_1,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:09b42b37d3e05cfe60187512403139b305c39d4d3ff59d122d73762a548d01fa
-size 21364
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-8_10_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-8_10_null_3,NEXUS_5,1.0,en].png
deleted file mode 100644
index 4426f04cce..0000000000
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-8_10_null_3,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:50b1f5c23b436ca2f6045c4d2a6d14c7902a5f10646ce59522a02c2edc297374
-size 22310
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-8_10_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-8_10_null_4,NEXUS_5,1.0,en].png
deleted file mode 100644
index 06fd4deac9..0000000000
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-8_10_null_4,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:1b7f91366d3576553c7dd2bc469290fa094dc4095510bb5e841fa0a9b179e85d
-size 23085