Merge pull request #2627 from vector-im/feature/bma/edit_power_level

Edit power level
This commit is contained in:
Benoit Marty 2021-01-12 10:31:17 +01:00 committed by GitHub
commit 5431584b3c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 874 additions and 44 deletions

View file

@ -3,6 +3,7 @@ Changes in Element 1.0.14 (2020-XX-XX)
Features ✨:
- Enable url previews for notices (#2562)
- Edit room permissions (#2471)
Improvements 🙌:
- Add System theme option and set as default (#904, #2387)

View file

@ -37,6 +37,6 @@ class SenderNotificationPermissionCondition(
fun isSatisfied(event: Event, powerLevels: PowerLevelsContent): Boolean {
val powerLevelsHelper = PowerLevelsHelper(powerLevels)
return event.senderId != null && powerLevelsHelper.getUserPowerLevelValue(event.senderId) >= powerLevelsHelper.notificationLevel(key)
return event.senderId != null && powerLevelsHelper.getUserPowerLevelValue(event.senderId) >= powerLevels.notificationLevel(key)
}
}

View file

@ -25,28 +25,85 @@ import org.matrix.android.sdk.api.session.room.powerlevels.Role
*/
@JsonClass(generateAdapter = true)
data class PowerLevelsContent(
/**
* The level required to ban a user. Defaults to 50 if unspecified.
*/
@Json(name = "ban") val ban: Int = Role.Moderator.value,
/**
* The level required to kick a user. Defaults to 50 if unspecified.
*/
@Json(name = "kick") val kick: Int = Role.Moderator.value,
/**
* The level required to invite a user. Defaults to 50 if unspecified.
*/
@Json(name = "invite") val invite: Int = Role.Moderator.value,
/**
* The level required to redact an event. Defaults to 50 if unspecified.
*/
@Json(name = "redact") val redact: Int = Role.Moderator.value,
/**
* The default level required to send message events. Can be overridden by the events key. Defaults to 0 if unspecified.
*/
@Json(name = "events_default") val eventsDefault: Int = Role.Default.value,
@Json(name = "events") val events: MutableMap<String, Int> = HashMap(),
/**
* The level required to send specific event types. This is a mapping from event type to power level required.
*/
@Json(name = "events") val events: Map<String, Int> = emptyMap(),
/**
* The default power level for every user in the room, unless their user_id is mentioned in the users key. Defaults to 0 if unspecified.
*/
@Json(name = "users_default") val usersDefault: Int = Role.Default.value,
@Json(name = "users") val users: MutableMap<String, Int> = HashMap(),
/**
* The power levels for specific users. This is a mapping from user_id to power level for that user.
*/
@Json(name = "users") val users: Map<String, Int> = emptyMap(),
/**
* The default level required to send state events. Can be overridden by the events key. Defaults to 50 if unspecified.
*/
@Json(name = "state_default") val stateDefault: Int = Role.Moderator.value,
@Json(name = "notifications") val notifications: Map<String, Any> = HashMap()
/**
* The power level requirements for specific notification types. This is a mapping from key to power level for that notifications key.
*/
@Json(name = "notifications") val notifications: Map<String, Any> = emptyMap()
) {
/**
* Alter this content with a new power level for the specified user
* Return a copy of this content with a new power level for the specified user
*
* @param userId the userId to alter the power level of
* @param powerLevel the new power level, or null to set the default value.
*/
fun setUserPowerLevel(userId: String, powerLevel: Int?) {
if (powerLevel == null || powerLevel == usersDefault) {
users.remove(userId)
} else {
users[userId] = powerLevel
fun setUserPowerLevel(userId: String, powerLevel: Int?): PowerLevelsContent {
return copy(
users = users.toMutableMap().apply {
if (powerLevel == null || powerLevel == usersDefault) {
remove(userId)
} else {
put(userId, powerLevel)
}
}
)
}
/**
* Get the notification level for a dedicated key.
*
* @param key the notification key
* @return the level, default to Moderator if the key is not found
*/
fun notificationLevel(key: String): Int {
return when (val value = notifications[key]) {
// the first implementation was a string value
is String -> value.toInt()
is Double -> value.toInt()
is Int -> value
else -> Role.Moderator.value
}
}
companion object {
/**
* Key to use for content.notifications and get the level required to trigger an @room notification. Defaults to 50 if unspecified.
*/
const val NOTIFICATIONS_ROOM_KEY = "room"
}
}

View file

@ -108,19 +108,4 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
val powerLevel = getUserPowerLevelValue(userId)
return powerLevel >= powerLevelsContent.redact
}
/**
* Get the notification level for a dedicated key.
*
* @param key the notification key
* @return the level
*/
fun notificationLevel(key: String): Int {
return when (val value = powerLevelsContent.notifications[key]) {
// the first implementation was a string value
is String -> value.toInt()
is Int -> value
else -> Role.Moderator.value
}
}
}

View file

@ -35,13 +35,11 @@ import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.api.util.MimeTypes
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.session.content.FileUploader
import org.matrix.android.sdk.internal.session.room.alias.AddRoomAliasTask
internal class DefaultStateService @AssistedInject constructor(@Assisted private val roomId: String,
private val stateEventDataSource: StateEventDataSource,
private val sendStateTask: SendStateTask,
private val fileUploader: FileUploader,
private val addRoomAliasTask: AddRoomAliasTask
private val fileUploader: FileUploader
) : StateService {
@AssistedInject.Factory
@ -74,11 +72,19 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
roomId = roomId,
stateKey = stateKey,
eventType = eventType,
body = body
body = body.toSafeJson(eventType)
)
sendStateTask.execute(params)
}
private fun JsonDict.toSafeJson(eventType: String): JsonDict {
// Safe treatment for PowerLevelContent
return when (eventType) {
EventType.STATE_ROOM_POWER_LEVELS -> toSafePowerLevelsContentDict()
else -> this
}
}
override suspend fun updateTopic(topic: String) {
sendStateEvent(
eventType = EventType.STATE_ROOM_TOPIC,

View file

@ -0,0 +1,60 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.room.state
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.powerlevels.Role
import org.matrix.android.sdk.api.util.JsonDict
@JsonClass(generateAdapter = true)
internal data class SerializablePowerLevelsContent(
@Json(name = "ban") val ban: Int = Role.Moderator.value,
@Json(name = "kick") val kick: Int = Role.Moderator.value,
@Json(name = "invite") val invite: Int = Role.Moderator.value,
@Json(name = "redact") val redact: Int = Role.Moderator.value,
@Json(name = "events_default") val eventsDefault: Int = Role.Default.value,
@Json(name = "events") val events: Map<String, Int> = emptyMap(),
@Json(name = "users_default") val usersDefault: Int = Role.Default.value,
@Json(name = "users") val users: Map<String, Int> = emptyMap(),
@Json(name = "state_default") val stateDefault: Int = Role.Moderator.value,
// `Int` is the diff here (instead of `Any`)
@Json(name = "notifications") val notifications: Map<String, Int> = emptyMap()
)
internal fun JsonDict.toSafePowerLevelsContentDict(): JsonDict {
return toModel<PowerLevelsContent>()
?.let { content ->
SerializablePowerLevelsContent(
ban = content.ban,
kick = content.kick,
invite = content.invite,
redact = content.redact,
eventsDefault = content.eventsDefault,
events = content.events,
usersDefault = content.usersDefault,
users = content.users,
stateDefault = content.stateDefault,
notifications = content.notifications.mapValues { content.notificationLevel(it.key) }
)
}
?.toContent()
?: emptyMap()
}

View file

@ -247,6 +247,7 @@ class UiAllScreensSanityTest {
// Room settings
clickListItem(R.id.matrixProfileRecyclerView, 3)
navigateToRoomParameters()
pressBack()
// Notifications
@ -285,6 +286,31 @@ class UiAllScreensSanityTest {
pressBack()
}
private fun navigateToRoomParameters() {
// Room addresses
clickListItem(R.id.roomSettingsRecyclerView, 4)
onView(isRoot()).perform(waitForView(withText(R.string.room_alias_published_alias_title)))
pressBack()
// Room permissions
clickListItem(R.id.roomSettingsRecyclerView, 6)
onView(isRoot()).perform(waitForView(withText(R.string.room_permissions_title)))
clickOn(R.string.room_permissions_change_room_avatar)
clickDialogNegativeButton()
// Toggle
clickOn(R.string.show_advanced)
clickOn(R.string.hide_advanced)
pressBack()
// Room history readability
clickListItem(R.id.roomSettingsRecyclerView, 8)
pressBack()
// Room access
clickListItem(R.id.roomSettingsRecyclerView, 10)
pressBack()
}
private fun navigateToInvite() {
assertDisplayed(R.id.inviteUsersButton)
clickOn(R.id.inviteUsersButton)

View file

@ -84,6 +84,7 @@ import im.vector.app.features.roomprofile.banned.RoomBannedMemberListFragment
import im.vector.app.features.roomprofile.members.RoomMemberListFragment
import im.vector.app.features.roomprofile.settings.RoomSettingsFragment
import im.vector.app.features.roomprofile.alias.RoomAliasFragment
import im.vector.app.features.roomprofile.permissions.RoomPermissionsFragment
import im.vector.app.features.roomprofile.uploads.RoomUploadsFragment
import im.vector.app.features.roomprofile.uploads.files.RoomUploadsFilesFragment
import im.vector.app.features.roomprofile.uploads.media.RoomUploadsMediaFragment
@ -364,6 +365,11 @@ interface FragmentModule {
@FragmentKey(RoomAliasFragment::class)
fun bindRoomAliasFragment(fragment: RoomAliasFragment): Fragment
@Binds
@IntoMap
@FragmentKey(RoomPermissionsFragment::class)
fun bindRoomPermissionsFragment(fragment: RoomPermissionsFragment): Fragment
@Binds
@IntoMap
@FragmentKey(RoomMemberProfileFragment::class)

View file

@ -887,13 +887,15 @@ class RoomDetailViewModel @AssistedInject constructor(
}
private fun handleSetUserPowerLevel(setUserPowerLevel: ParsedCommand.SetUserPowerLevel) {
val currentPowerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS)
val newPowerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS)
?.content
?.toModel<PowerLevelsContent>() ?: return
?.toModel<PowerLevelsContent>()
?.setUserPowerLevel(setUserPowerLevel.userId, setUserPowerLevel.powerLevel)
?.toContent()
?: return
launchSlashCommandFlowSuspendable {
currentPowerLevelsContent.setUserPowerLevel(setUserPowerLevel.userId, setUserPowerLevel.powerLevel)
room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, null, currentPowerLevelsContent.toContent())
room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, null, newPowerLevelsContent)
}
}

View file

@ -324,7 +324,7 @@ class RoomMemberProfileFragment @Inject constructor(
}
override fun onEditPowerLevel(currentRole: Role) {
EditPowerLevelDialogs.showChoice(requireActivity(), currentRole) { newPowerLevel ->
EditPowerLevelDialogs.showChoice(requireActivity(), R.string.power_level_edit_title, currentRole) { newPowerLevel ->
viewModel.handle(RoomMemberProfileAction.SetPowerLevel(currentRole.value, newPowerLevel, true))
}
}

View file

@ -162,11 +162,13 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
} else if (action.askForValidation && state.isMine) {
_viewEvents.post(RoomMemberProfileViewEvents.ShowPowerLevelDemoteWarning(action.previousValue, action.newValue))
} else {
currentPowerLevelsContent.setUserPowerLevel(state.userId, action.newValue)
val newPowerLevelsContent = currentPowerLevelsContent
.setUserPowerLevel(state.userId, action.newValue)
.toContent()
viewModelScope.launch {
_viewEvents.post(RoomMemberProfileViewEvents.Loading())
try {
room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, null, currentPowerLevelsContent.toContent())
room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, null, newPowerLevelsContent)
_viewEvents.post(RoomMemberProfileViewEvents.OnSetPowerLevelSuccess)
} catch (failure: Throwable) {
_viewEvents.post(RoomMemberProfileViewEvents.Failure(failure))

View file

@ -19,6 +19,7 @@ package im.vector.app.features.roommemberprofile.powerlevel
import android.app.Activity
import android.content.DialogInterface
import android.view.KeyEvent
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import im.vector.app.R
@ -29,7 +30,10 @@ import org.matrix.android.sdk.api.session.room.powerlevels.Role
object EditPowerLevelDialogs {
fun showChoice(activity: Activity, currentRole: Role, listener: (Int) -> Unit) {
fun showChoice(activity: Activity,
@StringRes titleRes: Int,
currentRole: Role,
listener: (Int) -> Unit) {
val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_edit_power_level, null)
val views = DialogEditPowerLevelBinding.bind(dialogLayout)
views.powerLevelRadioGroup.setOnCheckedChangeListener { _, checkedId ->
@ -45,7 +49,7 @@ object EditPowerLevelDialogs {
}
AlertDialog.Builder(activity)
.setTitle(R.string.power_level_edit_title)
.setTitle(titleRes)
.setView(dialogLayout)
.setPositiveButton(R.string.edit) { _, _ ->
val newValue = when (views.powerLevelRadioGroup.checkedRadioButtonId) {

View file

@ -27,6 +27,7 @@ import im.vector.app.R
import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.addFragment
import im.vector.app.core.extensions.addFragmentToBackstack
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.ToolbarConfigurable
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivitySimpleBinding
@ -38,6 +39,7 @@ import im.vector.app.features.roomprofile.banned.RoomBannedMemberListFragment
import im.vector.app.features.roomprofile.members.RoomMemberListFragment
import im.vector.app.features.roomprofile.settings.RoomSettingsFragment
import im.vector.app.features.roomprofile.alias.RoomAliasFragment
import im.vector.app.features.roomprofile.permissions.RoomPermissionsFragment
import im.vector.app.features.roomprofile.uploads.RoomUploadsFragment
import javax.inject.Inject
@ -102,12 +104,13 @@ class RoomProfileActivity :
.observe()
.subscribe { sharedAction ->
when (sharedAction) {
is RoomProfileSharedAction.OpenRoomMembers -> openRoomMembers()
is RoomProfileSharedAction.OpenRoomSettings -> openRoomSettings()
is RoomProfileSharedAction.OpenRoomAliasesSettings -> openRoomAlias()
is RoomProfileSharedAction.OpenRoomUploads -> openRoomUploads()
is RoomProfileSharedAction.OpenBannedRoomMembers -> openBannedRoomMembers()
}
RoomProfileSharedAction.OpenRoomMembers -> openRoomMembers()
RoomProfileSharedAction.OpenRoomSettings -> openRoomSettings()
RoomProfileSharedAction.OpenRoomAliasesSettings -> openRoomAlias()
RoomProfileSharedAction.OpenRoomPermissionsSettings -> openRoomPermissions()
RoomProfileSharedAction.OpenRoomUploads -> openRoomUploads()
RoomProfileSharedAction.OpenBannedRoomMembers -> openBannedRoomMembers()
}.exhaustive
}
.disposeOnDestroy()
@ -144,6 +147,10 @@ class RoomProfileActivity :
addFragmentToBackstack(R.id.simpleFragmentContainer, RoomAliasFragment::class.java, roomProfileArgs)
}
private fun openRoomPermissions() {
addFragmentToBackstack(R.id.simpleFragmentContainer, RoomPermissionsFragment::class.java, roomProfileArgs)
}
private fun openRoomMembers() {
addFragmentToBackstack(R.id.simpleFragmentContainer, RoomMemberListFragment::class.java, roomProfileArgs)
}

View file

@ -24,6 +24,7 @@ import im.vector.app.core.platform.VectorSharedAction
sealed class RoomProfileSharedAction : VectorSharedAction {
object OpenRoomSettings : RoomProfileSharedAction()
object OpenRoomAliasesSettings : RoomProfileSharedAction()
object OpenRoomPermissionsSettings : RoomProfileSharedAction()
object OpenRoomUploads : RoomProfileSharedAction()
object OpenRoomMembers : RoomProfileSharedAction()
object OpenBannedRoomMembers : RoomProfileSharedAction()

View file

@ -0,0 +1,104 @@
/*
* Copyright (c) 2021 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 im.vector.app.features.roomprofile.permissions
import androidx.annotation.StringRes
import im.vector.app.R
import org.matrix.android.sdk.api.session.events.model.EventType
/**
* Change on each permission has an effect on the power level event. Try to sort the effect by category.
*/
sealed class EditablePermission(@StringRes val labelResId: Int) {
// Updates `content.events.[eventType]`
open class EventTypeEditablePermission(val eventType: String, @StringRes labelResId: Int) : EditablePermission(labelResId)
class ModifyWidgets : EventTypeEditablePermission(
// Note: Element Web still use legacy value
EventType.STATE_ROOM_WIDGET_LEGACY,
R.string.room_permissions_modify_widgets
)
class ChangeRoomAvatar : EventTypeEditablePermission(
EventType.STATE_ROOM_AVATAR,
R.string.room_permissions_change_room_avatar
)
class ChangeMainAddressForTheRoom : EventTypeEditablePermission(
EventType.STATE_ROOM_CANONICAL_ALIAS,
R.string.room_permissions_change_main_address_for_the_room
)
class EnableRoomEncryption : EventTypeEditablePermission(
EventType.STATE_ROOM_ENCRYPTION,
R.string.room_permissions_enable_room_encryption
)
class ChangeHistoryVisibility : EventTypeEditablePermission(
EventType.STATE_ROOM_HISTORY_VISIBILITY,
R.string.room_permissions_change_history_visibility
)
class ChangeRoomName : EventTypeEditablePermission(
EventType.STATE_ROOM_NAME,
R.string.room_permissions_change_room_name
)
class ChangePermissions : EventTypeEditablePermission(
EventType.STATE_ROOM_POWER_LEVELS,
R.string.room_permissions_change_permissions
)
class SendRoomServerAclEvents : EventTypeEditablePermission(
EventType.STATE_ROOM_SERVER_ACL,
R.string.room_permissions_send_m_room_server_acl_events
)
class UpgradeTheRoom : EventTypeEditablePermission(
EventType.STATE_ROOM_TOMBSTONE,
R.string.room_permissions_upgrade_the_room
)
class ChangeTopic : EventTypeEditablePermission(
EventType.STATE_ROOM_TOPIC,
R.string.room_permissions_change_topic
)
// Updates `content.users_default`
class DefaultRole : EditablePermission(R.string.room_permissions_default_role)
// Updates `content.events_default`
class SendMessages : EditablePermission(R.string.room_permissions_send_messages)
// Updates `content.invites`
class InviteUsers : EditablePermission(R.string.room_permissions_invite_users)
// Updates `content.state_default`
class ChangeSettings : EditablePermission(R.string.room_permissions_change_settings)
// Updates `content.kick`
class KickUsers : EditablePermission(R.string.room_permissions_kick_users)
// Updates `content.ban`
class BanUsers : EditablePermission(R.string.room_permissions_ban_users)
// Updates `content.redact`
class RemoveMessagesSentByOthers : EditablePermission(R.string.room_permissions_remove_messages_sent_by_others)
// Updates `content.notification.room`
class NotifyEveryone : EditablePermission(R.string.room_permissions_notify_everyone)
}

View file

@ -0,0 +1,25 @@
/*
* Copyright 2021 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 im.vector.app.features.roomprofile.permissions
import im.vector.app.core.platform.VectorViewModelAction
sealed class RoomPermissionsAction : VectorViewModelAction {
object ToggleShowAllPermissions : RoomPermissionsAction()
data class UpdatePermission(val editablePermission: EditablePermission, val powerLevel: Int) : RoomPermissionsAction()
}

View file

@ -0,0 +1,176 @@
/*
* Copyright 2021 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 im.vector.app.features.roomprofile.permissions
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Success
import im.vector.app.R
import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.epoxy.profiles.buildProfileAction
import im.vector.app.core.epoxy.profiles.buildProfileSection
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.discovery.settingsInfoItem
import im.vector.app.features.form.formAdvancedToggleItem
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.powerlevels.Role
import javax.inject.Inject
class RoomPermissionsController @Inject constructor(
private val stringProvider: StringProvider,
colorProvider: ColorProvider
) : TypedEpoxyController<RoomPermissionsViewState>() {
interface Callback {
fun onEditPermission(editablePermission: EditablePermission, currentRole: Role)
fun toggleShowAllPermissions()
}
var callback: Callback? = null
private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color)
// Order is the order applied in the UI
// Element Web order is not really nice, try to put the settings which are more likely to be updated first
// And a second section, hidden by default
private val usefulEditablePermissions = listOf(
EditablePermission.ChangeRoomAvatar(),
EditablePermission.ChangeRoomName(),
EditablePermission.ChangeTopic()
)
private val advancedEditablePermissions = listOf(
EditablePermission.ChangeMainAddressForTheRoom(),
EditablePermission.DefaultRole(),
EditablePermission.InviteUsers(),
EditablePermission.KickUsers(),
EditablePermission.BanUsers(),
EditablePermission.SendMessages(),
EditablePermission.RemoveMessagesSentByOthers(),
EditablePermission.NotifyEveryone(),
EditablePermission.ChangeSettings(),
EditablePermission.ModifyWidgets(),
EditablePermission.ChangeHistoryVisibility(),
EditablePermission.ChangePermissions(),
EditablePermission.SendRoomServerAclEvents(),
EditablePermission.EnableRoomEncryption(),
EditablePermission.UpgradeTheRoom()
)
init {
setData(null)
}
override fun buildModels(data: RoomPermissionsViewState?) {
buildProfileSection(
stringProvider.getString(R.string.room_permissions_title)
)
when (val content = data?.currentPowerLevelsContent) {
is Success -> buildPermissions(data, content())
else -> {
loadingItem {
id("loading")
loadingText(stringProvider.getString(R.string.loading))
}
}
}
}
private fun buildPermissions(data: RoomPermissionsViewState, content: PowerLevelsContent) {
val editable = data.actionPermissions.canChangePowerLevels
settingsInfoItem {
id("notice")
helperText(stringProvider.getString(if (editable) R.string.room_permissions_notice else R.string.room_permissions_notice_read_only))
}
// Useful permissions
usefulEditablePermissions.forEach { buildPermission(it, content, editable) }
// Toggle
formAdvancedToggleItem {
id("showAdvanced")
title(stringProvider.getString(if (data.showAdvancedPermissions) R.string.hide_advanced else R.string.show_advanced))
expanded(!data.showAdvancedPermissions)
listener { callback?.toggleShowAllPermissions() }
}
// Advanced permissions
if (data.showAdvancedPermissions) {
advancedEditablePermissions.forEach { buildPermission(it, content, editable) }
}
}
private fun buildPermission(editablePermission: EditablePermission, content: PowerLevelsContent, editable: Boolean) {
val currentRole = getCurrentRole(editablePermission, content)
buildProfileAction(
id = editablePermission.labelResId.toString(),
title = stringProvider.getString(editablePermission.labelResId),
subtitle = getSubtitle(currentRole),
dividerColor = dividerColor,
divider = true,
editable = editable,
action = {
callback
?.takeIf { editable }
?.onEditPermission(editablePermission, currentRole)
}
)
}
private fun getSubtitle(currentRole: Role): String {
return when (currentRole) {
Role.Admin,
Role.Moderator,
Role.Default -> stringProvider.getString(currentRole.res)
is Role.Custom -> stringProvider.getString(currentRole.res, currentRole.value)
}
}
private fun getCurrentRole(editablePermission: EditablePermission, content: PowerLevelsContent): Role {
val value = when (editablePermission) {
is EditablePermission.EventTypeEditablePermission -> content.events[editablePermission.eventType] ?: content.stateDefault
is EditablePermission.DefaultRole -> content.usersDefault
is EditablePermission.SendMessages -> content.eventsDefault
is EditablePermission.InviteUsers -> content.invite
is EditablePermission.ChangeSettings -> content.stateDefault
is EditablePermission.KickUsers -> content.kick
is EditablePermission.BanUsers -> content.ban
is EditablePermission.RemoveMessagesSentByOthers -> content.redact
is EditablePermission.NotifyEveryone -> content.notificationLevel(PowerLevelsContent.NOTIFICATIONS_ROOM_KEY)
}
return Role.fromValue(
value,
when (editablePermission) {
is EditablePermission.EventTypeEditablePermission -> content.stateDefault
is EditablePermission.DefaultRole -> Role.Default.value
is EditablePermission.SendMessages -> Role.Default.value
is EditablePermission.InviteUsers -> Role.Moderator.value
is EditablePermission.ChangeSettings -> Role.Moderator.value
is EditablePermission.KickUsers -> Role.Moderator.value
is EditablePermission.BanUsers -> Role.Moderator.value
is EditablePermission.RemoveMessagesSentByOthers -> Role.Moderator.value
is EditablePermission.NotifyEveryone -> Role.Moderator.value
}
)
}
}

View file

@ -0,0 +1,106 @@
/*
* Copyright 2021 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 im.vector.app.features.roomprofile.permissions
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.toast
import im.vector.app.databinding.FragmentRoomSettingGenericBinding
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.roommemberprofile.powerlevel.EditPowerLevelDialogs
import im.vector.app.features.roomprofile.RoomProfileArgs
import org.matrix.android.sdk.api.session.room.powerlevels.Role
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class RoomPermissionsFragment @Inject constructor(
val viewModelFactory: RoomPermissionsViewModel.Factory,
private val controller: RoomPermissionsController,
private val avatarRenderer: AvatarRenderer
) :
VectorBaseFragment<FragmentRoomSettingGenericBinding>(),
RoomPermissionsController.Callback {
private val viewModel: RoomPermissionsViewModel by fragmentViewModel()
private val roomProfileArgs: RoomProfileArgs by args()
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRoomSettingGenericBinding {
return FragmentRoomSettingGenericBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
controller.callback = this
setupToolbar(views.roomSettingsToolbar)
views.roomSettingsRecyclerView.configureWith(controller, hasFixedSize = true)
views.waitingView.waitingStatusText.setText(R.string.please_wait)
views.waitingView.waitingStatusText.isVisible = true
viewModel.observeViewEvents {
when (it) {
is RoomPermissionsViewEvents.Failure -> showFailure(it.throwable)
RoomPermissionsViewEvents.Success -> showSuccess()
}.exhaustive
}
}
private fun showSuccess() {
activity?.toast(R.string.room_settings_save_success)
}
override fun onDestroyView() {
controller.callback = null
views.roomSettingsRecyclerView.cleanup()
super.onDestroyView()
}
override fun invalidate() = withState(viewModel) { state ->
views.waitingView.root.isVisible = state.isLoading
controller.setData(state)
renderRoomSummary(state)
}
private fun renderRoomSummary(state: RoomPermissionsViewState) {
state.roomSummary()?.let {
views.roomSettingsToolbarTitleView.text = it.displayName
avatarRenderer.render(it.toMatrixItem(), views.roomSettingsToolbarAvatarImageView)
}
}
override fun onEditPermission(editablePermission: EditablePermission, currentRole: Role) {
EditPowerLevelDialogs.showChoice(requireActivity(), editablePermission.labelResId, currentRole) { newPowerLevel ->
viewModel.handle(RoomPermissionsAction.UpdatePermission(editablePermission, newPowerLevel))
}
}
override fun toggleShowAllPermissions() {
viewModel.handle(RoomPermissionsAction.ToggleShowAllPermissions)
}
}

View file

@ -0,0 +1,28 @@
/*
* Copyright 2021 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 im.vector.app.features.roomprofile.permissions
import im.vector.app.core.platform.VectorViewEvents
/**
* Transient events for room settings screen
*/
sealed class RoomPermissionsViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : RoomPermissionsViewEvents()
object Success : RoomPermissionsViewEvents()
}

View file

@ -0,0 +1,152 @@
/*
* Copyright 2021 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 im.vector.app.features.roomprofile.permissions
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.rx.rx
import org.matrix.android.sdk.rx.unwrap
class RoomPermissionsViewModel @AssistedInject constructor(@Assisted initialState: RoomPermissionsViewState,
private val session: Session)
: VectorViewModel<RoomPermissionsViewState, RoomPermissionsAction, RoomPermissionsViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: RoomPermissionsViewState): RoomPermissionsViewModel
}
companion object : MvRxViewModelFactory<RoomPermissionsViewModel, RoomPermissionsViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: RoomPermissionsViewState): RoomPermissionsViewModel? {
val fragment: RoomPermissionsFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.viewModelFactory.create(state)
}
}
private val room = session.getRoom(initialState.roomId)!!
init {
observeRoomSummary()
observePowerLevel()
}
private fun observeRoomSummary() {
room.rx().liveRoomSummary()
.unwrap()
.execute { async ->
copy(
roomSummary = async
)
}
}
private fun observePowerLevel() {
PowerLevelsObservableFactory(room)
.createObservable()
.subscribe { powerLevelContent ->
val powerLevelsHelper = PowerLevelsHelper(powerLevelContent)
val permissions = RoomPermissionsViewState.ActionPermissions(
canChangePowerLevels = powerLevelsHelper.isUserAllowedToSend(
userId = session.myUserId,
isState = true,
eventType = EventType.STATE_ROOM_POWER_LEVELS
)
)
setState {
copy(
actionPermissions = permissions,
currentPowerLevelsContent = Success(powerLevelContent)
)
}
}
.disposeOnClear()
}
override fun handle(action: RoomPermissionsAction) {
when (action) {
is RoomPermissionsAction.UpdatePermission -> updatePermission(action)
RoomPermissionsAction.ToggleShowAllPermissions -> toggleShowAllPermissions()
}.exhaustive
}
private fun toggleShowAllPermissions() {
setState {
copy(showAdvancedPermissions = !showAdvancedPermissions)
}
}
private fun updatePermission(action: RoomPermissionsAction.UpdatePermission) {
withState { state ->
val currentPowerLevel = state.currentPowerLevelsContent.invoke() ?: return@withState
postLoading(true)
viewModelScope.launch {
try {
val newPowerLevelsContent = when (action.editablePermission) {
is EditablePermission.EventTypeEditablePermission -> currentPowerLevel.copy(
events = currentPowerLevel.events.toMutableMap().apply {
put(action.editablePermission.eventType, action.powerLevel)
}
)
is EditablePermission.DefaultRole -> currentPowerLevel.copy(usersDefault = action.powerLevel)
is EditablePermission.SendMessages -> currentPowerLevel.copy(eventsDefault = action.powerLevel)
is EditablePermission.InviteUsers -> currentPowerLevel.copy(invite = action.powerLevel)
is EditablePermission.ChangeSettings -> currentPowerLevel.copy(stateDefault = action.powerLevel)
is EditablePermission.KickUsers -> currentPowerLevel.copy(kick = action.powerLevel)
is EditablePermission.BanUsers -> currentPowerLevel.copy(ban = action.powerLevel)
is EditablePermission.RemoveMessagesSentByOthers -> currentPowerLevel.copy(redact = action.powerLevel)
is EditablePermission.NotifyEveryone -> currentPowerLevel.copy(
notifications = currentPowerLevel.notifications.toMutableMap().apply {
put(PowerLevelsContent.NOTIFICATIONS_ROOM_KEY, action.powerLevel)
}
)
}
room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, null, newPowerLevelsContent.toContent())
setState {
copy(
isLoading = false
)
}
} catch (failure: Throwable) {
postLoading(false)
_viewEvents.post(RoomPermissionsViewEvents.Failure(failure))
}
}
}
}
private fun postLoading(isLoading: Boolean) {
setState {
copy(isLoading = isLoading)
}
}
}

View file

@ -0,0 +1,40 @@
/*
* Copyright 2021 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 im.vector.app.features.roomprofile.permissions
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.app.features.roomprofile.RoomProfileArgs
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.RoomSummary
data class RoomPermissionsViewState(
val roomId: String,
val roomSummary: Async<RoomSummary> = Uninitialized,
val actionPermissions: ActionPermissions = ActionPermissions(),
val showAdvancedPermissions: Boolean = false,
val currentPowerLevelsContent: Async<PowerLevelsContent> = Uninitialized,
val isLoading: Boolean = false
) : MvRxState {
constructor(args: RoomProfileArgs) : this(roomId = args.roomId)
data class ActionPermissions(
val canChangePowerLevels: Boolean = false
)
}

View file

@ -46,6 +46,7 @@ class RoomSettingsController @Inject constructor(
fun onTopicChanged(topic: String)
fun onHistoryVisibilityClicked()
fun onRoomAliasesClicked()
fun onRoomPermissionsClicked()
fun onJoinRuleClicked()
}
@ -115,6 +116,16 @@ class RoomSettingsController @Inject constructor(
action = { callback?.onRoomAliasesClicked() }
)
buildProfileAction(
id = "permissions",
title = stringProvider.getString(R.string.room_settings_permissions_title),
subtitle = stringProvider.getString(R.string.room_settings_permissions_subtitle),
dividerColor = dividerColor,
divider = true,
editable = true,
action = { callback?.onRoomPermissionsClicked() }
)
buildProfileAction(
id = "historyReadability",
title = stringProvider.getString(R.string.room_settings_room_read_history_rules_pref_title),

View file

@ -178,6 +178,10 @@ class RoomSettingsFragment @Inject constructor(
roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomAliasesSettings)
}
override fun onRoomPermissionsClicked() {
roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomPermissionsSettings)
}
override fun onJoinRuleClicked() = withState(viewModel) { state ->
val currentJoinRule = state.newRoomJoinRules.newJoinRules ?: state.currentRoomJoinRules
val currentGuestAccess = state.newRoomJoinRules.newGuestAccess ?: state.currentGuestAccess

View file

@ -610,6 +610,33 @@
<string name="ssl_expected_existing_expl">The certificate has changed from a previously trusted one to one that is not trusted. The server may have renewed its certificate. Contact the server administrator for the expected fingerprint.</string>
<string name="ssl_only_accept">Only accept the certificate if the server administrator has published a fingerprint that matches the one above.</string>
<!-- Room Permissions -->
<string name="room_settings_permissions_title">Room permissions</string>
<string name="room_settings_permissions_subtitle">View and update the roles required to change various parts of the room.</string>
<string name="room_permissions_title">"Permissions"</string>
<string name="room_permissions_notice">"Select the roles required to change various parts of the room"</string>
<string name="room_permissions_notice_read_only">"You don't have permission to update the roles required to change various parts of the room"</string>
<string name="room_permissions_default_role">Default role</string>
<string name="room_permissions_send_messages">Send messages</string>
<string name="room_permissions_invite_users">Invite users</string>
<string name="room_permissions_change_settings">Change settings</string>
<string name="room_permissions_kick_users">Kick users</string>
<string name="room_permissions_ban_users">Ban users</string>
<string name="room_permissions_remove_messages_sent_by_others">Remove messages sent by others</string>
<string name="room_permissions_notify_everyone">Notify everyone</string>
<string name="room_permissions_modify_widgets">Modify widgets</string>
<string name="room_permissions_change_room_avatar">Change room avatar</string>
<string name="room_permissions_change_main_address_for_the_room">Change main address for the room</string>
<string name="room_permissions_enable_room_encryption">Enable room encryption</string>
<string name="room_permissions_change_history_visibility">Change history visibility</string>
<string name="room_permissions_change_room_name">Change room name</string>
<string name="room_permissions_change_permissions">Change permissions</string>
<string name="room_permissions_send_m_room_server_acl_events">Send m.room.server_acl events</string>
<string name="room_permissions_upgrade_the_room">Upgrade the room</string>
<string name="room_permissions_change_topic">Change topic</string>
<!-- Room Details -->
<string name="room_details_title">Room Details</string>
<string name="room_details_people">People</string>