change(roles and permissions): import codefrom roomdetails module
This commit is contained in:
parent
ab0e4429d6
commit
17dd75fe24
21 changed files with 65 additions and 27 deletions
|
|
@ -26,19 +26,19 @@ import io.element.android.annotations.ContributesNode
|
|||
import io.element.android.appconfig.LearnMoreConfig
|
||||
import io.element.android.features.call.api.CallType
|
||||
import io.element.android.features.call.api.ElementCallEntryPoint
|
||||
import io.element.android.features.rolesandpermissions.api.ChangeRoomMemberRolesEntryPoint
|
||||
import io.element.android.features.rolesandpermissions.api.ChangeRoomMemberRolesListType
|
||||
import io.element.android.features.knockrequests.api.list.KnockRequestsListEntryPoint
|
||||
import io.element.android.features.messages.api.MessagesEntryPoint
|
||||
import io.element.android.features.poll.api.history.PollHistoryEntryPoint
|
||||
import io.element.android.features.reportroom.api.ReportRoomEntryPoint
|
||||
import io.element.android.features.rolesandpermissions.api.ChangeRoomMemberRolesEntryPoint
|
||||
import io.element.android.features.rolesandpermissions.api.ChangeRoomMemberRolesListType
|
||||
import io.element.android.features.rolesandpermissions.api.RolesAndPermissionsEntryPoint
|
||||
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
||||
import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditNode
|
||||
import io.element.android.features.roomdetails.impl.invite.RoomInviteMembersNode
|
||||
import io.element.android.features.roomdetails.impl.members.RoomMemberListNode
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsNode
|
||||
import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsNode
|
||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsFlowNode
|
||||
import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyFlowNode
|
||||
import io.element.android.features.userprofile.shared.UserProfileNodeHelper
|
||||
import io.element.android.features.verifysession.api.OutgoingVerificationEntryPoint
|
||||
|
|
@ -82,6 +82,7 @@ class RoomDetailsFlowNode(
|
|||
private val outgoingVerificationEntryPoint: OutgoingVerificationEntryPoint,
|
||||
private val reportRoomEntryPoint: ReportRoomEntryPoint,
|
||||
private val changeRoomMemberRolesEntryPoint: ChangeRoomMemberRolesEntryPoint,
|
||||
private val rolesAndPermissionsEntryPoint: RolesAndPermissionsEntryPoint,
|
||||
) : BaseFlowNode<RoomDetailsFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = plugins.filterIsInstance<RoomDetailsEntryPoint.Params>().first().initialElement.toNavTarget(),
|
||||
|
|
@ -343,7 +344,7 @@ class RoomDetailsFlowNode(
|
|||
}
|
||||
|
||||
is NavTarget.AdminSettings -> {
|
||||
createNode<RolesAndPermissionsFlowNode>(buildContext)
|
||||
rolesAndPermissionsEntryPoint.createNode(this, buildContext)
|
||||
}
|
||||
NavTarget.PinnedMessagesList -> {
|
||||
val params = MessagesEntryPoint.Params(
|
||||
|
|
|
|||
|
|
@ -1,51 +0,0 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdetails.impl.analytics
|
||||
|
||||
import im.vector.app.features.analytics.plan.RoomModeration
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
|
||||
internal fun RoomMember.Role.toAnalyticsMemberRole(): RoomModeration.Role = when (this) {
|
||||
is RoomMember.Role.Owner -> RoomModeration.Role.Administrator // TODO - distinguish creator from admin
|
||||
RoomMember.Role.Admin -> RoomModeration.Role.Administrator
|
||||
RoomMember.Role.Moderator -> RoomModeration.Role.Moderator
|
||||
RoomMember.Role.User -> RoomModeration.Role.User
|
||||
}
|
||||
|
||||
internal fun analyticsMemberRoleForPowerLevel(powerLevel: Long): RoomModeration.Role {
|
||||
return RoomMember.Role.forPowerLevel(powerLevel).toAnalyticsMemberRole()
|
||||
}
|
||||
|
||||
internal fun AnalyticsService.trackPermissionChangeAnalytics(initial: RoomPowerLevelsValues?, updated: RoomPowerLevelsValues) {
|
||||
if (updated.ban != initial?.ban) {
|
||||
capture(RoomModeration(RoomModeration.Action.ChangePermissionsBanMembers, analyticsMemberRoleForPowerLevel(updated.ban)))
|
||||
}
|
||||
if (updated.invite != initial?.invite) {
|
||||
capture(RoomModeration(RoomModeration.Action.ChangePermissionsInviteUsers, analyticsMemberRoleForPowerLevel(updated.invite)))
|
||||
}
|
||||
if (updated.kick != initial?.kick) {
|
||||
capture(RoomModeration(RoomModeration.Action.ChangePermissionsKickMembers, analyticsMemberRoleForPowerLevel(updated.kick)))
|
||||
}
|
||||
if (updated.sendEvents != initial?.sendEvents) {
|
||||
capture(RoomModeration(RoomModeration.Action.ChangePermissionsSendMessages, analyticsMemberRoleForPowerLevel(updated.sendEvents)))
|
||||
}
|
||||
if (updated.redactEvents != initial?.redactEvents) {
|
||||
capture(RoomModeration(RoomModeration.Action.ChangePermissionsRedactMessages, analyticsMemberRoleForPowerLevel(updated.redactEvents)))
|
||||
}
|
||||
if (updated.roomName != initial?.roomName) {
|
||||
capture(RoomModeration(RoomModeration.Action.ChangePermissionsRoomName, analyticsMemberRoleForPowerLevel(updated.roomName)))
|
||||
}
|
||||
if (updated.roomAvatar != initial?.roomAvatar) {
|
||||
capture(RoomModeration(RoomModeration.Action.ChangePermissionsRoomAvatar, analyticsMemberRoleForPowerLevel(updated.roomAvatar)))
|
||||
}
|
||||
if (updated.roomTopic != initial?.roomTopic) {
|
||||
capture(RoomModeration(RoomModeration.Action.ChangePermissionsRoomTopic, analyticsMemberRoleForPowerLevel(updated.roomTopic)))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdetails.impl.rolesandpermissions
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
|
||||
sealed interface RolesAndPermissionsEvents {
|
||||
data object ChangeOwnRole : RolesAndPermissionsEvents
|
||||
data class DemoteSelfTo(val role: RoomMember.Role) : RolesAndPermissionsEvents
|
||||
data object ResetPermissions : RolesAndPermissionsEvents
|
||||
data object CancelPendingAction : RolesAndPermissionsEvents
|
||||
}
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdetails.impl.rolesandpermissions
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.rolesandpermissions.api.ChangeRoomMemberRolesEntryPoint
|
||||
import io.element.android.features.rolesandpermissions.api.ChangeRoomMemberRolesListType
|
||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.ChangeRoomPermissionsNode
|
||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.ChangeRoomPermissionsSection
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
@AssistedInject
|
||||
class RolesAndPermissionsFlowNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val changeRoomMemberRolesEntryPoint: ChangeRoomMemberRolesEntryPoint,
|
||||
private val joinedRoom: JoinedRoom,
|
||||
) : BaseFlowNode<RolesAndPermissionsFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.AdminSettings,
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
) {
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
data object AdminSettings : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object AdminList : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object ModeratorList : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class ChangeRoomPermissions(val section: ChangeRoomPermissionsSection) : NavTarget
|
||||
}
|
||||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
whenChildAttached { lifecycle, node: ChangeRoomMemberRolesEntryPoint.NodeProxy ->
|
||||
lifecycle.coroutineScope.launch {
|
||||
node.waitForRoleChanged()
|
||||
backstack.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
is NavTarget.AdminSettings -> {
|
||||
val callback = object : RolesAndPermissionsNode.Callback {
|
||||
override fun openAdminList() {
|
||||
backstack.push(NavTarget.AdminList)
|
||||
}
|
||||
|
||||
override fun openModeratorList() {
|
||||
backstack.push(NavTarget.ModeratorList)
|
||||
}
|
||||
|
||||
override fun openEditRoomDetailsPermissions() {
|
||||
backstack.push(NavTarget.ChangeRoomPermissions(ChangeRoomPermissionsSection.RoomDetails))
|
||||
}
|
||||
|
||||
override fun openMessagesAndContentPermissions() {
|
||||
backstack.push(NavTarget.ChangeRoomPermissions(ChangeRoomPermissionsSection.MessagesAndContent))
|
||||
}
|
||||
|
||||
override fun openModerationPermissions() {
|
||||
backstack.push(NavTarget.ChangeRoomPermissions(ChangeRoomPermissionsSection.MembershipModeration))
|
||||
}
|
||||
}
|
||||
createNode<RolesAndPermissionsNode>(
|
||||
buildContext = buildContext,
|
||||
plugins = listOf(callback),
|
||||
)
|
||||
}
|
||||
is NavTarget.AdminList -> {
|
||||
changeRoomMemberRolesEntryPoint.createNode(
|
||||
parentNode = this,
|
||||
buildContext = buildContext,
|
||||
room = joinedRoom,
|
||||
listType = ChangeRoomMemberRolesListType.Admins,
|
||||
)
|
||||
}
|
||||
is NavTarget.ModeratorList -> {
|
||||
changeRoomMemberRolesEntryPoint.createNode(
|
||||
parentNode = this,
|
||||
buildContext = buildContext,
|
||||
room = joinedRoom,
|
||||
listType = ChangeRoomMemberRolesListType.Moderators,
|
||||
)
|
||||
}
|
||||
is NavTarget.ChangeRoomPermissions -> {
|
||||
val inputs = ChangeRoomPermissionsNode.Inputs(navTarget.section)
|
||||
createNode<ChangeRoomPermissionsNode>(
|
||||
buildContext = buildContext,
|
||||
plugins = listOf(inputs),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
BackstackView()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdetails.impl.rolesandpermissions
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.room.BaseRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.ui.model.roleOf
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
@AssistedInject
|
||||
class RolesAndPermissionsNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: RolesAndPermissionsPresenter,
|
||||
private val room: BaseRoom,
|
||||
) : Node(buildContext, plugins = plugins), RolesAndPermissionsNavigator {
|
||||
interface Callback : Plugin, RolesAndPermissionsNavigator {
|
||||
override fun openAdminList()
|
||||
override fun openModeratorList()
|
||||
override fun openEditRoomDetailsPermissions()
|
||||
override fun openMessagesAndContentPermissions()
|
||||
override fun openModerationPermissions()
|
||||
override fun onBackClick() {}
|
||||
}
|
||||
|
||||
private val callback: Callback = callback()
|
||||
|
||||
@Stable
|
||||
private val navigator = object : RolesAndPermissionsNavigator by callback {
|
||||
override fun onBackClick() {
|
||||
navigateUp()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
|
||||
// If the user is not an admin anymore, exit this section since they won't have permissions to use it
|
||||
lifecycleScope.launch {
|
||||
room.roomInfoFlow
|
||||
.filter { info ->
|
||||
val role = info.roleOf(room.sessionId)
|
||||
role != RoomMember.Role.Admin && role !is RoomMember.Role.Owner
|
||||
}
|
||||
.take(1)
|
||||
.onEach { navigateUp() }
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
RolesAndPermissionsView(
|
||||
state = state,
|
||||
rolesAndPermissionsNavigator = navigator,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
interface RolesAndPermissionsNavigator {
|
||||
fun onBackClick() {}
|
||||
fun openAdminList() {}
|
||||
fun openModeratorList() {}
|
||||
fun openEditRoomDetailsPermissions() {}
|
||||
fun openMessagesAndContentPermissions() {}
|
||||
fun openModerationPermissions() {}
|
||||
}
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdetails.impl.rolesandpermissions
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import dev.zacsweers.metro.Inject
|
||||
import im.vector.app.features.analytics.plan.RoomModeration
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runUpdatingState
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomInfo
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.activeRoomMembers
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
|
||||
import io.element.android.libraries.matrix.ui.model.roleOf
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Inject
|
||||
class RolesAndPermissionsPresenter(
|
||||
private val room: JoinedRoom,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : Presenter<RolesAndPermissionsState> {
|
||||
@Composable
|
||||
override fun present(): RolesAndPermissionsState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val roomInfo by room.roomInfoFlow.collectAsState()
|
||||
val roomMembers by room.membersStateFlow.collectAsState()
|
||||
// Get the list of active room members (joined or invited), in order to filter members present in the power
|
||||
// level state Event.
|
||||
val activeRoomMemberIds by remember {
|
||||
derivedStateOf {
|
||||
roomMembers.activeRoomMembers().map { it.userId }
|
||||
}
|
||||
}
|
||||
val moderatorCount by remember {
|
||||
derivedStateOf {
|
||||
roomInfo.userCountWithRole(activeRoomMemberIds, RoomMember.Role.Moderator)
|
||||
}
|
||||
}
|
||||
val adminCount by remember {
|
||||
derivedStateOf {
|
||||
val admins = roomInfo.userCountWithRole(activeRoomMemberIds, RoomMember.Role.Admin)
|
||||
val ownersCount = if (roomInfo.privilegedCreatorRole) {
|
||||
val superAdmins = roomInfo.userCountWithRole(activeRoomMemberIds, RoomMember.Role.Owner(isCreator = false))
|
||||
val creators = roomInfo.userCountWithRole(activeRoomMemberIds, RoomMember.Role.Owner(isCreator = true))
|
||||
superAdmins + creators
|
||||
} else {
|
||||
0
|
||||
}
|
||||
admins + ownersCount
|
||||
}
|
||||
}
|
||||
val canDemoteSelf = remember { derivedStateOf { roomInfo.roleOf(room.sessionId) !is RoomMember.Role.Owner } }
|
||||
val changeOwnRoleAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||
val resetPermissionsAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||
|
||||
fun handleEvent(event: RolesAndPermissionsEvents) {
|
||||
when (event) {
|
||||
is RolesAndPermissionsEvents.ChangeOwnRole -> {
|
||||
changeOwnRoleAction.value = AsyncAction.ConfirmingNoParams
|
||||
}
|
||||
is RolesAndPermissionsEvents.CancelPendingAction -> {
|
||||
changeOwnRoleAction.value = AsyncAction.Uninitialized
|
||||
resetPermissionsAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
is RolesAndPermissionsEvents.DemoteSelfTo -> coroutineScope.demoteSelfTo(
|
||||
role = event.role,
|
||||
changeOwnRoleAction = changeOwnRoleAction,
|
||||
)
|
||||
is RolesAndPermissionsEvents.ResetPermissions -> if (resetPermissionsAction.value.isConfirming()) {
|
||||
coroutineScope.resetPermissions(resetPermissionsAction)
|
||||
} else {
|
||||
resetPermissionsAction.value = AsyncAction.ConfirmingNoParams
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return RolesAndPermissionsState(
|
||||
roomSupportsOwnerRole = roomInfo.privilegedCreatorRole,
|
||||
adminCount = adminCount,
|
||||
moderatorCount = moderatorCount,
|
||||
canDemoteSelf = canDemoteSelf.value,
|
||||
changeOwnRoleAction = changeOwnRoleAction.value,
|
||||
resetPermissionsAction = resetPermissionsAction.value,
|
||||
eventSink = { handleEvent(it) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.demoteSelfTo(
|
||||
role: RoomMember.Role,
|
||||
changeOwnRoleAction: MutableState<AsyncAction<Unit>>,
|
||||
) = launch(dispatchers.io) {
|
||||
runUpdatingState(changeOwnRoleAction) {
|
||||
room.updateUsersRoles(listOf(UserRoleChange(room.sessionId, role)))
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.resetPermissions(
|
||||
resetPermissionsAction: MutableState<AsyncAction<Unit>>,
|
||||
) = launch(dispatchers.io) {
|
||||
runUpdatingState(resetPermissionsAction) {
|
||||
analyticsService.capture(RoomModeration(RoomModeration.Action.ResetPermissions))
|
||||
room.resetPowerLevels()
|
||||
}
|
||||
}
|
||||
|
||||
private fun RoomInfo.userCountWithRole(userIds: List<UserId>, role: RoomMember.Role): Int {
|
||||
return usersWithRole(role).filter { it in userIds }.size
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdetails.impl.rolesandpermissions
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
|
||||
data class RolesAndPermissionsState(
|
||||
val roomSupportsOwnerRole: Boolean,
|
||||
val adminCount: Int,
|
||||
val moderatorCount: Int,
|
||||
val canDemoteSelf: Boolean,
|
||||
val changeOwnRoleAction: AsyncAction<Unit>,
|
||||
val resetPermissionsAction: AsyncAction<Unit>,
|
||||
val eventSink: (RolesAndPermissionsEvents) -> Unit,
|
||||
)
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdetails.impl.rolesandpermissions
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
|
||||
class RolesAndPermissionsStateProvider : PreviewParameterProvider<RolesAndPermissionsState> {
|
||||
override val values: Sequence<RolesAndPermissionsState>
|
||||
get() = sequenceOf(
|
||||
aRolesAndPermissionsState(roomSupportsOwners = false),
|
||||
aRolesAndPermissionsState(adminCount = 1, moderatorCount = 2),
|
||||
aRolesAndPermissionsState(
|
||||
adminCount = 1,
|
||||
moderatorCount = 2,
|
||||
changeOwnRoleAction = AsyncAction.ConfirmingNoParams,
|
||||
),
|
||||
aRolesAndPermissionsState(
|
||||
adminCount = 1,
|
||||
moderatorCount = 2,
|
||||
changeOwnRoleAction = AsyncAction.Loading,
|
||||
),
|
||||
aRolesAndPermissionsState(
|
||||
adminCount = 1,
|
||||
moderatorCount = 2,
|
||||
changeOwnRoleAction = AsyncAction.Failure(IllegalStateException("Failed to change role")),
|
||||
),
|
||||
aRolesAndPermissionsState(
|
||||
adminCount = 1,
|
||||
moderatorCount = 2,
|
||||
resetPermissionsAction = AsyncAction.ConfirmingNoParams,
|
||||
),
|
||||
aRolesAndPermissionsState(
|
||||
adminCount = 1,
|
||||
moderatorCount = 2,
|
||||
resetPermissionsAction = AsyncAction.Loading,
|
||||
),
|
||||
aRolesAndPermissionsState(
|
||||
adminCount = 1,
|
||||
moderatorCount = 2,
|
||||
resetPermissionsAction = AsyncAction.Failure(IllegalStateException("Failed to reset permissions")),
|
||||
),
|
||||
aRolesAndPermissionsState(canDemoteSelf = false),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aRolesAndPermissionsState(
|
||||
roomSupportsOwners: Boolean = true,
|
||||
adminCount: Int = 0,
|
||||
moderatorCount: Int = 0,
|
||||
canDemoteSelf: Boolean = true,
|
||||
changeOwnRoleAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
resetPermissionsAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
eventSink: (RolesAndPermissionsEvents) -> Unit = {},
|
||||
) = RolesAndPermissionsState(
|
||||
roomSupportsOwnerRole = roomSupportsOwners,
|
||||
adminCount = adminCount,
|
||||
canDemoteSelf = canDemoteSelf,
|
||||
moderatorCount = moderatorCount,
|
||||
changeOwnRoleAction = changeOwnRoleAction,
|
||||
resetPermissionsAction = resetPermissionsAction,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
|
@ -1,206 +0,0 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdetails.impl.rolesandpermissions
|
||||
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
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.ModalBottomSheet
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.hide
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun RolesAndPermissionsView(
|
||||
state: RolesAndPermissionsState,
|
||||
rolesAndPermissionsNavigator: RolesAndPermissionsNavigator,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
PreferencePage(
|
||||
modifier = modifier,
|
||||
title = stringResource(R.string.screen_room_roles_and_permissions_title),
|
||||
onBackClick = rolesAndPermissionsNavigator::onBackClick,
|
||||
) {
|
||||
ListSectionHeader(title = stringResource(R.string.screen_room_roles_and_permissions_roles_header), hasDivider = false)
|
||||
|
||||
val adminsTitle = if (state.roomSupportsOwnerRole) {
|
||||
stringResource(R.string.screen_room_roles_and_permissions_admins_and_owners)
|
||||
} else {
|
||||
stringResource(R.string.screen_room_roles_and_permissions_admins)
|
||||
}
|
||||
ListItem(
|
||||
headlineContent = { Text(adminsTitle) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Admin())),
|
||||
trailingContent = ListItemContent.Text("${state.adminCount}"),
|
||||
onClick = { rolesAndPermissionsNavigator.openAdminList() },
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_moderators)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ChatProblem())),
|
||||
trailingContent = ListItemContent.Text("${state.moderatorCount}"),
|
||||
onClick = { rolesAndPermissionsNavigator.openModeratorList() },
|
||||
)
|
||||
if (state.canDemoteSelf) {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_change_my_role)) },
|
||||
onClick = { state.eventSink(RolesAndPermissionsEvents.ChangeOwnRole) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Edit()))
|
||||
)
|
||||
}
|
||||
ListSectionHeader(title = stringResource(R.string.screen_room_roles_and_permissions_permissions_header), hasDivider = true)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_room_details)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Info())),
|
||||
onClick = { rolesAndPermissionsNavigator.openEditRoomDetailsPermissions() },
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_messages_and_content)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Chat())),
|
||||
onClick = { rolesAndPermissionsNavigator.openMessagesAndContentPermissions() },
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_member_moderation)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.User())),
|
||||
onClick = { rolesAndPermissionsNavigator.openModerationPermissions() },
|
||||
)
|
||||
HorizontalDivider()
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_reset)) },
|
||||
onClick = { state.eventSink(RolesAndPermissionsEvents.ResetPermissions) },
|
||||
style = ListItemStyle.Destructive,
|
||||
)
|
||||
}
|
||||
|
||||
AsyncActionView(
|
||||
async = state.resetPermissionsAction,
|
||||
confirmationDialog = {
|
||||
ConfirmationDialog(
|
||||
title = stringResource(R.string.screen_room_roles_and_permissions_reset_confirm_title),
|
||||
content = stringResource(R.string.screen_room_roles_and_permissions_reset_confirm_description),
|
||||
submitText = stringResource(CommonStrings.action_reset),
|
||||
destructiveSubmit = true,
|
||||
onSubmitClick = { state.eventSink(RolesAndPermissionsEvents.ResetPermissions) },
|
||||
onDismiss = { state.eventSink(RolesAndPermissionsEvents.CancelPendingAction) },
|
||||
)
|
||||
},
|
||||
onSuccess = { state.eventSink(RolesAndPermissionsEvents.CancelPendingAction) },
|
||||
onErrorDismiss = { state.eventSink(RolesAndPermissionsEvents.CancelPendingAction) }
|
||||
)
|
||||
|
||||
when (state.changeOwnRoleAction) {
|
||||
is AsyncAction.Confirming -> {
|
||||
ChangeOwnRoleBottomSheet(
|
||||
eventSink = state.eventSink,
|
||||
)
|
||||
}
|
||||
is AsyncAction.Loading -> {
|
||||
ProgressDialog()
|
||||
}
|
||||
is AsyncAction.Failure -> {
|
||||
ErrorDialog(
|
||||
content = stringResource(CommonStrings.error_unknown),
|
||||
onSubmit = { state.eventSink(RolesAndPermissionsEvents.CancelPendingAction) }
|
||||
)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ChangeOwnRoleBottomSheet(
|
||||
eventSink: (RolesAndPermissionsEvents) -> Unit,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
fun dismiss() {
|
||||
sheetState.hide(coroutineScope) {
|
||||
eventSink(RolesAndPermissionsEvents.CancelPendingAction)
|
||||
}
|
||||
}
|
||||
ModalBottomSheet(
|
||||
modifier = Modifier
|
||||
.systemBarsPadding()
|
||||
.navigationBarsPadding(),
|
||||
sheetState = sheetState,
|
||||
onDismissRequest = ::dismiss,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(14.dp),
|
||||
text = stringResource(R.string.screen_room_roles_and_permissions_change_my_role),
|
||||
style = ElementTheme.typography.fontBodyLgMedium,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 14.dp, end = 14.dp, bottom = 16.dp),
|
||||
text = stringResource(R.string.screen_room_change_role_confirm_demote_self_description),
|
||||
style = ElementTheme.typography.fontBodyLgRegular,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_change_role_demote_to_moderator)) },
|
||||
onClick = {
|
||||
sheetState.hide(coroutineScope) {
|
||||
eventSink(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.Moderator))
|
||||
}
|
||||
},
|
||||
style = ListItemStyle.Destructive,
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_change_role_demote_to_member)) },
|
||||
onClick = {
|
||||
sheetState.hide(coroutineScope) {
|
||||
eventSink(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.User))
|
||||
}
|
||||
},
|
||||
style = ListItemStyle.Destructive,
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(CommonStrings.action_cancel)) },
|
||||
onClick = ::dismiss,
|
||||
style = ListItemStyle.Primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun RolesAndPermissionsViewPreview(@PreviewParameter(RolesAndPermissionsStateProvider::class) state: RolesAndPermissionsState) {
|
||||
ElementPreview {
|
||||
RolesAndPermissionsView(
|
||||
state = state,
|
||||
rolesAndPermissionsNavigator = object : RolesAndPermissionsNavigator {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.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
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.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 dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.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)
|
||||
@AssistedInject
|
||||
class ChangeRoomPermissionsNode(
|
||||
@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.create(inputs.section)
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
ChangeRoomPermissionsView(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
onBackClick = this::navigateUp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
enum class ChangeRoomPermissionsSection : Parcelable {
|
||||
RoomDetails,
|
||||
MessagesAndContent,
|
||||
MembershipModeration,
|
||||
}
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.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 dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.features.roomdetails.impl.analytics.trackPermissionChangeAnalytics
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@AssistedInject
|
||||
class ChangeRoomPermissionsPresenter(
|
||||
@Assisted private val section: ChangeRoomPermissionsSection,
|
||||
private val room: JoinedRoom,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : 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<RoomPowerLevelsValues?>(null)
|
||||
private var currentPermissions by mutableStateOf<RoomPowerLevelsValues?>(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.ConfirmingNoParams
|
||||
}
|
||||
}
|
||||
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 {
|
||||
analyticsService.trackPermissionChangeAnalytics(initialPermissions, updatedRoomPowerLevels)
|
||||
initialPermissions = currentPermissions
|
||||
saveAction = AsyncAction.Success(Unit)
|
||||
}
|
||||
.onFailure {
|
||||
saveAction = AsyncAction.Failure(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdetails.impl.rolesandpermissions.permissions
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class ChangeRoomPermissionsState(
|
||||
val section: ChangeRoomPermissionsSection,
|
||||
val currentPermissions: RoomPowerLevelsValues?,
|
||||
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
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.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.RoomPowerLevelsValues
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
class ChangeRoomPermissionsStateProvider : 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.ConfirmingNoParams,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aChangeRoomPermissionsState(
|
||||
section: ChangeRoomPermissionsSection,
|
||||
currentPermissions: RoomPowerLevelsValues = 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.toImmutableList(),
|
||||
hasChanges = hasChanges,
|
||||
saveAction = saveAction,
|
||||
confirmExitAction = confirmExitAction,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
private fun previewPermissions(): RoomPowerLevelsValues {
|
||||
return RoomPowerLevelsValues(
|
||||
// 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,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,189 +0,0 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdetails.impl.rolesandpermissions.permissions
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
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.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.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.RoomPowerLevelsValues
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ChangeRoomPermissionsView(
|
||||
state: ChangeRoomPermissionsState,
|
||||
onBackClick: () -> 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(
|
||||
titleStr = title,
|
||||
navigationIcon = {
|
||||
BackButton(onClick = { state.eventSink(ChangeRoomPermissionsEvent.Exit) })
|
||||
},
|
||||
actions = {
|
||||
TextButton(
|
||||
text = stringResource(CommonStrings.action_save),
|
||||
onClick = { state.eventSink(ChangeRoomPermissionsEvent.Save) },
|
||||
enabled = state.hasChanges,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
for ((index, permissionItem) in state.items.withIndex()) {
|
||||
item {
|
||||
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 = { onBackClick() },
|
||||
onErrorDismiss = { state.eventSink(ChangeRoomPermissionsEvent.ResetPendingActions) }
|
||||
)
|
||||
|
||||
AsyncActionView(
|
||||
async = state.confirmExitAction,
|
||||
onSuccess = { onBackClick() },
|
||||
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),
|
||||
onSubmitClick = { state.eventSink(ChangeRoomPermissionsEvent.Save) },
|
||||
onDismiss = { state.eventSink(ChangeRoomPermissionsEvent.Exit) }
|
||||
)
|
||||
},
|
||||
onErrorDismiss = {},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SelectRoleItem(
|
||||
permissionsItem: RoomPermissionType,
|
||||
role: RoomMember.Role,
|
||||
currentPermissions: RoomPowerLevelsValues?,
|
||||
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)
|
||||
else -> error("Unsupported role selected: $role")
|
||||
}
|
||||
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 RoomPowerLevelsValues.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(ChangeRoomPermissionsStateProvider::class) state: ChangeRoomPermissionsState) {
|
||||
ElementPreview {
|
||||
ChangeRoomPermissionsView(
|
||||
state = state,
|
||||
onBackClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue