diff --git a/src/settings/CMakeLists.txt b/src/settings/CMakeLists.txt index 21010a692..5ce8b9bab 100644 --- a/src/settings/CMakeLists.txt +++ b/src/settings/CMakeLists.txt @@ -51,6 +51,7 @@ ecm_add_qml_module(Settings GENERATE_PLUGIN_SOURCE RoomProfile.qml RoomAdvancedPage.qml KeyboardShortcutsPage.qml + Members.qml SOURCES colorschemer.cpp threepidaddhelper.cpp diff --git a/src/settings/Members.qml b/src/settings/Members.qml new file mode 100644 index 000000000..0198b83e0 --- /dev/null +++ b/src/settings/Members.qml @@ -0,0 +1,247 @@ +// SPDX-FileCopyrightText: 2022 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts + +import org.kde.kirigami as Kirigami +import org.kde.kirigamiaddons.formcard as FormCard +import org.kde.kirigamiaddons.delegates as Delegates +import org.kde.kirigamiaddons.labs.components as KirigamiComponents +import org.kde.kitemmodels + +import org.kde.neochat + +FormCard.FormCardPage { + id: root + + property NeoChatRoom room + + title: i18nc("@title:window", "Members") + + readonly property bool loading: permissions.count === 0 && !root.room.roomCreatorHasUltimatePowerLevel() + + readonly property PowerLevelModel powerLevelModel: PowerLevelModel { + showMute: false + } + + FormCard.FormHeader { + title: i18nc("@title", "Privileged Members") + visible: !root.loading + } + FormCard.FormCard { + visible: !root.loading + + FormCard.AbstractFormDelegate { + id: userListSearchCard + visible: root.room.canSendState("m.room.power_levels") + + contentItem: Kirigami.SearchField { + id: userListSearchField + + autoAccept: false + + Layout.fillWidth: true + + Keys.onUpPressed: userListView.decrementCurrentIndex() + Keys.onDownPressed: userListView.incrementCurrentIndex() + + onAccepted: (userListView.itemAtIndex(userListView.currentIndex) as Delegates.RoundedItemDelegate).action.trigger() + } + QQC2.Popup { + id: userListSearchPopup + + x: userListSearchField.x + y: userListSearchField.y - height + width: userListSearchField.width + height: { + let maxHeight = userListSearchField.mapToGlobal(userListSearchField.x, userListSearchField.y).y - Kirigami.Units.largeSpacing * 3; + let minHeight = Kirigami.Units.gridUnit * 2 + userListSearchPopup.padding * 2; + let filterContentHeight = userListView.contentHeight + userListSearchPopup.padding * 2; + return Math.max(Math.min(filterContentHeight, maxHeight), minHeight); + } + padding: Kirigami.Units.smallSpacing + leftPadding: Kirigami.Units.smallSpacing / 2 + rightPadding: Kirigami.Units.smallSpacing / 2 + modal: false + onClosed: userListSearchField.text = "" + + background: Kirigami.ShadowedRectangle { + property color borderColor: Kirigami.Theme.textColor + + Kirigami.Theme.colorSet: Kirigami.Theme.View + Kirigami.Theme.inherit: false + + radius: Kirigami.Units.cornerRadius + color: Kirigami.Theme.backgroundColor + + border { + color: Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 0.3) + width: 1 + } + + shadow { + xOffset: 0 + yOffset: 4 + color: Qt.rgba(0, 0, 0, 0.3) + size: 8 + } + } + + contentItem: QQC2.ScrollView { + // HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890) + QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff + + ListView { + id: userListView + clip: true + + model: UserFilterModel { + id: userListFilterModel + sourceModel: RoomManager.userListModel + filterText: userListSearchField.text + + onFilterTextChanged: { + if (filterText.length > 0 && !userListSearchPopup.visible) { + userListSearchPopup.open(); + } else if (filterText.length <= 0 && userListSearchPopup.visible) { + userListSearchPopup.close(); + } + } + } + + delegate: Delegates.RoundedItemDelegate { + id: userListItem + + required property string userId + required property url avatar + required property string name + required property int powerLevel + required property string powerLevelString + + text: name + + contentItem: RowLayout { + KirigamiComponents.Avatar { + Layout.preferredWidth: Kirigami.Units.iconSizes.medium + Layout.preferredHeight: Kirigami.Units.iconSizes.medium + source: userListItem.avatar + name: userListItem.name + } + + Delegates.SubtitleContentItem { + itemDelegate: userListItem + subtitle: userListItem.userId + labelItem.textFormat: Text.PlainText + subtitleItem.textFormat: Text.PlainText + Layout.fillWidth: true + } + + QQC2.Label { + visible: userListItem.powerLevel > 0 + + text: userListItem.powerLevelString + color: Kirigami.Theme.disabledTextColor + textFormat: Text.PlainText + wrapMode: Text.NoWrap + } + } + + onClicked: { + userListSearchPopup.close(); + (powerLevelDialog.createObject(root.QQC2.Overlay.overlay, { + room: root.room, + userId: userListItem.userId, + powerLevel: userListItem.powerLevel + }) as PowerLevelDialog).open(); + } + + Component { + id: powerLevelDialog + PowerLevelDialog {} + } + } + + QQC2.Label { + text: i18nc("@info", "No users found.") + visible: userListView.count === 0 + + anchors { + left: parent.left + leftMargin: Kirigami.Units.mediumSpacing + verticalCenter: parent.verticalCenter + } + } + } + } + } + } + FormCard.FormDelegateSeparator { + above: userListSearchCard + } + Repeater { + id: permissions + model: KSortFilterProxyModel { + sourceModel: RoomManager.userListModel + sortRoleName: "powerLevel" + sortOrder: Qt.DescendingOrder + filterRowCallback: function (source_row, source_parent) { + let powerLevelRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), UserListModel.PowerLevelRole); + return powerLevelRole != 0; + } + } + delegate: FormCard.FormTextDelegate { + id: privilegedUserDelegate + required property string userId + required property string name + required property int powerLevel + required property string powerLevelString + required property bool isCreator + + text: name + textItem.textFormat: Text.PlainText + description: userId + contentItem.children: RowLayout { + spacing: Kirigami.Units.largeSpacing + QQC2.Label { + id: powerLevelLabel + text: privilegedUserDelegate.powerLevelString + visible: (!root.room.canSendState("m.room.power_levels") || (root.room.memberEffectivePowerLevel(root.room.localMember.id) <= privilegedUserDelegate.powerLevel && privilegedUserDelegate.userId != root.room.localMember.id)) || privilegedUserDelegate.isCreator + color: Kirigami.Theme.disabledTextColor + } + QQC2.ComboBox { + focusPolicy: Qt.NoFocus // provided by parent + model: PowerLevelModel {} + textRole: "name" + valueRole: "value" + visible: !powerLevelLabel.visible + Component.onCompleted: { + let index = indexOfValue(privilegedUserDelegate.powerLevel) + if (index === -1) { + displayText = privilegedUserDelegate.powerLevelString; + } else { + currentIndex = index; + } + } + onActivated: { + root.room.setUserPowerLevel(privilegedUserDelegate.userId, currentValue); + } + } + } + } + } + } + + Item { + visible: root.loading + Layout.fillWidth: true + implicitHeight: root.height * 0.9 + Kirigami.LoadingPlaceholder { + anchors.centerIn: parent + text: i18nc("@placeholder", "Loading…") + } + } +} diff --git a/src/settings/Permissions.qml b/src/settings/Permissions.qml index 515679891..b3fdf3f1d 100644 --- a/src/settings/Permissions.qml +++ b/src/settings/Permissions.qml @@ -33,207 +33,10 @@ FormCard.FormCardPage { } FormCard.FormHeader { - title: i18nc("@title", "Privileged Users") - visible: !root.loading + title: i18nc("@title", "Power Levels") } FormCard.FormCard { - visible: !root.loading - - Repeater { - id: permissions - model: KSortFilterProxyModel { - sourceModel: RoomManager.userListModel - sortRoleName: "powerLevel" - sortOrder: Qt.DescendingOrder - filterRowCallback: function (source_row, source_parent) { - let powerLevelRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), UserListModel.PowerLevelRole); - return powerLevelRole != 0; - } - } - delegate: FormCard.FormTextDelegate { - id: privilegedUserDelegate - required property string userId - required property string name - required property int powerLevel - required property string powerLevelString - required property bool isCreator - - text: name - textItem.textFormat: Text.PlainText - description: userId - contentItem.children: RowLayout { - spacing: Kirigami.Units.largeSpacing - QQC2.Label { - id: powerLevelLabel - text: privilegedUserDelegate.powerLevelString - visible: (!root.room.canSendState("m.room.power_levels") || (root.room.memberEffectivePowerLevel(root.room.localMember.id) <= privilegedUserDelegate.powerLevel && privilegedUserDelegate.userId != root.room.localMember.id)) || privilegedUserDelegate.isCreator - color: Kirigami.Theme.disabledTextColor - } - QQC2.ComboBox { - focusPolicy: Qt.NoFocus // provided by parent - model: PowerLevelModel {} - textRole: "name" - valueRole: "value" - visible: !powerLevelLabel.visible - Component.onCompleted: { - let index = indexOfValue(privilegedUserDelegate.powerLevel) - if (index === -1) { - displayText = privilegedUserDelegate.powerLevelString; - } else { - currentIndex = index; - } - } - onActivated: { - root.room.setUserPowerLevel(privilegedUserDelegate.userId, currentValue); - } - } - } - } - } - FormCard.FormDelegateSeparator { - below: userListSearchCard - } - FormCard.AbstractFormDelegate { - id: userListSearchCard - visible: root.room.canSendState("m.room.power_levels") - - contentItem: Kirigami.SearchField { - id: userListSearchField - - autoAccept: false - - Layout.fillWidth: true - - Keys.onUpPressed: userListView.decrementCurrentIndex() - Keys.onDownPressed: userListView.incrementCurrentIndex() - - onAccepted: (userListView.itemAtIndex(userListView.currentIndex) as Delegates.RoundedItemDelegate).action.trigger() - } - QQC2.Popup { - id: userListSearchPopup - - x: userListSearchField.x - y: userListSearchField.y - height - width: userListSearchField.width - height: { - let maxHeight = userListSearchField.mapToGlobal(userListSearchField.x, userListSearchField.y).y - Kirigami.Units.largeSpacing * 3; - let minHeight = Kirigami.Units.gridUnit * 2 + userListSearchPopup.padding * 2; - let filterContentHeight = userListView.contentHeight + userListSearchPopup.padding * 2; - return Math.max(Math.min(filterContentHeight, maxHeight), minHeight); - } - padding: Kirigami.Units.smallSpacing - leftPadding: Kirigami.Units.smallSpacing / 2 - rightPadding: Kirigami.Units.smallSpacing / 2 - modal: false - onClosed: userListSearchField.text = "" - - background: Kirigami.ShadowedRectangle { - property color borderColor: Kirigami.Theme.textColor - - Kirigami.Theme.colorSet: Kirigami.Theme.View - Kirigami.Theme.inherit: false - - radius: Kirigami.Units.cornerRadius - color: Kirigami.Theme.backgroundColor - - border { - color: Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 0.3) - width: 1 - } - - shadow { - xOffset: 0 - yOffset: 4 - color: Qt.rgba(0, 0, 0, 0.3) - size: 8 - } - } - - contentItem: QQC2.ScrollView { - // HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890) - QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff - - ListView { - id: userListView - clip: true - - model: UserFilterModel { - id: userListFilterModel - sourceModel: RoomManager.userListModel - filterText: userListSearchField.text - - onFilterTextChanged: { - if (filterText.length > 0 && !userListSearchPopup.visible) { - userListSearchPopup.open(); - } else if (filterText.length <= 0 && userListSearchPopup.visible) { - userListSearchPopup.close(); - } - } - } - - delegate: Delegates.RoundedItemDelegate { - id: userListItem - - required property string userId - required property url avatar - required property string name - required property int powerLevel - required property string powerLevelString - - text: name - - contentItem: RowLayout { - KirigamiComponents.Avatar { - Layout.preferredWidth: Kirigami.Units.iconSizes.medium - Layout.preferredHeight: Kirigami.Units.iconSizes.medium - source: userListItem.avatar - name: userListItem.name - } - - Delegates.SubtitleContentItem { - itemDelegate: userListItem - subtitle: userListItem.userId - labelItem.textFormat: Text.PlainText - subtitleItem.textFormat: Text.PlainText - Layout.fillWidth: true - } - - QQC2.Label { - visible: userListItem.powerLevel > 0 - - text: userListItem.powerLevelString - color: Kirigami.Theme.disabledTextColor - textFormat: Text.PlainText - wrapMode: Text.NoWrap - } - } - - onClicked: { - userListSearchPopup.close(); - (powerLevelDialog.createObject(root.QQC2.Overlay.overlay, { - room: root.room, - userId: userListItem.userId, - powerLevel: userListItem.powerLevel - }) as PowerLevelDialog).open(); - } - - Component { - id: powerLevelDialog - PowerLevelDialog {} - } - } - } - } - } - } - } - - FormCard.FormHeader { - visible: root.room.canSendState("m.room.power_levels") - title: i18nc("@title", "Default permissions") - } - FormCard.FormCard { - visible: root.room.canSendState("m.room.power_levels") + enabled: root.room.canSendState("m.room.power_levels") Repeater { model: KSortFilterProxyModel { sourceModel: root.permissionsModel @@ -269,11 +72,49 @@ FormCard.FormCardPage { } FormCard.FormHeader { - visible: root.room.canSendState("m.room.power_levels") - title: i18nc("@title", "Basic permissions") + title: i18nc("@title", "Messages") } FormCard.FormCard { - visible: root.room.canSendState("m.room.power_levels") + enabled: root.room.canSendState("m.room.power_levels") + Repeater { + model: KSortFilterProxyModel { + sourceModel: root.permissionsModel + filterRowCallback: function (source_row, source_parent) { + return sourceModel.data(sourceModel.index(source_row, 0, source_parent), PermissionsModel.IsMessagePermissionRole); + } + } + delegate: FormCard.FormComboBoxDelegate { + required property string name + required property string subtitle + required property string type + required property int level + required property string levelName + + text: name + description: subtitle + textRole: "name" + valueRole: "value" + model: root.powerLevelModel + Component.onCompleted: { + let index = indexOfValue(level) + if (index === -1) { + displayText = levelName; + } else { + currentIndex = index; + } + } + onCurrentValueChanged: if (root.room.canSendState("m.room.power_levels")) { + root.permissionsModel.setPowerLevel(type, currentValue); + } + } + } + } + + FormCard.FormHeader { + title: i18nc("@title", "Moderation") + } + FormCard.FormCard { + enabled: root.room.canSendState("m.room.power_levels") Repeater { model: KSortFilterProxyModel { sourceModel: root.permissionsModel @@ -309,18 +150,15 @@ FormCard.FormCardPage { } FormCard.FormHeader { - visible: root.room.canSendState("m.room.power_levels") - title: i18nc("@title", "Event permissions") + title: i18nc("@title", "General") } FormCard.FormCard { - visible: root.room.canSendState("m.room.power_levels") + enabled: root.room.canSendState("m.room.power_levels") Repeater { model: KSortFilterProxyModel { sourceModel: root.permissionsModel filterRowCallback: function (source_row, source_parent) { - let isBasicPermissionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PermissionsModel.IsBasicPermissionRole); - let isDefaultValueRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PermissionsModel.IsDefaultValueRole); - return !isBasicPermissionRole && !isDefaultValueRole; + return sourceModel.data(sourceModel.index(source_row, 0, source_parent), PermissionsModel.IsGeneralPermissionRole); } } delegate: FormCard.FormComboBoxDelegate { @@ -348,7 +186,59 @@ FormCard.FormCardPage { } } } + } + + FormCard.FormHeader { + title: i18nc("@title", "Other Events") + } + FormCard.FormCard { + enabled: root.room.canSendState("m.room.power_levels") + + Repeater { + id: otherEventsRepeater + + model: KSortFilterProxyModel { + sourceModel: root.permissionsModel + filterRowCallback: function (source_row, source_parent) { + let isBasicPermissionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PermissionsModel.IsBasicPermissionRole); + let isDefaultValueRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PermissionsModel.IsDefaultValueRole); + let isMessagePermissionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PermissionsModel.IsMessagePermissionRole); + let isGeneralPermissionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PermissionsModel.IsGeneralPermissionRole); + return !isBasicPermissionRole && !isDefaultValueRole && !isMessagePermissionRole && !isGeneralPermissionRole; + } + } + delegate: FormCard.FormComboBoxDelegate { + required property string name + required property string subtitle + required property string type + required property int level + required property string levelName + + text: name + description: subtitle + textRole: "name" + valueRole: "value" + model: root.powerLevelModel + Component.onCompleted: { + let index = indexOfValue(level) + if (index === -1) { + displayText = levelName; + } else { + currentIndex = index; + } + } + onCurrentValueChanged: if (root.room.canSendState("m.room.power_levels")) { + root.permissionsModel.setPowerLevel(type, currentValue); + } + } + } + FormCard.FormDelegateSeparator { + below: addNewEventDelegate + visible: otherEventsRepeater.count > 0 + } FormCard.AbstractFormDelegate { + id: addNewEventDelegate + Layout.fillWidth: true contentItem: RowLayout { diff --git a/src/settings/RoomSettingsView.qml b/src/settings/RoomSettingsView.qml index b012a2b48..3d394f644 100644 --- a/src/settings/RoomSettingsView.qml +++ b/src/settings/RoomSettingsView.qml @@ -55,6 +55,17 @@ KirigamiSettings.ConfigurationView { }; } }, + KirigamiSettings.ConfigurationModule { + moduleId: "members" + text: i18nc("@title", "Members") + icon.name: "system-users-symbolic" + page: () => Qt.createComponent("org.kde.neochat.settings", "Members") + initialProperties: () => { + return { + room: root._room + }; + } + }, KirigamiSettings.ConfigurationModule { moduleId: "permissions" text: i18nc("@title", "Permissions") diff --git a/src/settings/models/permissionsmodel.cpp b/src/settings/models/permissionsmodel.cpp index 76ebf9231..e0944a9a8 100644 --- a/src/settings/models/permissionsmodel.cpp +++ b/src/settings/models/permissionsmodel.cpp @@ -50,12 +50,16 @@ static const QStringList knownPermissions = { u"m.room.server_acl"_s, u"m.space.child"_s, u"m.space.parent"_s, + u"org.matrix.msc3672.beacon_info"_s, + u"org.matrix.msc3381.poll.start"_s, + u"org.matrix.msc3381.poll.response"_s, + u"org.matrix.msc3381.poll.end"_s, }; // Alternate name text for default permissions. static const QHash permissionNames = { - {UsersDefaultKey, kli18nc("Room permission type", "Default user power level")}, - {StateDefaultKey, kli18nc("Room permission type", "Default power level to set the room state")}, + {UsersDefaultKey, kli18nc("Room permission type", "Default power level")}, + {StateDefaultKey, kli18nc("Room permission type", "Default power level to change room state")}, {EventsDefaultKey, kli18nc("Room permission type", "Default power level to send messages")}, {InviteKey, kli18nc("Room permission type", "Invite users")}, {KickKey, kli18nc("Room permission type", "Kick users")}, @@ -70,25 +74,58 @@ static const QHash permissionNames = { {u"m.room.topic"_s, kli18nc("Room permission type", "Change the room topic")}, {u"m.room.encryption"_s, kli18nc("Room permission type", "Enable encryption for the room")}, {u"m.room.history_visibility"_s, kli18nc("Room permission type", "Change the room history visibility")}, - {u"m.room.pinned_events"_s, kli18nc("Room permission type", "Set pinned events")}, + {u"m.room.pinned_events"_s, kli18nc("Room permission type", "Pin and unpin messages")}, {u"m.room.tombstone"_s, kli18nc("Room permission type", "Upgrade the room")}, {u"m.room.server_acl"_s, kli18nc("Room permission type", "Set the room server access control list (ACL)")}, {u"m.space.child"_s, kli18nc("Room permission type", "Set the children of this space")}, {u"m.space.parent"_s, kli18nc("Room permission type", "Set the parent space of this room")}, + {u"org.matrix.msc3672.beacon_info"_s, kli18nc("Room permission type", "Send live location updates")}, + {u"org.matrix.msc3381.poll.start"_s, kli18nc("Room permission type", "Start polls")}, + {u"org.matrix.msc3381.poll.response"_s, kli18nc("Room permission type", "Vote in polls")}, + {u"org.matrix.msc3381.poll.end"_s, kli18nc("Room permission type", "Close polls")}, }; // Subtitles for the default values. static const QHash permissionSubtitles = { - {UsersDefaultKey, kli18nc("Room permission type", "This is the power level for all new users when joining the room")}, - {StateDefaultKey, kli18nc("Room permission type", "This is used for all state events that do not have their own entry here")}, - {EventsDefaultKey, kli18nc("Room permission type", "This is used for all message events that do not have their own entry here")}, + {UsersDefaultKey, kli18nc("Room permission type", "This is the power level for all new users when joining the room.")}, + {StateDefaultKey, kli18nc("Room permission type", "This is used for all state-type events that do not have their own entry.")}, + {EventsDefaultKey, kli18nc("Room permission type", "This is used for all message-type events that do not have their own entry.")}, }; -// Permissions that should use the event default. +// Permissions that should use the message event default. static const QStringList eventPermissions = { u"m.room.message"_s, u"m.reaction"_s, u"m.room.redaction"_s, + u"org.matrix.msc3381.poll.start"_s, + u"org.matrix.msc3381.poll.response"_s, + u"org.matrix.msc3381.poll.end"_s, +}; + +// Permissions related to messaging. +static const QStringList messagingPermissions = { + u"m.reaction"_s, + u"m.room.redaction"_s, + u"org.matrix.msc3672.beacon_info"_s, + u"org.matrix.msc3381.poll.start"_s, + u"org.matrix.msc3381.poll.response"_s, + u"org.matrix.msc3381.poll.end"_s, +}; + +// Permissions related to general room management. +static const QStringList generalPermissions = { + u"m.room.power_levels"_s, + u"m.room.name"_s, + u"m.room.avatar"_s, + u"m.room.canonical_alias"_s, + u"m.room.topic"_s, + u"m.room.encryption"_s, + u"m.room.history_visibility"_s, + u"m.room.pinned_events"_s, + u"m.room.tombstone"_s, + u"m.room.server_acl"_s, + u"m.space.child"_s, + u"m.space.parent"_s, }; }; @@ -194,6 +231,12 @@ QVariant PermissionsModel::data(const QModelIndex &index, int role) const if (role == IsBasicPermissionRole) { return basicPermissions.contains(permission); } + if (role == IsMessagePermissionRole) { + return messagingPermissions.contains(permission); + } + if (role == IsGeneralPermissionRole) { + return generalPermissions.contains(permission); + } return {}; } @@ -213,6 +256,8 @@ QHash PermissionsModel::roleNames() const roles[LevelNameRole] = "levelName"; roles[IsDefaultValueRole] = "isDefaultValue"; roles[IsBasicPermissionRole] = "isBasicPermission"; + roles[IsMessagePermissionRole] = "isMessagePermission"; + roles[IsGeneralPermissionRole] = "isGeneralPermission"; return roles; } diff --git a/src/settings/models/permissionsmodel.h b/src/settings/models/permissionsmodel.h index 26a6592b7..82738cf58 100644 --- a/src/settings/models/permissionsmodel.h +++ b/src/settings/models/permissionsmodel.h @@ -36,6 +36,8 @@ public: LevelNameRole, /**< The current power level for the permission as a string. */ IsDefaultValueRole, /**< Whether the permission is a default value, e.g. for users. */ IsBasicPermissionRole, /**< Whether the permission is one of the basic ones, e.g. kick, ban, etc. */ + IsMessagePermissionRole, /** Permissions related to messaging. */ + IsGeneralPermissionRole, /** Permissions related to general room management. */ }; Q_ENUM(Roles)