Change a room's permissions power levels (#2525)
* Change a room's permissions power levels * Make `currentPermissions` use a `MatrixRoomPowerLevels?` instance instead. * Update strings * Update screenshots --------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
parent
796bdc0224
commit
d19b3fe091
71 changed files with 1556 additions and 58 deletions
1
changelog.d/2259.feature
Normal file
1
changelog.d/2259.feature
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Change a room's permissions power levels.
|
||||||
|
|
@ -36,10 +36,10 @@
|
||||||
<string name="screen_room_change_permissions_messages_and_content">"Messages and content"</string>
|
<string name="screen_room_change_permissions_messages_and_content">"Messages and content"</string>
|
||||||
<string name="screen_room_change_permissions_moderators">"Admins and moderators"</string>
|
<string name="screen_room_change_permissions_moderators">"Admins and moderators"</string>
|
||||||
<string name="screen_room_change_permissions_remove_people">"Remove people"</string>
|
<string name="screen_room_change_permissions_remove_people">"Remove people"</string>
|
||||||
<string name="screen_room_change_permissions_room_avatar">"Change Room Avatar"</string>
|
<string name="screen_room_change_permissions_room_avatar">"Change room avatar"</string>
|
||||||
<string name="screen_room_change_permissions_room_details">"Room details"</string>
|
<string name="screen_room_change_permissions_room_details">"Room details"</string>
|
||||||
<string name="screen_room_change_permissions_room_name">"Change Room Name"</string>
|
<string name="screen_room_change_permissions_room_name">"Change room name"</string>
|
||||||
<string name="screen_room_change_permissions_room_topic">"Change Room Topic"</string>
|
<string name="screen_room_change_permissions_room_topic">"Change room topic"</string>
|
||||||
<string name="screen_room_change_permissions_send_messages">"Send messages"</string>
|
<string name="screen_room_change_permissions_send_messages">"Send messages"</string>
|
||||||
<string name="screen_room_change_role_administrators_title">"Edit Admins"</string>
|
<string name="screen_room_change_role_administrators_title">"Edit Admins"</string>
|
||||||
<string name="screen_room_change_role_confirm_add_admin_description">"You will not be able to undo this action. You are promoting the user to have the same power level as you."</string>
|
<string name="screen_room_change_role_confirm_add_admin_description">"You will not be able to undo this action. You are promoting the user to have the same power level as you."</string>
|
||||||
|
|
@ -87,7 +87,7 @@
|
||||||
<string name="screen_room_roles_and_permissions_moderators">"Moderators"</string>
|
<string name="screen_room_roles_and_permissions_moderators">"Moderators"</string>
|
||||||
<string name="screen_room_roles_and_permissions_permissions_header">"Permissions"</string>
|
<string name="screen_room_roles_and_permissions_permissions_header">"Permissions"</string>
|
||||||
<string name="screen_room_roles_and_permissions_reset">"Reset permissions"</string>
|
<string name="screen_room_roles_and_permissions_reset">"Reset permissions"</string>
|
||||||
<string name="screen_room_roles_and_permissions_reset_confirm_description">"Once you reset permissions, you will lose your current settings."</string>
|
<string name="screen_room_roles_and_permissions_reset_confirm_description">"Once you reset permissions, you will lose the current settings."</string>
|
||||||
<string name="screen_room_roles_and_permissions_reset_confirm_title">"Reset permissions?"</string>
|
<string name="screen_room_roles_and_permissions_reset_confirm_title">"Reset permissions?"</string>
|
||||||
<string name="screen_room_roles_and_permissions_roles_header">"Roles"</string>
|
<string name="screen_room_roles_and_permissions_roles_header">"Roles"</string>
|
||||||
<string name="screen_room_roles_and_permissions_room_details">"Room details"</string>
|
<string name="screen_room_roles_and_permissions_room_details">"Room details"</string>
|
||||||
|
|
|
||||||
|
|
@ -21,5 +21,6 @@ import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
sealed interface RolesAndPermissionsEvents {
|
sealed interface RolesAndPermissionsEvents {
|
||||||
data object ChangeOwnRole : RolesAndPermissionsEvents
|
data object ChangeOwnRole : RolesAndPermissionsEvents
|
||||||
data class DemoteSelfTo(val role: RoomMember.Role) : RolesAndPermissionsEvents
|
data class DemoteSelfTo(val role: RoomMember.Role) : RolesAndPermissionsEvents
|
||||||
|
data object ResetPermissions : RolesAndPermissionsEvents
|
||||||
data object CancelPendingAction : RolesAndPermissionsEvents
|
data object CancelPendingAction : RolesAndPermissionsEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.ChangeRolesNode
|
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.BackstackView
|
||||||
import io.element.android.libraries.architecture.BaseFlowNode
|
import io.element.android.libraries.architecture.BaseFlowNode
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
|
|
@ -55,6 +57,9 @@ class RolesAndPermissionsFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object ModeratorList : NavTarget
|
data object ModeratorList : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ChangeRoomPermissions(val section: ChangeRoomPermissionsSection) : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
|
@ -68,6 +73,18 @@ class RolesAndPermissionsFlowNode @AssistedInject constructor(
|
||||||
override fun openModeratorList() {
|
override fun openModeratorList() {
|
||||||
backstack.push(NavTarget.ModeratorList)
|
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<RolesAndPermissionsNode>(
|
createNode<RolesAndPermissionsNode>(
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
|
|
@ -88,6 +105,13 @@ class RolesAndPermissionsFlowNode @AssistedInject constructor(
|
||||||
plugins = listOf(inputs),
|
plugins = listOf(inputs),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
is NavTarget.ChangeRoomPermissions -> {
|
||||||
|
val inputs = ChangeRoomPermissionsNode.Inputs(navTarget.section)
|
||||||
|
createNode<ChangeRoomPermissionsNode>(
|
||||||
|
buildContext = buildContext,
|
||||||
|
plugins = listOf(inputs),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.features.roomdetails.impl.rolesandpermissions
|
package io.element.android.features.roomdetails.impl.rolesandpermissions
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
|
|
@ -46,17 +47,24 @@ class RolesAndPermissionsNode @AssistedInject constructor(
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
private val presenter: RolesAndPermissionsPresenter,
|
private val presenter: RolesAndPermissionsPresenter,
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
) : Node(buildContext, plugins = plugins), RoomDetailsAdminSettingsNavigator {
|
) : Node(buildContext, plugins = plugins), RolesAndPermissionsNavigator {
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin, RolesAndPermissionsNavigator {
|
||||||
fun openAdminList()
|
override fun openAdminList()
|
||||||
fun openModeratorList()
|
override fun openModeratorList()
|
||||||
|
override fun openEditRoomDetailsPermissions()
|
||||||
|
override fun openMessagesAndContentPermissions()
|
||||||
|
override fun openModerationPermissions()
|
||||||
|
override fun onBackPressed() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val callback = plugins<Callback>().first()
|
private val callback = plugins<Callback>().first()
|
||||||
|
|
||||||
override fun onBackPressed() = navigateUp()
|
@Stable
|
||||||
override fun openAdminList() = callback.openAdminList()
|
private val navigator = object : RolesAndPermissionsNavigator by callback {
|
||||||
override fun openModeratorList() = callback.openModeratorList()
|
override fun onBackPressed() {
|
||||||
|
navigateUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
super.onBuilt()
|
super.onBuilt()
|
||||||
|
|
@ -88,14 +96,17 @@ class RolesAndPermissionsNode @AssistedInject constructor(
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
RolesAndPermissionsView(
|
RolesAndPermissionsView(
|
||||||
state = state,
|
state = state,
|
||||||
roomDetailsAdminSettingsNavigator = this,
|
rolesAndPermissionsNavigator = navigator,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RoomDetailsAdminSettingsNavigator {
|
interface RolesAndPermissionsNavigator {
|
||||||
fun onBackPressed() {}
|
fun onBackPressed() {}
|
||||||
fun openAdminList() {}
|
fun openAdminList() {}
|
||||||
fun openModeratorList() {}
|
fun openModeratorList() {}
|
||||||
|
fun openEditRoomDetailsPermissions() {}
|
||||||
|
fun openMessagesAndContentPermissions() {}
|
||||||
|
fun openModerationPermissions() {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ class RolesAndPermissionsPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val changeOwnRoleAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
val changeOwnRoleAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||||
|
val resetPermissionsAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||||
|
|
||||||
fun handleEvent(event: RolesAndPermissionsEvents) {
|
fun handleEvent(event: RolesAndPermissionsEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
|
|
@ -63,11 +64,17 @@ class RolesAndPermissionsPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
is RolesAndPermissionsEvents.CancelPendingAction -> {
|
is RolesAndPermissionsEvents.CancelPendingAction -> {
|
||||||
changeOwnRoleAction.value = AsyncAction.Uninitialized
|
changeOwnRoleAction.value = AsyncAction.Uninitialized
|
||||||
|
resetPermissionsAction.value = AsyncAction.Uninitialized
|
||||||
}
|
}
|
||||||
is RolesAndPermissionsEvents.DemoteSelfTo -> coroutineScope.demoteSelfTo(
|
is RolesAndPermissionsEvents.DemoteSelfTo -> coroutineScope.demoteSelfTo(
|
||||||
role = event.role,
|
role = event.role,
|
||||||
changeOwnRoleAction = changeOwnRoleAction,
|
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,
|
adminCount = adminCount,
|
||||||
moderatorCount = moderatorCount,
|
moderatorCount = moderatorCount,
|
||||||
changeOwnRoleAction = changeOwnRoleAction.value,
|
changeOwnRoleAction = changeOwnRoleAction.value,
|
||||||
|
resetPermissionsAction = resetPermissionsAction.value,
|
||||||
eventSink = { handleEvent(it) },
|
eventSink = { handleEvent(it) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -88,6 +96,14 @@ class RolesAndPermissionsPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.resetPermissions(
|
||||||
|
resetPermissionsAction: MutableState<AsyncAction<Unit>>,
|
||||||
|
) = launch(dispatchers.io) {
|
||||||
|
runUpdatingState(resetPermissionsAction) {
|
||||||
|
room.resetPowerLevels().map {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun MatrixRoomInfo?.userCountWithRole(role: RoomMember.Role): Int {
|
private fun MatrixRoomInfo?.userCountWithRole(role: RoomMember.Role): Int {
|
||||||
return if (this != null) {
|
return if (this != null) {
|
||||||
userPowerLevels.count { (_, level) -> RoomMember.Role.forPowerLevel(level) == role }
|
userPowerLevels.count { (_, level) -> RoomMember.Role.forPowerLevel(level) == role }
|
||||||
|
|
|
||||||
|
|
@ -22,5 +22,6 @@ data class RolesAndPermissionsState(
|
||||||
val adminCount: Int,
|
val adminCount: Int,
|
||||||
val moderatorCount: Int,
|
val moderatorCount: Int,
|
||||||
val changeOwnRoleAction: AsyncAction<Unit>,
|
val changeOwnRoleAction: AsyncAction<Unit>,
|
||||||
|
val resetPermissionsAction: AsyncAction<Unit>,
|
||||||
val eventSink: (RolesAndPermissionsEvents) -> Unit,
|
val eventSink: (RolesAndPermissionsEvents) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,21 @@ class RolesAndPermissionsStateProvider : PreviewParameterProvider<RolesAndPermis
|
||||||
moderatorCount = 2,
|
moderatorCount = 2,
|
||||||
changeOwnRoleAction = AsyncAction.Failure(IllegalStateException("Failed to change role")),
|
changeOwnRoleAction = AsyncAction.Failure(IllegalStateException("Failed to change role")),
|
||||||
),
|
),
|
||||||
|
aRolesAndPermissionsState(
|
||||||
|
adminCount = 1,
|
||||||
|
moderatorCount = 2,
|
||||||
|
resetPermissionsAction = AsyncAction.Confirming,
|
||||||
|
),
|
||||||
|
aRolesAndPermissionsState(
|
||||||
|
adminCount = 1,
|
||||||
|
moderatorCount = 2,
|
||||||
|
resetPermissionsAction = AsyncAction.Loading,
|
||||||
|
),
|
||||||
|
aRolesAndPermissionsState(
|
||||||
|
adminCount = 1,
|
||||||
|
moderatorCount = 2,
|
||||||
|
resetPermissionsAction = AsyncAction.Failure(IllegalStateException("Failed to reset permissions")),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,10 +61,12 @@ internal fun aRolesAndPermissionsState(
|
||||||
adminCount: Int = 0,
|
adminCount: Int = 0,
|
||||||
moderatorCount: Int = 0,
|
moderatorCount: Int = 0,
|
||||||
changeOwnRoleAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
changeOwnRoleAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
|
resetPermissionsAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
eventSink: (RolesAndPermissionsEvents) -> Unit = {},
|
eventSink: (RolesAndPermissionsEvents) -> Unit = {},
|
||||||
) = RolesAndPermissionsState(
|
) = RolesAndPermissionsState(
|
||||||
adminCount = adminCount,
|
adminCount = adminCount,
|
||||||
moderatorCount = moderatorCount,
|
moderatorCount = moderatorCount,
|
||||||
changeOwnRoleAction = changeOwnRoleAction,
|
changeOwnRoleAction = changeOwnRoleAction,
|
||||||
|
resetPermissionsAction = resetPermissionsAction,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.roomdetails.impl.R
|
import io.element.android.features.roomdetails.impl.R
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
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.dialogs.ErrorDialog
|
||||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
|
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
|
||||||
|
|
@ -53,35 +55,72 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
@Composable
|
@Composable
|
||||||
fun RolesAndPermissionsView(
|
fun RolesAndPermissionsView(
|
||||||
state: RolesAndPermissionsState,
|
state: RolesAndPermissionsState,
|
||||||
roomDetailsAdminSettingsNavigator: RoomDetailsAdminSettingsNavigator,
|
rolesAndPermissionsNavigator: RolesAndPermissionsNavigator,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
PreferencePage(
|
PreferencePage(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
title = stringResource(R.string.screen_room_roles_and_permissions_title),
|
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)
|
ListSectionHeader(title = stringResource(R.string.screen_room_roles_and_permissions_roles_header), hasDivider = false)
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_admins)) },
|
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_admins)) },
|
||||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Admin())),
|
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Admin())),
|
||||||
trailingContent = ListItemContent.Text("${state.adminCount}"),
|
trailingContent = ListItemContent.Text("${state.adminCount}"),
|
||||||
onClick = { roomDetailsAdminSettingsNavigator.openAdminList() },
|
onClick = { rolesAndPermissionsNavigator.openAdminList() },
|
||||||
)
|
)
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_moderators)) },
|
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_moderators)) },
|
||||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ChatProblem())),
|
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ChatProblem())),
|
||||||
trailingContent = ListItemContent.Text("${state.moderatorCount}"),
|
trailingContent = ListItemContent.Text("${state.moderatorCount}"),
|
||||||
onClick = { roomDetailsAdminSettingsNavigator.openModeratorList() },
|
onClick = { rolesAndPermissionsNavigator.openModeratorList() },
|
||||||
)
|
)
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_change_my_role)) },
|
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_change_my_role)) },
|
||||||
onClick = { state.eventSink(RolesAndPermissionsEvents.ChangeOwnRole) },
|
onClick = { state.eventSink(RolesAndPermissionsEvents.ChangeOwnRole) },
|
||||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Edit()))
|
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()
|
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) {
|
when (state.changeOwnRoleAction) {
|
||||||
is AsyncAction.Confirming -> {
|
is AsyncAction.Confirming -> {
|
||||||
ChangeOwnRoleBottomSheet(
|
ChangeOwnRoleBottomSheet(
|
||||||
|
|
@ -156,11 +195,7 @@ private fun ChangeOwnRoleBottomSheet(
|
||||||
)
|
)
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(stringResource(CommonStrings.action_cancel)) },
|
headlineContent = { Text(stringResource(CommonStrings.action_cancel)) },
|
||||||
onClick = {
|
onClick = ::dismiss,
|
||||||
sheetState.hide(coroutineScope) {
|
|
||||||
eventSink(RolesAndPermissionsEvents.CancelPendingAction)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
style = ListItemStyle.Primary,
|
style = ListItemStyle.Primary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -168,11 +203,11 @@ private fun ChangeOwnRoleBottomSheet(
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun RoomDetailsAdminSettingsViewPreview(@PreviewParameter(RolesAndPermissionsStateProvider::class) state: RolesAndPermissionsState) {
|
internal fun RolesAndPermissionViewPreview(@PreviewParameter(RolesAndPermissionsStateProvider::class) state: RolesAndPermissionsState) {
|
||||||
ElementPreview {
|
ElementPreview {
|
||||||
RolesAndPermissionsView(
|
RolesAndPermissionsView(
|
||||||
state = state,
|
state = state,
|
||||||
roomDetailsAdminSettingsNavigator = object : RoomDetailsAdminSettingsNavigator {},
|
rolesAndPermissionsNavigator = object : RolesAndPermissionsNavigator {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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<Plugin>,
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
@ -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<ChangeRoomPermissionsState> {
|
||||||
|
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<RoomPermissionType> = itemsForSection(section)
|
||||||
|
|
||||||
|
private var initialPermissions by mutableStateOf<MatrixRoomPowerLevels?>(null)
|
||||||
|
private var currentPermissions by mutableStateOf<MatrixRoomPowerLevels?>(null)
|
||||||
|
private var saveAction by mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
|
||||||
|
private var confirmExitAction by mutableStateOf<AsyncAction<Unit>>(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<RoomPermissionType>,
|
||||||
|
val hasChanges: Boolean,
|
||||||
|
val saveAction: AsyncAction<Unit>,
|
||||||
|
val confirmExitAction: AsyncAction<Unit>,
|
||||||
|
val eventSink: (ChangeRoomPermissionsEvent) -> Unit,
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class RoomPermissionType {
|
||||||
|
BAN,
|
||||||
|
INVITE,
|
||||||
|
KICK,
|
||||||
|
SEND_EVENTS,
|
||||||
|
REDACT_EVENTS,
|
||||||
|
ROOM_NAME,
|
||||||
|
ROOM_AVATAR,
|
||||||
|
ROOM_TOPIC
|
||||||
|
}
|
||||||
|
|
@ -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<ChangeRoomPermissionsState> {
|
||||||
|
override val values: Sequence<ChangeRoomPermissionsState>
|
||||||
|
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<RoomPermissionType> = ChangeRoomPermissionsPresenter.itemsForSection(section),
|
||||||
|
hasChanges: Boolean = false,
|
||||||
|
saveAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
|
confirmExitAction: AsyncAction<Unit> = 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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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 = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,10 +18,10 @@
|
||||||
<string name="screen_room_change_permissions_messages_and_content">"Messages and content"</string>
|
<string name="screen_room_change_permissions_messages_and_content">"Messages and content"</string>
|
||||||
<string name="screen_room_change_permissions_moderators">"Admins and moderators"</string>
|
<string name="screen_room_change_permissions_moderators">"Admins and moderators"</string>
|
||||||
<string name="screen_room_change_permissions_remove_people">"Remove people"</string>
|
<string name="screen_room_change_permissions_remove_people">"Remove people"</string>
|
||||||
<string name="screen_room_change_permissions_room_avatar">"Change Room Avatar"</string>
|
<string name="screen_room_change_permissions_room_avatar">"Change room avatar"</string>
|
||||||
<string name="screen_room_change_permissions_room_details">"Room details"</string>
|
<string name="screen_room_change_permissions_room_details">"Room details"</string>
|
||||||
<string name="screen_room_change_permissions_room_name">"Change Room Name"</string>
|
<string name="screen_room_change_permissions_room_name">"Change room name"</string>
|
||||||
<string name="screen_room_change_permissions_room_topic">"Change Room Topic"</string>
|
<string name="screen_room_change_permissions_room_topic">"Change room topic"</string>
|
||||||
<string name="screen_room_change_permissions_send_messages">"Send messages"</string>
|
<string name="screen_room_change_permissions_send_messages">"Send messages"</string>
|
||||||
<string name="screen_room_change_role_administrators_title">"Edit Admins"</string>
|
<string name="screen_room_change_role_administrators_title">"Edit Admins"</string>
|
||||||
<string name="screen_room_change_role_confirm_add_admin_description">"You will not be able to undo this action. You are promoting the user to have the same power level as you."</string>
|
<string name="screen_room_change_role_confirm_add_admin_description">"You will not be able to undo this action. You are promoting the user to have the same power level as you."</string>
|
||||||
|
|
@ -104,7 +104,7 @@
|
||||||
<string name="screen_room_roles_and_permissions_moderators">"Moderators"</string>
|
<string name="screen_room_roles_and_permissions_moderators">"Moderators"</string>
|
||||||
<string name="screen_room_roles_and_permissions_permissions_header">"Permissions"</string>
|
<string name="screen_room_roles_and_permissions_permissions_header">"Permissions"</string>
|
||||||
<string name="screen_room_roles_and_permissions_reset">"Reset permissions"</string>
|
<string name="screen_room_roles_and_permissions_reset">"Reset permissions"</string>
|
||||||
<string name="screen_room_roles_and_permissions_reset_confirm_description">"Once you reset permissions, you will lose your current settings."</string>
|
<string name="screen_room_roles_and_permissions_reset_confirm_description">"Once you reset permissions, you will lose the current settings."</string>
|
||||||
<string name="screen_room_roles_and_permissions_reset_confirm_title">"Reset permissions?"</string>
|
<string name="screen_room_roles_and_permissions_reset_confirm_title">"Reset permissions?"</string>
|
||||||
<string name="screen_room_roles_and_permissions_roles_header">"Roles"</string>
|
<string name="screen_room_roles_and_permissions_roles_header">"Roles"</string>
|
||||||
<string name="screen_room_roles_and_permissions_room_details">"Room details"</string>
|
<string name="screen_room_roles_and_permissions_room_details">"Room details"</string>
|
||||||
|
|
|
||||||
|
|
@ -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(
|
private fun TestScope.createRolesAndPermissionsPresenter(
|
||||||
room: FakeMatrixRoom = FakeMatrixRoom(),
|
room: FakeMatrixRoom = FakeMatrixRoom(),
|
||||||
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
||||||
|
|
|
||||||
|
|
@ -21,19 +21,26 @@ import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import io.element.android.features.roomdetails.impl.R
|
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.RolesAndPermissionsState
|
||||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsView
|
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.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.EnsureNeverCalled
|
||||||
import io.element.android.tests.testutils.EventsRecorder
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
import io.element.android.tests.testutils.clickOn
|
import io.element.android.tests.testutils.clickOn
|
||||||
import io.element.android.tests.testutils.ensureCalledOnce
|
import io.element.android.tests.testutils.ensureCalledOnce
|
||||||
|
import io.element.android.tests.testutils.ensureCalledTimes
|
||||||
import io.element.android.tests.testutils.pressBack
|
import io.element.android.tests.testutils.pressBack
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.rules.TestRule
|
import org.junit.rules.TestRule
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class RolesAndPermissionsViewTests {
|
class RolesAndPermissionsViewTests {
|
||||||
|
|
@ -68,6 +75,100 @@ class RolesAndPermissionsViewTests {
|
||||||
rule.clickOn(R.string.screen_room_roles_and_permissions_moderators)
|
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<RolesAndPermissionsEvents>()
|
||||||
|
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<RolesAndPermissionsEvents>()
|
||||||
|
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<RolesAndPermissionsEvents>()
|
||||||
|
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<RolesAndPermissionsEvents>()
|
||||||
|
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<RolesAndPermissionsEvents>()
|
||||||
|
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<RolesAndPermissionsEvents>()
|
||||||
|
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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRolesAndPermissionsView(
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRolesAndPermissionsView(
|
||||||
|
|
@ -77,14 +178,18 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoles
|
||||||
goBack: () -> Unit = EnsureNeverCalled(),
|
goBack: () -> Unit = EnsureNeverCalled(),
|
||||||
openAdminList: () -> Unit = EnsureNeverCalled(),
|
openAdminList: () -> Unit = EnsureNeverCalled(),
|
||||||
openModeratorList: () -> Unit = EnsureNeverCalled(),
|
openModeratorList: () -> Unit = EnsureNeverCalled(),
|
||||||
|
openPermissionScreens: () -> Unit = EnsureNeverCalled(),
|
||||||
) {
|
) {
|
||||||
setContent {
|
setContent {
|
||||||
RolesAndPermissionsView(
|
RolesAndPermissionsView(
|
||||||
state = state,
|
state = state,
|
||||||
roomDetailsAdminSettingsNavigator = object : RoomDetailsAdminSettingsNavigator {
|
rolesAndPermissionsNavigator = object : RolesAndPermissionsNavigator {
|
||||||
override fun onBackPressed() = goBack()
|
override fun onBackPressed() = goBack()
|
||||||
override fun openAdminList() = openAdminList()
|
override fun openAdminList() = openAdminList()
|
||||||
override fun openModeratorList() = openModeratorList()
|
override fun openModeratorList() = openModeratorList()
|
||||||
|
override fun openEditRoomDetailsPermissions() = openPermissionScreens()
|
||||||
|
override fun openModerationPermissions() = openPermissionScreens()
|
||||||
|
override fun openMessagesAndContentPermissions() = openPermissionScreens()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<ChangeRoomPermissionsState>)?.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<ChangeRoomPermissionsState>.awaitUpdatedItem(): ChangeRoomPermissionsState {
|
||||||
|
skipItems(1)
|
||||||
|
return awaitItem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<ComponentActivity>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `click on back icon invokes Exit`() {
|
||||||
|
val recorder = EventsRecorder<ChangeRoomPermissionsEvent>()
|
||||||
|
rule.setChangeRoomPermissionsRule(
|
||||||
|
eventsRecorder = recorder,
|
||||||
|
)
|
||||||
|
rule.pressBack()
|
||||||
|
recorder.assertSingle(ChangeRoomPermissionsEvent.Exit)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `click on back key invokes Exit`() {
|
||||||
|
val recorder = EventsRecorder<ChangeRoomPermissionsEvent>()
|
||||||
|
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<ChangeRoomPermissionsEvent>()
|
||||||
|
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<ChangeRoomPermissionsEvent>()
|
||||||
|
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<ChangeRoomPermissionsEvent>()
|
||||||
|
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<ChangeRoomPermissionsEvent>()
|
||||||
|
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<ChangeRoomPermissionsEvent>()
|
||||||
|
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<ChangeRoomPermissionsEvent>()
|
||||||
|
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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setChangeRoomPermissionsRule(
|
||||||
|
eventsRecorder: EventsRecorder<ChangeRoomPermissionsEvent> = EventsRecorder(expectEvents = false),
|
||||||
|
state: ChangeRoomPermissionsState = aChangeRoomPermissionsState(
|
||||||
|
section = ChangeRoomPermissionsSection.RoomDetails,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onBackPressed: () -> Unit = EnsureNeverCalled(),
|
||||||
|
) {
|
||||||
|
setContent {
|
||||||
|
ChangeRoomPermissionsView(
|
||||||
|
state = state,
|
||||||
|
onBackPressed = onBackPressed,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -170,7 +170,7 @@ class StateContentFormatter @Inject constructor(
|
||||||
"RoomPinnedEvents"
|
"RoomPinnedEvents"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is OtherState.RoomPowerLevels -> when (renderingMode) {
|
is OtherState.RoomUserPowerLevels -> when (renderingMode) {
|
||||||
RenderingMode.RoomList -> {
|
RenderingMode.RoomList -> {
|
||||||
Timber.v("Filtering timeline item for room state change: $content")
|
Timber.v("Filtering timeline item for room state change: $content")
|
||||||
null
|
null
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,16 @@
|
||||||
<string name="state_event_avatar_changed_too">"(avatar was changed too)"</string>
|
<string name="state_event_avatar_changed_too">"(avatar was changed too)"</string>
|
||||||
<string name="state_event_avatar_url_changed">"%1$s changed their avatar"</string>
|
<string name="state_event_avatar_url_changed">"%1$s changed their avatar"</string>
|
||||||
<string name="state_event_avatar_url_changed_by_you">"You changed your avatar"</string>
|
<string name="state_event_avatar_url_changed_by_you">"You changed your avatar"</string>
|
||||||
|
<string name="state_event_demoted_to_member">"%1$s was demoted to member"</string>
|
||||||
|
<string name="state_event_demoted_to_moderator">"%1$s was demoted to moderator"</string>
|
||||||
<string name="state_event_display_name_changed_from">"%1$s changed their display name from %2$s to %3$s"</string>
|
<string name="state_event_display_name_changed_from">"%1$s changed their display name from %2$s to %3$s"</string>
|
||||||
<string name="state_event_display_name_changed_from_by_you">"You changed your display name from %1$s to %2$s"</string>
|
<string name="state_event_display_name_changed_from_by_you">"You changed your display name from %1$s to %2$s"</string>
|
||||||
<string name="state_event_display_name_removed">"%1$s removed their display name (it was %2$s)"</string>
|
<string name="state_event_display_name_removed">"%1$s removed their display name (it was %2$s)"</string>
|
||||||
<string name="state_event_display_name_removed_by_you">"You removed your display name (it was %1$s)"</string>
|
<string name="state_event_display_name_removed_by_you">"You removed your display name (it was %1$s)"</string>
|
||||||
<string name="state_event_display_name_set">"%1$s set their display name to %2$s"</string>
|
<string name="state_event_display_name_set">"%1$s set their display name to %2$s"</string>
|
||||||
<string name="state_event_display_name_set_by_you">"You set your display name to %1$s"</string>
|
<string name="state_event_display_name_set_by_you">"You set your display name to %1$s"</string>
|
||||||
|
<string name="state_event_promoted_to_administrator">"%1$s was promoted to admin"</string>
|
||||||
|
<string name="state_event_promoted_to_moderator">"%1$s was promoted to moderator"</string>
|
||||||
<string name="state_event_room_avatar_changed">"%1$s changed the room avatar"</string>
|
<string name="state_event_room_avatar_changed">"%1$s changed the room avatar"</string>
|
||||||
<string name="state_event_room_avatar_changed_by_you">"You changed the room avatar"</string>
|
<string name="state_event_room_avatar_changed_by_you">"You changed the room avatar"</string>
|
||||||
<string name="state_event_room_avatar_removed">"%1$s removed the room avatar"</string>
|
<string name="state_event_room_avatar_removed">"%1$s removed the room avatar"</string>
|
||||||
|
|
|
||||||
|
|
@ -650,7 +650,7 @@ class DefaultRoomLastMessageFormatterTest {
|
||||||
OtherState.RoomHistoryVisibility,
|
OtherState.RoomHistoryVisibility,
|
||||||
OtherState.RoomJoinRules,
|
OtherState.RoomJoinRules,
|
||||||
OtherState.RoomPinnedEvents,
|
OtherState.RoomPinnedEvents,
|
||||||
OtherState.RoomPowerLevels(emptyMap()),
|
OtherState.RoomUserPowerLevels(emptyMap()),
|
||||||
OtherState.RoomServerAcl,
|
OtherState.RoomServerAcl,
|
||||||
OtherState.RoomTombstone,
|
OtherState.RoomTombstone,
|
||||||
OtherState.SpaceChild,
|
OtherState.SpaceChild,
|
||||||
|
|
|
||||||
|
|
@ -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.media.VideoInfo
|
||||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
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.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.powerlevels.UserRoleChange
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||||
|
|
@ -97,6 +98,12 @@ interface MatrixRoom : Closeable {
|
||||||
|
|
||||||
suspend fun unsubscribeFromSync()
|
suspend fun unsubscribeFromSync()
|
||||||
|
|
||||||
|
suspend fun powerLevels(): Result<MatrixRoomPowerLevels>
|
||||||
|
|
||||||
|
suspend fun updatePowerLevels(matrixRoomPowerLevels: MatrixRoomPowerLevels): Result<Unit>
|
||||||
|
|
||||||
|
suspend fun resetPowerLevels(): Result<MatrixRoomPowerLevels>
|
||||||
|
|
||||||
suspend fun userRole(userId: UserId): Result<RoomMember.Role>
|
suspend fun userRole(userId: UserId): Result<RoomMember.Role>
|
||||||
|
|
||||||
suspend fun updateUsersRoles(changes: List<UserRoleChange>): Result<Unit>
|
suspend fun updateUsersRoles(changes: List<UserRoleChange>): Result<Unit>
|
||||||
|
|
|
||||||
|
|
@ -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.MessageEventType
|
||||||
import io.element.android.libraries.matrix.api.room.StateEventType
|
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.
|
* Shortcut for calling [MatrixRoom.canUserInvite] with our own user.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ sealed interface OtherState {
|
||||||
data object RoomJoinRules : OtherState
|
data object RoomJoinRules : OtherState
|
||||||
data class RoomName(val name: String?) : OtherState
|
data class RoomName(val name: String?) : OtherState
|
||||||
data object RoomPinnedEvents : OtherState
|
data object RoomPinnedEvents : OtherState
|
||||||
data class RoomPowerLevels(val users: Map<String, Long>) : OtherState
|
data class RoomUserPowerLevels(val users: Map<String, Long>) : OtherState
|
||||||
data object RoomServerAcl : OtherState
|
data object RoomServerAcl : OtherState
|
||||||
data class RoomThirdPartyInvite(val displayName: String?) : OtherState
|
data class RoomThirdPartyInvite(val displayName: String?) : OtherState
|
||||||
data object RoomTombstone : OtherState
|
data object RoomTombstone : OtherState
|
||||||
|
|
|
||||||
|
|
@ -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.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.StateEventType
|
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.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.powerlevels.UserRoleChange
|
||||||
import io.element.android.libraries.matrix.api.room.roomNotificationSettings
|
import io.element.android.libraries.matrix.api.room.roomNotificationSettings
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
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.location.toInner
|
||||||
import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher
|
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.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.RustMatrixTimeline
|
||||||
import io.element.android.libraries.matrix.impl.timeline.toRustReceiptType
|
import io.element.android.libraries.matrix.impl.timeline.toRustReceiptType
|
||||||
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
|
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.messageEventContentFromMarkdown
|
||||||
import org.matrix.rustcomponents.sdk.use
|
import org.matrix.rustcomponents.sdk.use
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import uniffi.matrix_sdk.RoomPowerLevelChanges
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import org.matrix.rustcomponents.sdk.Room as InnerRoom
|
import org.matrix.rustcomponents.sdk.Room as InnerRoom
|
||||||
import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline
|
import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline
|
||||||
|
|
@ -253,6 +256,34 @@ class RustMatrixRoom(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun powerLevels(): Result<MatrixRoomPowerLevels> = withContext(roomDispatcher) {
|
||||||
|
runCatching {
|
||||||
|
RoomPowerLevelsMapper.map(innerRoom.getPowerLevels())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun updatePowerLevels(matrixRoomPowerLevels: MatrixRoomPowerLevels): Result<Unit> = 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<MatrixRoomPowerLevels> = withContext(roomDispatcher) {
|
||||||
|
runCatching {
|
||||||
|
RoomPowerLevelsMapper.map(innerRoom.resetPowerLevels())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun userAvatarUrl(userId: UserId): Result<String?> = withContext(roomDispatcher) {
|
override suspend fun userAvatarUrl(userId: UserId): Result<String?> = withContext(roomDispatcher) {
|
||||||
runCatching {
|
runCatching {
|
||||||
innerRoom.memberAvatarUrl(userId.value)
|
innerRoom.memberAvatarUrl(userId.value)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -163,7 +163,7 @@ private fun RustOtherState.map(): OtherState {
|
||||||
RustOtherState.RoomJoinRules -> OtherState.RoomJoinRules
|
RustOtherState.RoomJoinRules -> OtherState.RoomJoinRules
|
||||||
is RustOtherState.RoomName -> OtherState.RoomName(name)
|
is RustOtherState.RoomName -> OtherState.RoomName(name)
|
||||||
RustOtherState.RoomPinnedEvents -> OtherState.RoomPinnedEvents
|
RustOtherState.RoomPinnedEvents -> OtherState.RoomPinnedEvents
|
||||||
is RustOtherState.RoomPowerLevels -> OtherState.RoomPowerLevels(users)
|
is RustOtherState.RoomPowerLevels -> OtherState.RoomUserPowerLevels(users)
|
||||||
RustOtherState.RoomServerAcl -> OtherState.RoomServerAcl
|
RustOtherState.RoomServerAcl -> OtherState.RoomServerAcl
|
||||||
is RustOtherState.RoomThirdPartyInvite -> OtherState.RoomThirdPartyInvite(displayName)
|
is RustOtherState.RoomThirdPartyInvite -> OtherState.RoomThirdPartyInvite(displayName)
|
||||||
RustOtherState.RoomTombstone -> OtherState.RoomTombstone
|
RustOtherState.RoomTombstone -> OtherState.RoomTombstone
|
||||||
|
|
|
||||||
|
|
@ -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.RoomNotificationMode
|
||||||
import io.element.android.libraries.matrix.api.room.StateEventType
|
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.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.powerlevels.UserRoleChange
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||||
|
|
@ -125,6 +126,9 @@ class FakeMatrixRoom(
|
||||||
private var canUserTriggerRoomNotificationResult: Result<Boolean> = Result.success(true)
|
private var canUserTriggerRoomNotificationResult: Result<Boolean> = Result.success(true)
|
||||||
private var canUserJoinCallResult: Result<Boolean> = Result.success(true)
|
private var canUserJoinCallResult: Result<Boolean> = Result.success(true)
|
||||||
private var setIsFavoriteResult = Result.success(Unit)
|
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<Mention>()
|
var sendMessageMentions = emptyList<Mention>()
|
||||||
val editMessageCalls = mutableListOf<Pair<String, String?>>()
|
val editMessageCalls = mutableListOf<Pair<String, String?>>()
|
||||||
private val _typingRecord = mutableListOf<Boolean>()
|
private val _typingRecord = mutableListOf<Boolean>()
|
||||||
|
|
@ -204,6 +208,17 @@ class FakeMatrixRoom(
|
||||||
override suspend fun subscribeToSync() = Unit
|
override suspend fun subscribeToSync() = Unit
|
||||||
|
|
||||||
override suspend fun unsubscribeFromSync() = Unit
|
override suspend fun unsubscribeFromSync() = Unit
|
||||||
|
override suspend fun powerLevels(): Result<MatrixRoomPowerLevels> {
|
||||||
|
return powerLevelsResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun updatePowerLevels(matrixRoomPowerLevels: MatrixRoomPowerLevels): Result<Unit> = simulateLongTask {
|
||||||
|
updatePowerLevelsResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun resetPowerLevels(): Result<MatrixRoomPowerLevels> = simulateLongTask {
|
||||||
|
resetPowerLevelsResult
|
||||||
|
}
|
||||||
|
|
||||||
override fun destroy() = Unit
|
override fun destroy() = Unit
|
||||||
|
|
||||||
|
|
@ -676,6 +691,18 @@ class FakeMatrixRoom(
|
||||||
fun givenRoomTypingMembers(typingMembers: List<UserId>) {
|
fun givenRoomTypingMembers(typingMembers: List<UserId>) {
|
||||||
_roomTypingMembersFlow.tryEmit(typingMembers)
|
_roomTypingMembersFlow.tryEmit(typingMembers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun givenPowerLevelsResult(result: Result<MatrixRoomPowerLevels>) {
|
||||||
|
powerLevelsResult = result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun givenUpdatePowerLevelsResult(result: Result<Unit>) {
|
||||||
|
updatePowerLevelsResult = result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun givenResetPowerLevelsResult(result: Result<MatrixRoomPowerLevels>) {
|
||||||
|
resetPowerLevelsResult = result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class SendLocationInvocation(
|
data class SendLocationInvocation(
|
||||||
|
|
@ -752,3 +779,14 @@ fun aRoomInfo(
|
||||||
userPowerLevels = userPowerLevels,
|
userPowerLevels = userPowerLevels,
|
||||||
activeRoomCallParticipants = activeRoomCallParticipants.toImmutableList(),
|
activeRoomCallParticipants = activeRoomCallParticipants.toImmutableList(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun defaultRoomPowerLevels() = MatrixRoomPowerLevels(
|
||||||
|
ban = 50,
|
||||||
|
invite = 0,
|
||||||
|
kick = 50,
|
||||||
|
sendEvents = 0,
|
||||||
|
redactEvents = 50,
|
||||||
|
roomName = 100,
|
||||||
|
roomAvatar = 100,
|
||||||
|
roomTopic = 100
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@
|
||||||
<string name="action_decline">"Decline"</string>
|
<string name="action_decline">"Decline"</string>
|
||||||
<string name="action_delete_poll">"Delete Poll"</string>
|
<string name="action_delete_poll">"Delete Poll"</string>
|
||||||
<string name="action_disable">"Disable"</string>
|
<string name="action_disable">"Disable"</string>
|
||||||
|
<string name="action_discard">"Discard"</string>
|
||||||
<string name="action_done">"Done"</string>
|
<string name="action_done">"Done"</string>
|
||||||
<string name="action_edit">"Edit"</string>
|
<string name="action_edit">"Edit"</string>
|
||||||
<string name="action_edit_poll">"Edit poll"</string>
|
<string name="action_edit_poll">"Edit poll"</string>
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
fun ensureCalledOnce(block: (callback: () -> Unit) -> Unit) {
|
||||||
val callback = EnsureCalledOnce()
|
val callback = EnsureCalledOnce()
|
||||||
block(callback)
|
block(callback)
|
||||||
callback.assertSuccess()
|
callback.assertSuccess()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ensureCalledTimes(times: Int, block: (callback: () -> Unit) -> Unit) {
|
||||||
|
val callback = EnsureCalledTimes(times)
|
||||||
|
block(callback)
|
||||||
|
callback.assertSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
class EnsureCalledOnceWithParam<T, R>(
|
class EnsureCalledOnceWithParam<T, R>(
|
||||||
private val expectedParam: T,
|
private val expectedParam: T,
|
||||||
private val result: R,
|
private val result: R,
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import androidx.compose.ui.test.hasContentDescription
|
||||||
import androidx.compose.ui.test.hasTestTag
|
import androidx.compose.ui.test.hasTestTag
|
||||||
import androidx.compose.ui.test.hasText
|
import androidx.compose.ui.test.hasText
|
||||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
|
import androidx.compose.ui.test.onFirst
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import org.junit.rules.TestRule
|
import org.junit.rules.TestRule
|
||||||
|
|
@ -34,6 +35,16 @@ fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.clickOn(@StringR
|
||||||
.performClick()
|
.performClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.clickOnFirst(@StringRes res: Int) {
|
||||||
|
val text = activity.getString(res)
|
||||||
|
onAllNodes(hasText(text) and hasClickAction()).onFirst().performClick()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.clickOnLast(@StringRes res: Int) {
|
||||||
|
val text = activity.getString(res)
|
||||||
|
onAllNodes(hasText(text) and hasClickAction()).onFirst().performClick()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Press the back button in the app bar.
|
* Press the back button in the app bar.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:cce394023e7c5bac40bcf234f9073196b1eb424893760f29d609eb329f62817f
|
||||||
|
size 42587
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:97eaf8930ebc7c127dddb12a1002161aa10b120f87ce88779ea96ff35790e344
|
||||||
|
size 38372
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:2d6818023d37493825ba7b929470152847c397b5f19a5bd8ee862fc900dee690
|
||||||
|
size 40755
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:86aa47d55841e5041bf56534852dd716b4546cc3657a30133b88c0c87998f16c
|
||||||
|
size 42464
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:237125629b6449d6898391235b625248b71629b4d96415b80fb7ef5e773e86b2
|
||||||
|
size 40484
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:753eecb930bf0659671f2f1ecb2a64ae226aac96c5365ed549c635fd90c8c729
|
||||||
|
size 40951
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:d0b37566f62e30fcb4f16364a9ac2c008a98a5429c3fa5724d7ef6455e79cf34
|
||||||
|
size 47995
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:dff4c4a80156b2977da007d6258d1cd6cefc93bc709ec33bff0e1b484604cf0a
|
||||||
|
size 38506
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:635c8d6bdaafb07706d95d5a9cb9a4e828ef312c31b33a80f6deff1a1c3b206f
|
||||||
|
size 34554
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:af5a843cc9cd5a51d986ba7dae7d2c6a088f862f46bac961667a1fb1d62b8a20
|
||||||
|
size 36966
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:059e598dc973e8c01dba4ef49accf47ea2bd74377dfbfb441c3c213fc77a1cee
|
||||||
|
size 38158
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:c34415877b279b2018d02667cc98b4857f4ae5bfa68d169f3226a0c99ed4620b
|
||||||
|
size 36699
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:dc02c16667b78ca5a9985da6816f17bbd0f7bb2307569aacaf3391ccd8431bd5
|
||||||
|
size 36567
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:4faece23a4780f5465abe42587f56933ecded55bf3ae966a9e6b634e7df1e049
|
||||||
|
size 42845
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:ee5c36535582ecb91b7315afa70bae38db2c7287760071293dbf5fac2b8000b4
|
||||||
|
size 43609
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:4c5d2d8a8e672c19e1ed9cd37291a6355f815fcd55157fca7a5966bba0baa830
|
||||||
|
size 43303
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:3c63e9045666c863d13865f4d40563a57ee7ec2110de023455a16d400eb67d26
|
||||||
|
size 41977
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:905960605341e3cf5987025bdc560e0141f1b5f1ff5e5b114d3f698712e3428d
|
||||||
|
size 39869
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:7cd6b2f0b51fe273ee5479e77a4762d4addb8e59560edbbb4a3ce4520ceb4a75
|
||||||
|
size 51493
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:3c63e9045666c863d13865f4d40563a57ee7ec2110de023455a16d400eb67d26
|
||||||
|
size 41977
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:8c25b6b3d2699895a4b34a61864d2b57aad9f44d5488830f5f2e061fddf42e32
|
||||||
|
size 40355
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:09a844501c3aea35d5ec151415b25c5be8960b98697a99dba450c8a25fb69c8b
|
||||||
|
size 39847
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:5d262c2c0681ea12ce6920af395bd56aa58a97448ade03997cd22e921d1e2f77
|
||||||
|
size 39564
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a1e7414cc8c9412c45e55ea77842b908a1979aa680a66609cbdfa6f5a91d87c9
|
||||||
|
size 37925
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a08860391a1f9854c2354ede0236e0ea78bd1242b134121e2e6a1903e7b8a6b2
|
||||||
|
size 35378
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:865c6148937271e08a14f4820652b9c9109b81eca71b715711460c8fe14fcc97
|
||||||
|
size 45654
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a1e7414cc8c9412c45e55ea77842b908a1979aa680a66609cbdfa6f5a91d87c9
|
||||||
|
size 37925
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:9b9d5ca3bccf0fbca3ced8392f2ca42dd448c032d97d81291357dc3a1bb33ea2
|
||||||
|
size 35806
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:5c6410198fc2f7e74279e21013718bf8326bfd871fa7ffa244470725f7bf3ae5
|
|
||||||
size 23045
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:3d6fce3b7fa9ac12e25f135ddeabb96dad4bba9422926e5451647255a712e481
|
|
||||||
size 22772
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:c788eab0495478d8253cbd11970c57c83670e2b761a675b413642db3c6ee57fa
|
|
||||||
size 25095
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:98f81fcfa9cb5c7c02f8322ca4e444df32e81acb5f0d79f39841f9b71c436594
|
|
||||||
size 26376
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:dbc67150e8d533fd473204fbf4c9360751b8498831c435a06e9ed1525dfa1d7b
|
|
||||||
size 21637
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:09b42b37d3e05cfe60187512403139b305c39d4d3ff59d122d73762a548d01fa
|
|
||||||
size 21364
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:50b1f5c23b436ca2f6045c4d2a6d14c7902a5f10646ce59522a02c2edc297374
|
|
||||||
size 22310
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:1b7f91366d3576553c7dd2bc469290fa094dc4095510bb5e841fa0a9b179e85d
|
|
||||||
size 23085
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue