mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-11-25 12:38:48 +03:00
Add basic powerlevel editor
This commit is contained in:
parent
ff6e4826e4
commit
6c6d43691d
12 changed files with 1212 additions and 12 deletions
|
@ -370,6 +370,7 @@ set(SRC_FILES
|
||||||
src/MatrixClient.cpp
|
src/MatrixClient.cpp
|
||||||
src/MemberList.cpp
|
src/MemberList.cpp
|
||||||
src/MxcImageProvider.cpp
|
src/MxcImageProvider.cpp
|
||||||
|
src/PowerlevelsEditModels.cpp
|
||||||
src/ReadReceiptsModel.cpp
|
src/ReadReceiptsModel.cpp
|
||||||
src/RegisterPage.cpp
|
src/RegisterPage.cpp
|
||||||
src/SSOHandler.cpp
|
src/SSOHandler.cpp
|
||||||
|
@ -573,6 +574,7 @@ qt5_wrap_cpp(MOC_HEADERS
|
||||||
src/TrayIcon.h
|
src/TrayIcon.h
|
||||||
src/UserSettingsPage.h
|
src/UserSettingsPage.h
|
||||||
src/UsersModel.h
|
src/UsersModel.h
|
||||||
|
src/PowerlevelsEditModels.h
|
||||||
src/RoomDirectoryModel.h
|
src/RoomDirectoryModel.h
|
||||||
src/RoomsModel.h
|
src/RoomsModel.h
|
||||||
src/ReadReceiptsModel.h
|
src/ReadReceiptsModel.h
|
||||||
|
|
|
@ -13,6 +13,7 @@ Control {
|
||||||
id: popup
|
id: popup
|
||||||
|
|
||||||
property alias currentIndex: listView.currentIndex
|
property alias currentIndex: listView.currentIndex
|
||||||
|
property string roomId
|
||||||
property string completerName
|
property string completerName
|
||||||
property var completer
|
property var completer
|
||||||
property bool bottomToTop: true
|
property bool bottomToTop: true
|
||||||
|
@ -24,6 +25,10 @@ Control {
|
||||||
property int rowSpacing: 5
|
property int rowSpacing: 5
|
||||||
property alias count: listView.count
|
property alias count: listView.count
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
console.log("RRRRRRRRRR: " + roomId);
|
||||||
|
}
|
||||||
|
|
||||||
signal completionClicked(string completion)
|
signal completionClicked(string completion)
|
||||||
signal completionSelected(string id)
|
signal completionSelected(string id)
|
||||||
|
|
||||||
|
@ -65,18 +70,22 @@ Control {
|
||||||
function finishCompletion() {
|
function finishCompletion() {
|
||||||
if (popup.completerName == "room")
|
if (popup.completerName == "room")
|
||||||
popup.completionSelected(listView.itemAtIndex(currentIndex).modelData.roomid);
|
popup.completionSelected(listView.itemAtIndex(currentIndex).modelData.roomid);
|
||||||
|
else if (popup.completerName == "user")
|
||||||
|
popup.completionSelected(listView.itemAtIndex(currentIndex).modelData.userid);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onCompleterNameChanged: {
|
function changeCompleter() {
|
||||||
if (completerName) {
|
if (completerName) {
|
||||||
completer = TimelineManager.completerFor(completerName, completerName == "room" ? "" : room.roomId);
|
completer = TimelineManager.completerFor(completerName, completerName == "room" ? "" : (popup.roomId != "" ? popup.roomId : room.roomId));
|
||||||
completer.setSearchString("");
|
completer.setSearchString("");
|
||||||
} else {
|
} else {
|
||||||
completer = undefined;
|
completer = undefined;
|
||||||
}
|
}
|
||||||
currentIndex = -1
|
currentIndex = -1
|
||||||
}
|
}
|
||||||
|
onCompleterNameChanged: changeCompleter()
|
||||||
|
onRoomIdChanged: changeCompleter()
|
||||||
|
|
||||||
bottomPadding: 1
|
bottomPadding: 1
|
||||||
leftPadding: 1
|
leftPadding: 1
|
||||||
|
@ -131,6 +140,8 @@ Control {
|
||||||
popup.completionClicked(completer.completionAt(model.index));
|
popup.completionClicked(completer.completionAt(model.index));
|
||||||
if (popup.completerName == "room")
|
if (popup.completerName == "room")
|
||||||
popup.completionSelected(model.roomid);
|
popup.completionSelected(model.roomid);
|
||||||
|
else if (popup.completerName == "user")
|
||||||
|
popup.completionSelected(model.userid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ripple {
|
Ripple {
|
||||||
|
@ -151,7 +162,7 @@ Control {
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: del
|
id: del
|
||||||
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: centerRowContent ? parent : undefined
|
||||||
spacing: rowSpacing
|
spacing: rowSpacing
|
||||||
|
|
||||||
Avatar {
|
Avatar {
|
||||||
|
@ -160,7 +171,7 @@ Control {
|
||||||
displayName: model.displayName
|
displayName: model.displayName
|
||||||
userid: model.userid
|
userid: model.userid
|
||||||
url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
|
url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
|
||||||
onClicked: popup.completionClicked(completer.completionAt(model.index))
|
enabled: false
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
|
@ -216,7 +227,7 @@ Control {
|
||||||
displayName: model.shortcode
|
displayName: model.shortcode
|
||||||
//userid: model.shortcode
|
//userid: model.shortcode
|
||||||
url: model.url.replace("mxc://", "image://MxcImage/")
|
url: model.url.replace("mxc://", "image://MxcImage/")
|
||||||
onClicked: popup.completionClicked(completer.completionAt(model.index))
|
enabled: false
|
||||||
crop: false
|
crop: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -249,10 +260,7 @@ Control {
|
||||||
displayName: model.roomName
|
displayName: model.roomName
|
||||||
roomid: model.roomid
|
roomid: model.roomid
|
||||||
url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
|
url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
|
||||||
onClicked: {
|
enabled: false
|
||||||
popup.completionClicked(completer.completionAt(model.index));
|
|
||||||
popup.completionSelected(model.roomid);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
|
@ -281,7 +289,7 @@ Control {
|
||||||
displayName: model.roomName
|
displayName: model.roomName
|
||||||
roomid: model.roomid
|
roomid: model.roomid
|
||||||
url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
|
url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
|
||||||
onClicked: popup.completionClicked(completer.completionAt(model.index))
|
enabled: false
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
|
|
|
@ -51,6 +51,22 @@ Pane {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showPLEditor(settings) {
|
||||||
|
var dialog = plEditor.createObject(timelineRoot, {
|
||||||
|
"roomSettings": settings
|
||||||
|
});
|
||||||
|
dialog.show();
|
||||||
|
destroyOnClose(dialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: plEditor
|
||||||
|
|
||||||
|
PowerLevelEditor {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: roomSettingsComponent
|
id: roomSettingsComponent
|
||||||
|
|
||||||
|
|
126
resources/qml/components/ReorderableListview.qml
Normal file
126
resources/qml/components/ReorderableListview.qml
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Nheko Contributors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
import QtQuick 2.15
|
||||||
|
import QtQml.Models 2.1
|
||||||
|
import im.nheko 1.0
|
||||||
|
import ".."
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property alias model: visualModel.model
|
||||||
|
property Component delegate
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: dragDelegate
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: dragArea
|
||||||
|
|
||||||
|
required property var model
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
enabled: model.moveable == undefined || model.moveable
|
||||||
|
|
||||||
|
property bool held: false
|
||||||
|
|
||||||
|
anchors { left: parent.left; right: parent.right }
|
||||||
|
height: content.height
|
||||||
|
|
||||||
|
drag.target: held ? content : undefined
|
||||||
|
drag.axis: Drag.YAxis
|
||||||
|
|
||||||
|
onPressAndHold: held = true
|
||||||
|
onPressed: if (mouse.source !== Qt.MouseEventNotSynthesized) { held = true }
|
||||||
|
onReleased: held = false
|
||||||
|
onHeldChanged: if (held) ListView.view.currentIndex = dragArea.index; else ListView.view.currentIndex = -1
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: content
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
horizontalCenter: parent.horizontalCenter
|
||||||
|
verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
width: dragArea.width; height: actualDelegate.implicitHeight + 4
|
||||||
|
|
||||||
|
border.width: dragArea.enabled ? 1 : 0
|
||||||
|
border.color: Nheko.colors.highlight
|
||||||
|
|
||||||
|
color: dragArea.held ? Nheko.colors.highlight : Nheko.colors.base
|
||||||
|
Behavior on color { ColorAnimation { duration: 100 } }
|
||||||
|
|
||||||
|
radius: 2
|
||||||
|
|
||||||
|
Drag.active: dragArea.held
|
||||||
|
Drag.source: dragArea
|
||||||
|
Drag.hotSpot.x: width / 2
|
||||||
|
Drag.hotSpot.y: height / 2
|
||||||
|
|
||||||
|
states: State {
|
||||||
|
when: dragArea.held
|
||||||
|
|
||||||
|
ParentChange { target: content; parent: root }
|
||||||
|
AnchorChanges {
|
||||||
|
target: content
|
||||||
|
anchors { horizontalCenter: undefined; verticalCenter: undefined }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: actualDelegate
|
||||||
|
sourceComponent: root.delegate
|
||||||
|
property var model: dragArea.model
|
||||||
|
property int index: dragArea.index
|
||||||
|
property int offset: -view.contentY + dragArea.y
|
||||||
|
anchors { fill: parent; margins: 2 }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
DropArea {
|
||||||
|
enabled: index != 0 || model.moveable == undefined || model.moveable
|
||||||
|
anchors { fill: parent; margins: 8 }
|
||||||
|
|
||||||
|
onEntered: (drag)=> {
|
||||||
|
visualModel.model.move(drag.source.index, dragArea.index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
DelegateModel {
|
||||||
|
id: visualModel
|
||||||
|
|
||||||
|
delegate: dragDelegate
|
||||||
|
}
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: view
|
||||||
|
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
anchors { fill: parent; margins: 2 }
|
||||||
|
ScrollHelper {
|
||||||
|
flickable: parent
|
||||||
|
anchors.fill: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
model: visualModel
|
||||||
|
|
||||||
|
highlightRangeMode: ListView.ApplyRange
|
||||||
|
preferredHighlightBegin: 0.2 * height
|
||||||
|
preferredHighlightEnd: 0.8 * height
|
||||||
|
|
||||||
|
spacing: 4
|
||||||
|
cacheBuffer: 50
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
347
resources/qml/dialogs/PowerLevelEditor.qml
Normal file
347
resources/qml/dialogs/PowerLevelEditor.qml
Normal file
|
@ -0,0 +1,347 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Nheko Contributors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
import ".."
|
||||||
|
import "../components"
|
||||||
|
import QtQuick 2.12
|
||||||
|
import QtQuick.Controls 2.5
|
||||||
|
import QtQuick.Layouts 1.3
|
||||||
|
import im.nheko 1.0
|
||||||
|
|
||||||
|
|
||||||
|
ApplicationWindow {
|
||||||
|
id: plEditorW
|
||||||
|
|
||||||
|
property var roomSettings
|
||||||
|
property var editingModel: Nheko.editPowerlevels(roomSettings.roomId)
|
||||||
|
|
||||||
|
modality: Qt.NonModal
|
||||||
|
flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
|
||||||
|
minimumWidth: 300
|
||||||
|
minimumHeight: 400
|
||||||
|
|
||||||
|
title: qsTr("Permissions in %1").arg(roomSettings.roomName);
|
||||||
|
|
||||||
|
// Shortcut {
|
||||||
|
// sequence: StandardKey.Cancel
|
||||||
|
// onActivated: dbb.rejected()
|
||||||
|
// }
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.margins: Nheko.paddingMedium
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
|
||||||
|
MatrixText {
|
||||||
|
text: qsTr("Be careful when editing permissions. You can't lower the permissions of people with a same or higher level than you. Be careful when promoting others.")
|
||||||
|
font.pixelSize: Math.floor(fontMetrics.font.pixelSize * 1.1)
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: false
|
||||||
|
color: Nheko.colors.text
|
||||||
|
Layout.bottomMargin: Nheko.paddingMedium
|
||||||
|
}
|
||||||
|
|
||||||
|
TabBar {
|
||||||
|
id: bar
|
||||||
|
width: parent.width
|
||||||
|
palette: Nheko.colors
|
||||||
|
|
||||||
|
component TabB : TabButton {
|
||||||
|
id: control
|
||||||
|
|
||||||
|
contentItem: Text {
|
||||||
|
text: control.text
|
||||||
|
font: control.font
|
||||||
|
opacity: enabled ? 1.0 : 0.3
|
||||||
|
color: control.down ? Nheko.colors.highlightedText : Nheko.colors.text
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
border.color: control.down ? Nheko.colors.highlight : Nheko.theme.separator
|
||||||
|
color: control.checked ? Nheko.colors.highlight : Nheko.colors.base
|
||||||
|
border.width: 1
|
||||||
|
radius: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TabB {
|
||||||
|
text: qsTr("Roles")
|
||||||
|
}
|
||||||
|
TabB {
|
||||||
|
text: qsTr("Users")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
color: Nheko.colors.alternateBase
|
||||||
|
border.width: 1
|
||||||
|
border.color: Nheko.theme.separator
|
||||||
|
|
||||||
|
StackLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Nheko.paddingMedium
|
||||||
|
currentIndex: bar.currentIndex
|
||||||
|
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: Nheko.paddingMedium
|
||||||
|
|
||||||
|
MatrixText {
|
||||||
|
text: qsTr("Move permissions between roles to change them")
|
||||||
|
font.pixelSize: Math.floor(fontMetrics.font.pixelSize * 1.1)
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: false
|
||||||
|
color: Nheko.colors.text
|
||||||
|
}
|
||||||
|
|
||||||
|
ReorderableListview {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
|
||||||
|
model: editingModel.types
|
||||||
|
|
||||||
|
delegate: RowLayout {
|
||||||
|
Column {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Text { visible: model.isType; text: model.displayName; color: Nheko.colors.text}
|
||||||
|
Text {
|
||||||
|
visible: !model.isType;
|
||||||
|
text: {
|
||||||
|
if (editingModel.adminLevel == model.powerlevel)
|
||||||
|
return qsTr("Administrator (%1)").arg(model.powerlevel)
|
||||||
|
else if (editingModel.moderatorLevel == model.powerlevel)
|
||||||
|
return qsTr("Moderator (%1)").arg(model.powerlevel)
|
||||||
|
else
|
||||||
|
return qsTr("Custom (%1)").arg(model.powerlevel)
|
||||||
|
}
|
||||||
|
color: Nheko.colors.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageButton {
|
||||||
|
Layout.alignment: Qt.AlignRight
|
||||||
|
Layout.rightMargin: 2
|
||||||
|
image: model.isType ? ":/icons/icons/ui/dismiss.svg" : ":/icons/icons/ui/add-square-button.svg"
|
||||||
|
visible: !model.isType || model.removeable
|
||||||
|
hoverEnabled: true
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: model.isType ? qsTr("Remove event type") : qsTr("Add event type")
|
||||||
|
onClicked: {
|
||||||
|
if (model.isType) {
|
||||||
|
editingModel.types.remove(index);
|
||||||
|
} else {
|
||||||
|
typeEntry.y = offset
|
||||||
|
typeEntry.visible = true
|
||||||
|
typeEntry.index = index;
|
||||||
|
typeEntry.forceActiveFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MatrixTextField {
|
||||||
|
id: typeEntry
|
||||||
|
|
||||||
|
property int index
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
z: 5
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
color: Nheko.colors.text
|
||||||
|
|
||||||
|
Keys.onPressed: {
|
||||||
|
if (typeEntry.text.includes('.') && event.matches(StandardKey.InsertParagraphSeparator)) {
|
||||||
|
editingModel.types.add(typeEntry.index, typeEntry.text)
|
||||||
|
typeEntry.visible = false;
|
||||||
|
typeEntry.clear();
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
else if (event.matches(StandardKey.Cancel)) {
|
||||||
|
typeEntry.visible = false;
|
||||||
|
typeEntry.clear();
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: Nheko.paddingMedium
|
||||||
|
|
||||||
|
MatrixText {
|
||||||
|
text: qsTr("Move users up or down to change their permissions")
|
||||||
|
font.pixelSize: Math.floor(fontMetrics.font.pixelSize * 1.1)
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: false
|
||||||
|
}
|
||||||
|
|
||||||
|
ReorderableListview {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
|
||||||
|
model: editingModel.users
|
||||||
|
|
||||||
|
Column{
|
||||||
|
id: userEntryCompleter
|
||||||
|
|
||||||
|
property int index: 0
|
||||||
|
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
spacing: 1
|
||||||
|
z: 5
|
||||||
|
MatrixTextField {
|
||||||
|
id: userEntry
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
//font.pixelSize: Math.ceil(quickSwitcher.textHeight * 0.6)
|
||||||
|
color: Nheko.colors.text
|
||||||
|
onTextEdited: {
|
||||||
|
userCompleter.completer.searchString = text;
|
||||||
|
}
|
||||||
|
Keys.onPressed: {
|
||||||
|
if (event.key == Qt.Key_Up || event.key == Qt.Key_Backtab) {
|
||||||
|
event.accepted = true;
|
||||||
|
userCompleter.up();
|
||||||
|
} else if (event.key == Qt.Key_Down || event.key == Qt.Key_Tab) {
|
||||||
|
event.accepted = true;
|
||||||
|
if (event.key == Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))
|
||||||
|
userCompleter.up();
|
||||||
|
else
|
||||||
|
userCompleter.down();
|
||||||
|
} else if (event.matches(StandardKey.InsertParagraphSeparator)) {
|
||||||
|
userCompleter.finishCompletion();
|
||||||
|
event.accepted = true;
|
||||||
|
} else if (event.matches(StandardKey.Cancel)) {
|
||||||
|
typeEntry.visible = false;
|
||||||
|
typeEntry.clear();
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Completer {
|
||||||
|
id: userCompleter
|
||||||
|
|
||||||
|
visible: userEntry.text.length > 0
|
||||||
|
width: parent.width
|
||||||
|
roomId: plEditorW.roomSettings.roomId
|
||||||
|
completerName: "user"
|
||||||
|
bottomToTop: false
|
||||||
|
fullWidth: true
|
||||||
|
avatarHeight: Nheko.avatarSize / 2
|
||||||
|
avatarWidth: Nheko.avatarSize / 2
|
||||||
|
centerRowContent: false
|
||||||
|
rowMargin: 2
|
||||||
|
rowSpacing: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onCompletionSelected(id) {
|
||||||
|
console.log("selected: " + id);
|
||||||
|
editingModel.users.add(userEntryCompleter.index, id);
|
||||||
|
userEntry.clear();
|
||||||
|
userEntryCompleter.visible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCountChanged() {
|
||||||
|
if (userCompleter.count > 0 && (userCompleter.currentIndex < 0 || userCompleter.currentIndex >= userCompleter.count))
|
||||||
|
userCompleter.currentIndex = 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
target: userCompleter
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: RowLayout {
|
||||||
|
//anchors { fill: parent; margins: 2 }
|
||||||
|
id: row
|
||||||
|
|
||||||
|
Avatar {
|
||||||
|
id: avatar
|
||||||
|
|
||||||
|
Layout.preferredHeight: Nheko.avatarSize / 2
|
||||||
|
Layout.preferredWidth: Nheko.avatarSize / 2
|
||||||
|
Layout.leftMargin: 2
|
||||||
|
userid: model.mxid
|
||||||
|
url: {
|
||||||
|
if (model.isUser)
|
||||||
|
return model.avatarUrl.replace("mxc://", "image://MxcImage/")
|
||||||
|
else if (editingModel.adminLevel >= model.powerlevel)
|
||||||
|
return "image://colorimage/:/icons/icons/ui/ribbon_star.svg?" + Nheko.colors.buttonText;
|
||||||
|
else if (editingModel.moderatorLevel >= model.powerlevel)
|
||||||
|
return "image://colorimage/:/icons/icons/ui/ribbon.svg?" + Nheko.colors.buttonText;
|
||||||
|
else
|
||||||
|
return "image://colorimage/:/icons/icons/ui/person.svg?" + Nheko.colors.buttonText;
|
||||||
|
}
|
||||||
|
displayName: model.displayName
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
Column {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Text { visible: model.isUser; text: model.displayName; color: Nheko.colors.text}
|
||||||
|
Text { visible: model.isUser; text: model.mxid; color: Nheko.colors.text}
|
||||||
|
Text {
|
||||||
|
visible: !model.isUser;
|
||||||
|
text: {
|
||||||
|
if (editingModel.adminLevel == model.powerlevel)
|
||||||
|
return qsTr("Administrator (%1)").arg(model.powerlevel)
|
||||||
|
else if (editingModel.moderatorLevel == model.powerlevel)
|
||||||
|
return qsTr("Moderator (%1)").arg(model.powerlevel)
|
||||||
|
else
|
||||||
|
return qsTr("Custom (%1)").arg(model.powerlevel)
|
||||||
|
}
|
||||||
|
color: Nheko.colors.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageButton {
|
||||||
|
Layout.alignment: Qt.AlignRight
|
||||||
|
Layout.rightMargin: 2
|
||||||
|
image: model.isUser ? ":/icons/icons/ui/dismiss.svg" : ":/icons/icons/ui/add-square-button.svg"
|
||||||
|
visible: !model.isUser || model.removeable
|
||||||
|
hoverEnabled: true
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: model.isUser ? qsTr("Remove user") : qsTr("Add user")
|
||||||
|
onClicked: {
|
||||||
|
if (model.isUser) {
|
||||||
|
editingModel.users.remove(index);
|
||||||
|
} else {
|
||||||
|
userEntryCompleter.y = offset
|
||||||
|
userEntryCompleter.visible = true
|
||||||
|
userEntryCompleter.index = index;
|
||||||
|
userEntry.forceActiveFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
footer: DialogButtonBox {
|
||||||
|
id: dbb
|
||||||
|
|
||||||
|
standardButtons: DialogButtonBox.Ok | DialogButtonBox.Cancel
|
||||||
|
onAccepted: {
|
||||||
|
editingModel.commit();
|
||||||
|
plEditorW.close();
|
||||||
|
}
|
||||||
|
onRejected: plEditorW.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -335,6 +335,18 @@ ApplicationWindow {
|
||||||
buttons: Platform.MessageDialog.Ok | Platform.MessageDialog.Cancel
|
buttons: Platform.MessageDialog.Ok | Platform.MessageDialog.Cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr("Permission")
|
||||||
|
color: Nheko.colors.text
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: qsTr("Configure")
|
||||||
|
ToolTip.text: qsTr("View and change the permissions in this room")
|
||||||
|
onClicked: timelineRoot.showPLEditor(roomSettings)
|
||||||
|
Layout.alignment: Qt.AlignRight
|
||||||
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
text: qsTr("Sticker & Emote Settings")
|
text: qsTr("Sticker & Emote Settings")
|
||||||
color: Nheko.colors.text
|
color: Nheko.colors.text
|
||||||
|
|
|
@ -127,6 +127,7 @@
|
||||||
<file>qml/components/AvatarListTile.qml</file>
|
<file>qml/components/AvatarListTile.qml</file>
|
||||||
<file>qml/components/FlatButton.qml</file>
|
<file>qml/components/FlatButton.qml</file>
|
||||||
<file>qml/components/MainWindowDialog.qml</file>
|
<file>qml/components/MainWindowDialog.qml</file>
|
||||||
|
<file>qml/components/ReorderableListview.qml</file>
|
||||||
<file>qml/components/TextButton.qml</file>
|
<file>qml/components/TextButton.qml</file>
|
||||||
<file>qml/delegates/Encrypted.qml</file>
|
<file>qml/delegates/Encrypted.qml</file>
|
||||||
<file>qml/delegates/FileMessage.qml</file>
|
<file>qml/delegates/FileMessage.qml</file>
|
||||||
|
@ -148,22 +149,23 @@
|
||||||
<file>qml/device-verification/Waiting.qml</file>
|
<file>qml/device-verification/Waiting.qml</file>
|
||||||
<file>qml/dialogs/CreateDirect.qml</file>
|
<file>qml/dialogs/CreateDirect.qml</file>
|
||||||
<file>qml/dialogs/CreateRoom.qml</file>
|
<file>qml/dialogs/CreateRoom.qml</file>
|
||||||
|
<file>qml/dialogs/HiddenEventsDialog.qml</file>
|
||||||
<file>qml/dialogs/ImageOverlay.qml</file>
|
<file>qml/dialogs/ImageOverlay.qml</file>
|
||||||
<file>qml/dialogs/ImagePackEditorDialog.qml</file>
|
<file>qml/dialogs/ImagePackEditorDialog.qml</file>
|
||||||
<file>qml/dialogs/ImagePackSettingsDialog.qml</file>
|
<file>qml/dialogs/ImagePackSettingsDialog.qml</file>
|
||||||
<file>qml/dialogs/PhoneNumberInputDialog.qml</file>
|
|
||||||
<file>qml/dialogs/InputDialog.qml</file>
|
<file>qml/dialogs/InputDialog.qml</file>
|
||||||
<file>qml/dialogs/InviteDialog.qml</file>
|
<file>qml/dialogs/InviteDialog.qml</file>
|
||||||
<file>qml/dialogs/JoinRoomDialog.qml</file>
|
<file>qml/dialogs/JoinRoomDialog.qml</file>
|
||||||
<file>qml/dialogs/LeaveRoomDialog.qml</file>
|
<file>qml/dialogs/LeaveRoomDialog.qml</file>
|
||||||
<file>qml/dialogs/LogoutDialog.qml</file>
|
<file>qml/dialogs/LogoutDialog.qml</file>
|
||||||
|
<file>qml/dialogs/PhoneNumberInputDialog.qml</file>
|
||||||
|
<file>qml/dialogs/PowerLevelEditor.qml</file>
|
||||||
<file>qml/dialogs/RawMessageDialog.qml</file>
|
<file>qml/dialogs/RawMessageDialog.qml</file>
|
||||||
<file>qml/dialogs/ReadReceipts.qml</file>
|
<file>qml/dialogs/ReadReceipts.qml</file>
|
||||||
<file>qml/dialogs/RoomDirectory.qml</file>
|
<file>qml/dialogs/RoomDirectory.qml</file>
|
||||||
<file>qml/dialogs/RoomMembers.qml</file>
|
<file>qml/dialogs/RoomMembers.qml</file>
|
||||||
<file>qml/dialogs/RoomSettings.qml</file>
|
<file>qml/dialogs/RoomSettings.qml</file>
|
||||||
<file>qml/dialogs/UserProfile.qml</file>
|
<file>qml/dialogs/UserProfile.qml</file>
|
||||||
<file>qml/dialogs/HiddenEventsDialog.qml</file>
|
|
||||||
<file>qml/emoji/EmojiPicker.qml</file>
|
<file>qml/emoji/EmojiPicker.qml</file>
|
||||||
<file>qml/emoji/StickerPicker.qml</file>
|
<file>qml/emoji/StickerPicker.qml</file>
|
||||||
<file>qml/ui/NhekoSlider.qml</file>
|
<file>qml/ui/NhekoSlider.qml</file>
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
#include "MatrixClient.h"
|
#include "MatrixClient.h"
|
||||||
#include "MemberList.h"
|
#include "MemberList.h"
|
||||||
#include "MxcImageProvider.h"
|
#include "MxcImageProvider.h"
|
||||||
|
#include "PowerlevelsEditModels.h"
|
||||||
#include "ReadReceiptsModel.h"
|
#include "ReadReceiptsModel.h"
|
||||||
#include "RegisterPage.h"
|
#include "RegisterPage.h"
|
||||||
#include "RoomDirectoryModel.h"
|
#include "RoomDirectoryModel.h"
|
||||||
|
@ -174,6 +175,12 @@ MainWindow::registerQmlTypes()
|
||||||
qmlRegisterType<LoginPage>("im.nheko", 1, 0, "Login");
|
qmlRegisterType<LoginPage>("im.nheko", 1, 0, "Login");
|
||||||
qmlRegisterType<RegisterPage>("im.nheko", 1, 0, "Registration");
|
qmlRegisterType<RegisterPage>("im.nheko", 1, 0, "Registration");
|
||||||
qmlRegisterType<HiddenEvents>("im.nheko", 1, 0, "HiddenEvents");
|
qmlRegisterType<HiddenEvents>("im.nheko", 1, 0, "HiddenEvents");
|
||||||
|
qmlRegisterUncreatableType<PowerlevelEditingModels>(
|
||||||
|
"im.nheko",
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
"PowerlevelEditingModels",
|
||||||
|
QStringLiteral("Please use editPowerlevels to create the models"));
|
||||||
qmlRegisterUncreatableType<DeviceVerificationFlow>(
|
qmlRegisterUncreatableType<DeviceVerificationFlow>(
|
||||||
"im.nheko",
|
"im.nheko",
|
||||||
1,
|
1,
|
||||||
|
|
534
src/PowerlevelsEditModels.cpp
Normal file
534
src/PowerlevelsEditModels.cpp
Normal file
|
@ -0,0 +1,534 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Nheko Contributors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "PowerlevelsEditModels.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
|
#include "Cache.h"
|
||||||
|
#include "Cache_p.h"
|
||||||
|
#include "ChatPage.h"
|
||||||
|
#include "Logging.h"
|
||||||
|
#include "MatrixClient.h"
|
||||||
|
|
||||||
|
PowerlevelsTypeListModel::PowerlevelsTypeListModel(const std::string &rid,
|
||||||
|
const mtx::events::state::PowerLevels &pl,
|
||||||
|
QObject *parent)
|
||||||
|
: QAbstractListModel(parent)
|
||||||
|
, room_id(rid)
|
||||||
|
, powerLevels_(pl)
|
||||||
|
{
|
||||||
|
std::set<mtx::events::state::power_level_t> seen_levels;
|
||||||
|
for (const auto &[type, level] : powerLevels_.events) {
|
||||||
|
if (!seen_levels.count(level)) {
|
||||||
|
types.push_back(Entry{"", level});
|
||||||
|
seen_levels.insert(level);
|
||||||
|
}
|
||||||
|
types.push_back(Entry{type, level});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto &[user, level] : powerLevels_.users) {
|
||||||
|
(void)user;
|
||||||
|
if (!seen_levels.count(level)) {
|
||||||
|
types.push_back(Entry{"", level});
|
||||||
|
seen_levels.insert(level);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto &level : {
|
||||||
|
powerLevels_.events_default,
|
||||||
|
powerLevels_.state_default,
|
||||||
|
powerLevels_.users_default,
|
||||||
|
powerLevels_.ban,
|
||||||
|
powerLevels_.kick,
|
||||||
|
powerLevels_.invite,
|
||||||
|
powerLevels_.redact,
|
||||||
|
}) {
|
||||||
|
if (!seen_levels.count(level)) {
|
||||||
|
types.push_back(Entry{"", level});
|
||||||
|
seen_levels.insert(level);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
types.push_back(Entry{"zdefault_states", powerLevels_.state_default});
|
||||||
|
types.push_back(Entry{"zdefault_events", powerLevels_.events_default});
|
||||||
|
types.push_back(Entry{"ban", powerLevels_.ban});
|
||||||
|
types.push_back(Entry{"kick", powerLevels_.kick});
|
||||||
|
types.push_back(Entry{"invite", powerLevels_.invite});
|
||||||
|
types.push_back(Entry{"redact", powerLevels_.redact});
|
||||||
|
|
||||||
|
std::sort(types.begin(), types.end(), [](const Entry &a, const Entry &b) {
|
||||||
|
if (a.pl != b.pl) // sort by PL
|
||||||
|
return a.pl > b.pl;
|
||||||
|
else if (a.type.empty() != b.type.empty()) // empty types are headers
|
||||||
|
return a.type.empty() > b.type.empty();
|
||||||
|
else {
|
||||||
|
bool a_contains_dot = a.type.find('.') != std::string::npos;
|
||||||
|
bool b_contains_dot = b.type.find('.') != std::string::npos;
|
||||||
|
if (a_contains_dot != b_contains_dot) // sort stuff like "invite" or "default" last
|
||||||
|
return a_contains_dot > b_contains_dot;
|
||||||
|
else // rest is sorted alphabetical
|
||||||
|
return a.type < b.type;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::map<std::string, mtx::events::state::power_level_t, std::less<>>
|
||||||
|
PowerlevelsTypeListModel::toEvents()
|
||||||
|
{
|
||||||
|
std::map<std::string, mtx::events::state::power_level_t, std::less<>> m;
|
||||||
|
for (const auto &[key, pl] : types)
|
||||||
|
if (key.find('.') != std::string::npos)
|
||||||
|
m[key] = pl;
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
mtx::events::state::power_level_t
|
||||||
|
PowerlevelsTypeListModel::kick()
|
||||||
|
{
|
||||||
|
for (const auto &[key, pl] : types)
|
||||||
|
if (key == "kick")
|
||||||
|
return pl;
|
||||||
|
return powerLevels_.users_default;
|
||||||
|
}
|
||||||
|
mtx::events::state::power_level_t
|
||||||
|
PowerlevelsTypeListModel::invite()
|
||||||
|
{
|
||||||
|
for (const auto &[key, pl] : types)
|
||||||
|
if (key == "invite")
|
||||||
|
return pl;
|
||||||
|
return powerLevels_.users_default;
|
||||||
|
}
|
||||||
|
mtx::events::state::power_level_t
|
||||||
|
PowerlevelsTypeListModel::ban()
|
||||||
|
{
|
||||||
|
for (const auto &[key, pl] : types)
|
||||||
|
if (key == "ban")
|
||||||
|
return pl;
|
||||||
|
return powerLevels_.users_default;
|
||||||
|
}
|
||||||
|
mtx::events::state::power_level_t
|
||||||
|
PowerlevelsTypeListModel::eventsDefault()
|
||||||
|
{
|
||||||
|
for (const auto &[key, pl] : types)
|
||||||
|
if (key == "zdefault_events")
|
||||||
|
return pl;
|
||||||
|
return powerLevels_.users_default;
|
||||||
|
}
|
||||||
|
mtx::events::state::power_level_t
|
||||||
|
PowerlevelsTypeListModel::stateDefault()
|
||||||
|
{
|
||||||
|
for (const auto &[key, pl] : types)
|
||||||
|
if (key == "zdefault_states")
|
||||||
|
return pl;
|
||||||
|
return powerLevels_.users_default;
|
||||||
|
}
|
||||||
|
|
||||||
|
QHash<int, QByteArray>
|
||||||
|
PowerlevelsTypeListModel::roleNames() const
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
{DisplayName, "displayName"},
|
||||||
|
{Powerlevel, "powerlevel"},
|
||||||
|
{IsType, "isType"},
|
||||||
|
{Moveable, "moveable"},
|
||||||
|
{Removeable, "removeable"},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant
|
||||||
|
PowerlevelsTypeListModel::data(const QModelIndex &index, int role) const
|
||||||
|
{
|
||||||
|
if (!index.isValid() || index.row() >= types.size())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
const auto &type = types.at(index.row());
|
||||||
|
|
||||||
|
switch (static_cast<Roles>(role)) {
|
||||||
|
case DisplayName:
|
||||||
|
if (type.type == "zdefault_events")
|
||||||
|
return tr("Other events");
|
||||||
|
else if (type.type == "zdefault_states")
|
||||||
|
return tr("Other state events");
|
||||||
|
else if (type.type == "kick")
|
||||||
|
return tr("Remove other users");
|
||||||
|
else if (type.type == "ban")
|
||||||
|
return tr("Ban other users");
|
||||||
|
else if (type.type == "invite")
|
||||||
|
return tr("Invite other users");
|
||||||
|
else if (type.type == "redact")
|
||||||
|
return tr("Redact events sent by others");
|
||||||
|
else if (type.type == "m.reaction")
|
||||||
|
return tr("Reactions");
|
||||||
|
else if (type.type == "m.room.aliases")
|
||||||
|
return tr("Deprecated aliases events");
|
||||||
|
else if (type.type == "m.room.avatar")
|
||||||
|
return tr("Change the room avatar");
|
||||||
|
else if (type.type == "m.room.canonical_alias")
|
||||||
|
return tr("Change the room addresses");
|
||||||
|
else if (type.type == "m.room.encrypted")
|
||||||
|
return tr("Send encrypted messages");
|
||||||
|
else if (type.type == "m.room.encryption")
|
||||||
|
return tr("Enable encryption");
|
||||||
|
else if (type.type == "m.room.guest_access")
|
||||||
|
return tr("Change guest access");
|
||||||
|
else if (type.type == "m.room.history_visibility")
|
||||||
|
return tr("Change history visibility");
|
||||||
|
else if (type.type == "m.room.join_rules")
|
||||||
|
return tr("Change who can join");
|
||||||
|
else if (type.type == "m.room.message")
|
||||||
|
return tr("Send messages");
|
||||||
|
else if (type.type == "m.room.name")
|
||||||
|
return tr("Change the room name");
|
||||||
|
else if (type.type == "m.room.power_levels")
|
||||||
|
return tr("Change the room permissions");
|
||||||
|
else if (type.type == "m.room.topic")
|
||||||
|
return tr("Change the rooms topic");
|
||||||
|
else if (type.type == "m.widget")
|
||||||
|
return tr("Change the widgets");
|
||||||
|
else if (type.type == "im.vector.modular.widgets")
|
||||||
|
return tr("Change the widgets (experimental)");
|
||||||
|
else if (type.type == "m.room.redaction")
|
||||||
|
return tr("Redact own events");
|
||||||
|
else if (type.type == "m.room.pinned_events")
|
||||||
|
return tr("Change the pinned events");
|
||||||
|
else if (type.type == "m.room.tombstone")
|
||||||
|
return tr("Upgrade the room");
|
||||||
|
else if (type.type == "m.sticker")
|
||||||
|
return tr("Send stickers");
|
||||||
|
|
||||||
|
else if (type.type == "m.space.child")
|
||||||
|
return tr("Edit child rooms");
|
||||||
|
else if (type.type == "m.space.parent")
|
||||||
|
return tr("Change parent spaces");
|
||||||
|
|
||||||
|
else if (type.type == "m.call.invite")
|
||||||
|
return tr("Start a call");
|
||||||
|
else if (type.type == "m.call.candidates")
|
||||||
|
return tr("Negotiate a call");
|
||||||
|
else if (type.type == "m.call.answer")
|
||||||
|
return tr("Answer a call");
|
||||||
|
else if (type.type == "m.call.hangup")
|
||||||
|
return tr("Hang up a call");
|
||||||
|
else if (type.type == "im.ponies.room_emotes")
|
||||||
|
return tr("Change the room emotes");
|
||||||
|
return QString::fromStdString(type.type);
|
||||||
|
case Powerlevel:
|
||||||
|
return static_cast<qlonglong>(type.pl);
|
||||||
|
case IsType:
|
||||||
|
return !type.type.empty();
|
||||||
|
case Moveable:
|
||||||
|
return !type.type.empty();
|
||||||
|
case Removeable:
|
||||||
|
return !type.type.empty() && type.type.find('.') != std::string::npos;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
PowerlevelsTypeListModel::remove(int row)
|
||||||
|
{
|
||||||
|
if (row < 0 || row >= types.size() || types.at(row).type.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
beginRemoveRows(QModelIndex(), row, row);
|
||||||
|
types.remove(row);
|
||||||
|
endRemoveRows();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
void
|
||||||
|
PowerlevelsTypeListModel::add(int row, QString type)
|
||||||
|
{
|
||||||
|
if (row < 0 || row > types.size())
|
||||||
|
return;
|
||||||
|
|
||||||
|
const auto typeStr = type.toStdString();
|
||||||
|
for (int i = 0; i < types.size(); i++) {
|
||||||
|
if (types[i].type == typeStr) {
|
||||||
|
if (i > row)
|
||||||
|
move(i, row + 1);
|
||||||
|
else
|
||||||
|
move(i, row);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beginInsertRows(QModelIndex(), row + 1, row + 1);
|
||||||
|
types.insert(row + 1, Entry{type.toStdString(), types.at(row).pl});
|
||||||
|
endInsertRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
PowerlevelsTypeListModel::move(int from, int to)
|
||||||
|
{
|
||||||
|
if (from == to)
|
||||||
|
return false;
|
||||||
|
if (from < to)
|
||||||
|
to += 1;
|
||||||
|
|
||||||
|
beginMoveRows(QModelIndex(), from, from, QModelIndex(), to);
|
||||||
|
auto ret = moveRow(QModelIndex(), from, QModelIndex(), to);
|
||||||
|
endMoveRows();
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
PowerlevelsTypeListModel::moveRows(const QModelIndex &,
|
||||||
|
int sourceRow,
|
||||||
|
int count,
|
||||||
|
const QModelIndex &,
|
||||||
|
int destinationChild)
|
||||||
|
{
|
||||||
|
if (sourceRow == destinationChild)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (count != 1)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (sourceRow < 0 || sourceRow >= types.size())
|
||||||
|
return false;
|
||||||
|
if (destinationChild < 0 || destinationChild > types.size())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (types.at(sourceRow).type.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto pl = types.at(destinationChild > 0 ? destinationChild - 1 : 0).pl;
|
||||||
|
auto sourceItem = types.takeAt(sourceRow);
|
||||||
|
sourceItem.pl = pl;
|
||||||
|
if (destinationChild < sourceRow)
|
||||||
|
types.insert(destinationChild, std::move(sourceItem));
|
||||||
|
else
|
||||||
|
types.insert(destinationChild - 1, std::move(sourceItem));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
PowerlevelsUserListModel::PowerlevelsUserListModel(const std::string &rid,
|
||||||
|
const mtx::events::state::PowerLevels &pl,
|
||||||
|
QObject *parent)
|
||||||
|
: QAbstractListModel(parent)
|
||||||
|
, room_id(rid)
|
||||||
|
, powerLevels_(pl)
|
||||||
|
{
|
||||||
|
std::set<mtx::events::state::power_level_t> seen_levels;
|
||||||
|
for (const auto &[user, level] : powerLevels_.users) {
|
||||||
|
if (!seen_levels.count(level)) {
|
||||||
|
users.push_back(Entry{"", level});
|
||||||
|
seen_levels.insert(level);
|
||||||
|
}
|
||||||
|
users.push_back(Entry{user, level});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto &[type, level] : powerLevels_.events) {
|
||||||
|
(void)type;
|
||||||
|
if (!seen_levels.count(level)) {
|
||||||
|
users.push_back(Entry{"", level});
|
||||||
|
seen_levels.insert(level);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto &level : {
|
||||||
|
powerLevels_.events_default,
|
||||||
|
powerLevels_.state_default,
|
||||||
|
powerLevels_.users_default,
|
||||||
|
powerLevels_.ban,
|
||||||
|
powerLevels_.kick,
|
||||||
|
powerLevels_.invite,
|
||||||
|
powerLevels_.redact,
|
||||||
|
}) {
|
||||||
|
if (!seen_levels.count(level)) {
|
||||||
|
users.push_back(Entry{"", level});
|
||||||
|
seen_levels.insert(level);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
users.push_back(Entry{"default", powerLevels_.users_default});
|
||||||
|
|
||||||
|
std::sort(users.begin(), users.end(), [](const Entry &a, const Entry &b) {
|
||||||
|
if (a.pl != b.pl)
|
||||||
|
return a.pl > b.pl;
|
||||||
|
else
|
||||||
|
return a.mxid < b.mxid;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::map<std::string, mtx::events::state::power_level_t, std::less<>>
|
||||||
|
PowerlevelsUserListModel::toUsers()
|
||||||
|
{
|
||||||
|
std::map<std::string, mtx::events::state::power_level_t, std::less<>> m;
|
||||||
|
for (const auto &[key, pl] : users)
|
||||||
|
if (key.size() > 0 && key.at(0) == '@')
|
||||||
|
m[key] = pl;
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
mtx::events::state::power_level_t
|
||||||
|
PowerlevelsUserListModel::usersDefault()
|
||||||
|
{
|
||||||
|
for (const auto &[key, pl] : users)
|
||||||
|
if (key == "default")
|
||||||
|
return pl;
|
||||||
|
return powerLevels_.users_default;
|
||||||
|
}
|
||||||
|
|
||||||
|
QHash<int, QByteArray>
|
||||||
|
PowerlevelsUserListModel::roleNames() const
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
{Mxid, "mxid"},
|
||||||
|
{DisplayName, "displayName"},
|
||||||
|
{AvatarUrl, "avatarUrl"},
|
||||||
|
{Powerlevel, "powerlevel"},
|
||||||
|
{IsUser, "isUser"},
|
||||||
|
{Moveable, "moveable"},
|
||||||
|
{Removeable, "removeable"},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant
|
||||||
|
PowerlevelsUserListModel::data(const QModelIndex &index, int role) const
|
||||||
|
{
|
||||||
|
if (!index.isValid() || index.row() >= users.size())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
const auto &user = users.at(index.row());
|
||||||
|
|
||||||
|
switch (static_cast<Roles>(role)) {
|
||||||
|
case Mxid:
|
||||||
|
if ("default" == user.mxid)
|
||||||
|
return QStringLiteral("*");
|
||||||
|
return QString::fromStdString(user.mxid);
|
||||||
|
case DisplayName:
|
||||||
|
if (user.mxid == "default")
|
||||||
|
return tr("Other users");
|
||||||
|
return QString::fromStdString(cache::displayName(room_id, user.mxid));
|
||||||
|
case AvatarUrl:
|
||||||
|
return cache::avatarUrl(QString::fromStdString(room_id), QString::fromStdString(user.mxid));
|
||||||
|
case Powerlevel:
|
||||||
|
return static_cast<qlonglong>(user.pl);
|
||||||
|
case IsUser:
|
||||||
|
return !user.mxid.empty();
|
||||||
|
case Moveable:
|
||||||
|
return !user.mxid.empty();
|
||||||
|
case Removeable:
|
||||||
|
return !user.mxid.empty() && user.mxid.find('.') != std::string::npos;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
PowerlevelsUserListModel::remove(int row)
|
||||||
|
{
|
||||||
|
if (row < 0 || row >= users.size() || users.at(row).mxid.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
beginRemoveRows(QModelIndex(), row, row);
|
||||||
|
users.remove(row);
|
||||||
|
endRemoveRows();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
PowerlevelsUserListModel::add(int row, QString user)
|
||||||
|
{
|
||||||
|
if (row < 0 || row > users.size())
|
||||||
|
return;
|
||||||
|
|
||||||
|
const auto userStr = user.toStdString();
|
||||||
|
for (int i = 0; i < users.size(); i++) {
|
||||||
|
if (users[i].mxid == userStr) {
|
||||||
|
if (i > row)
|
||||||
|
move(i, row + 1);
|
||||||
|
else
|
||||||
|
move(i, row);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beginInsertRows(QModelIndex(), row + 1, row + 1);
|
||||||
|
users.insert(row + 1, Entry{user.toStdString(), users.at(row).pl});
|
||||||
|
endInsertRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
PowerlevelsUserListModel::move(int from, int to)
|
||||||
|
{
|
||||||
|
if (from == to)
|
||||||
|
return false;
|
||||||
|
if (from < to)
|
||||||
|
to += 1;
|
||||||
|
|
||||||
|
beginMoveRows(QModelIndex(), from, from, QModelIndex(), to);
|
||||||
|
auto ret = moveRow(QModelIndex(), from, QModelIndex(), to);
|
||||||
|
endMoveRows();
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
PowerlevelsUserListModel::moveRows(const QModelIndex &,
|
||||||
|
int sourceRow,
|
||||||
|
int count,
|
||||||
|
const QModelIndex &,
|
||||||
|
int destinationChild)
|
||||||
|
{
|
||||||
|
if (sourceRow == destinationChild)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (count != 1)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (sourceRow < 0 || sourceRow >= users.size())
|
||||||
|
return false;
|
||||||
|
if (destinationChild < 0 || destinationChild > users.size())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (users.at(sourceRow).mxid.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto pl = users.at(destinationChild > 0 ? destinationChild - 1 : 0).pl;
|
||||||
|
auto sourceItem = users.takeAt(sourceRow);
|
||||||
|
sourceItem.pl = pl;
|
||||||
|
if (destinationChild < sourceRow)
|
||||||
|
users.insert(destinationChild, std::move(sourceItem));
|
||||||
|
else
|
||||||
|
users.insert(destinationChild - 1, std::move(sourceItem));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
PowerlevelEditingModels::PowerlevelEditingModels(QString room_id, QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, powerLevels_(cache::client()
|
||||||
|
->getStateEvent<mtx::events::state::PowerLevels>(room_id.toStdString())
|
||||||
|
.value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{})
|
||||||
|
.content)
|
||||||
|
, types_(room_id.toStdString(), powerLevels_, this)
|
||||||
|
, users_(room_id.toStdString(), powerLevels_, this)
|
||||||
|
, room_id_(room_id.toStdString())
|
||||||
|
{}
|
||||||
|
|
||||||
|
void
|
||||||
|
PowerlevelEditingModels::commit()
|
||||||
|
{
|
||||||
|
powerLevels_.events = types_.toEvents();
|
||||||
|
powerLevels_.kick = types_.kick();
|
||||||
|
powerLevels_.invite = types_.invite();
|
||||||
|
powerLevels_.ban = types_.ban();
|
||||||
|
powerLevels_.events_default = types_.eventsDefault();
|
||||||
|
powerLevels_.state_default = types_.stateDefault();
|
||||||
|
powerLevels_.users = users_.toUsers();
|
||||||
|
powerLevels_.users_default = users_.usersDefault();
|
||||||
|
|
||||||
|
http::client()->send_state_event(
|
||||||
|
room_id_, powerLevels_, [](const mtx::responses::EventId &, mtx::http::RequestErr e) {
|
||||||
|
if (e) {
|
||||||
|
nhlog::net()->error("Failed to send PL event: {}", *e);
|
||||||
|
ChatPage::instance()->showNotification(
|
||||||
|
tr("Failed to update powerlevel: %1")
|
||||||
|
.arg(QString::fromStdString(e->matrix_error.error)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
140
src/PowerlevelsEditModels.h
Normal file
140
src/PowerlevelsEditModels.h
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Nheko Contributors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QAbstractListModel>
|
||||||
|
#include <QSortFilterProxyModel>
|
||||||
|
|
||||||
|
#include <mtx/events/power_levels.hpp>
|
||||||
|
|
||||||
|
#include "CacheStructs.h"
|
||||||
|
|
||||||
|
class PowerlevelsTypeListModel : public QAbstractListModel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum Roles
|
||||||
|
{
|
||||||
|
DisplayName,
|
||||||
|
Powerlevel,
|
||||||
|
IsType,
|
||||||
|
Moveable,
|
||||||
|
Removeable,
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit PowerlevelsTypeListModel(const std::string &room_id_,
|
||||||
|
const mtx::events::state::PowerLevels &pl,
|
||||||
|
QObject *parent = nullptr);
|
||||||
|
|
||||||
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
|
int rowCount(const QModelIndex &) const override { return static_cast<int>(types.size()); }
|
||||||
|
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||||
|
|
||||||
|
Q_INVOKABLE bool remove(int row);
|
||||||
|
Q_INVOKABLE bool move(int from, int to);
|
||||||
|
Q_INVOKABLE void add(int index, QString type);
|
||||||
|
|
||||||
|
bool moveRows(const QModelIndex &sourceParent,
|
||||||
|
int sourceRow,
|
||||||
|
int count,
|
||||||
|
const QModelIndex &destinationParent,
|
||||||
|
int destinationChild) override;
|
||||||
|
|
||||||
|
std::map<std::string, mtx::events::state::power_level_t, std::less<>> toEvents();
|
||||||
|
mtx::events::state::power_level_t kick();
|
||||||
|
mtx::events::state::power_level_t invite();
|
||||||
|
mtx::events::state::power_level_t ban();
|
||||||
|
mtx::events::state::power_level_t eventsDefault();
|
||||||
|
mtx::events::state::power_level_t stateDefault();
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Entry
|
||||||
|
{
|
||||||
|
std::string type;
|
||||||
|
mtx::events::state::power_level_t pl;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string room_id;
|
||||||
|
QVector<Entry> types;
|
||||||
|
mtx::events::state::PowerLevels powerLevels_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class PowerlevelsUserListModel : public QAbstractListModel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum Roles
|
||||||
|
{
|
||||||
|
Mxid,
|
||||||
|
DisplayName,
|
||||||
|
AvatarUrl,
|
||||||
|
Powerlevel,
|
||||||
|
IsUser,
|
||||||
|
Moveable,
|
||||||
|
Removeable,
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit PowerlevelsUserListModel(const std::string &room_id_,
|
||||||
|
const mtx::events::state::PowerLevels &pl,
|
||||||
|
QObject *parent = nullptr);
|
||||||
|
|
||||||
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
|
int rowCount(const QModelIndex &) const override { return static_cast<int>(users.size()); }
|
||||||
|
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||||
|
|
||||||
|
Q_INVOKABLE bool remove(int row);
|
||||||
|
Q_INVOKABLE bool move(int from, int to);
|
||||||
|
Q_INVOKABLE void add(int index, QString user);
|
||||||
|
|
||||||
|
bool moveRows(const QModelIndex &sourceParent,
|
||||||
|
int sourceRow,
|
||||||
|
int count,
|
||||||
|
const QModelIndex &destinationParent,
|
||||||
|
int destinationChild) override;
|
||||||
|
|
||||||
|
std::map<std::string, mtx::events::state::power_level_t, std::less<>> toUsers();
|
||||||
|
mtx::events::state::power_level_t usersDefault();
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Entry
|
||||||
|
{
|
||||||
|
std::string mxid;
|
||||||
|
mtx::events::state::power_level_t pl;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string room_id;
|
||||||
|
QVector<Entry> users;
|
||||||
|
mtx::events::state::PowerLevels powerLevels_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class PowerlevelEditingModels : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
Q_PROPERTY(PowerlevelsUserListModel *users READ users CONSTANT)
|
||||||
|
Q_PROPERTY(PowerlevelsTypeListModel *types READ types CONSTANT)
|
||||||
|
Q_PROPERTY(qlonglong adminLevel READ adminLevel CONSTANT)
|
||||||
|
Q_PROPERTY(qlonglong moderatorLevel READ moderatorLevel CONSTANT)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit PowerlevelEditingModels(QString room_id, QObject *parent = nullptr);
|
||||||
|
|
||||||
|
PowerlevelsUserListModel *users() { return &users_; }
|
||||||
|
PowerlevelsTypeListModel *types() { return &types_; }
|
||||||
|
qlonglong adminLevel() const
|
||||||
|
{
|
||||||
|
return powerLevels_.state_level(to_string(mtx::events::EventType::RoomPowerLevels));
|
||||||
|
}
|
||||||
|
qlonglong moderatorLevel() const { return powerLevels_.redact; }
|
||||||
|
|
||||||
|
Q_INVOKABLE void commit();
|
||||||
|
|
||||||
|
mtx::events::state::PowerLevels powerLevels_;
|
||||||
|
PowerlevelsTypeListModel types_;
|
||||||
|
PowerlevelsUserListModel users_;
|
||||||
|
std::string room_id_;
|
||||||
|
};
|
|
@ -282,6 +282,7 @@ public:
|
||||||
Q_INVOKABLE bool saveMedia(const QString &eventId) const;
|
Q_INVOKABLE bool saveMedia(const QString &eventId) const;
|
||||||
Q_INVOKABLE void showEvent(QString eventId);
|
Q_INVOKABLE void showEvent(QString eventId);
|
||||||
Q_INVOKABLE void copyLinkToEvent(const QString &eventId) const;
|
Q_INVOKABLE void copyLinkToEvent(const QString &eventId) const;
|
||||||
|
|
||||||
void
|
void
|
||||||
cacheMedia(const QString &eventId, const std::function<void(const QString filename)> &callback);
|
cacheMedia(const QString &eventId, const std::function<void(const QString filename)> &callback);
|
||||||
Q_INVOKABLE void sendReset()
|
Q_INVOKABLE void sendReset()
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QPalette>
|
#include <QPalette>
|
||||||
|
|
||||||
|
#include "PowerlevelsEditModels.h"
|
||||||
#include "Theme.h"
|
#include "Theme.h"
|
||||||
#include "UserProfile.h"
|
#include "UserProfile.h"
|
||||||
|
|
||||||
|
@ -54,6 +55,10 @@ public:
|
||||||
Q_INVOKABLE void logout() const;
|
Q_INVOKABLE void logout() const;
|
||||||
Q_INVOKABLE void
|
Q_INVOKABLE void
|
||||||
createRoom(QString name, QString topic, QString aliasLocalpart, bool isEncrypted, int preset);
|
createRoom(QString name, QString topic, QString aliasLocalpart, bool isEncrypted, int preset);
|
||||||
|
Q_INVOKABLE PowerlevelEditingModels *editPowerlevels(QString room_id_) const
|
||||||
|
{
|
||||||
|
return new PowerlevelEditingModels(room_id_);
|
||||||
|
}
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void updateUserProfile();
|
void updateUserProfile();
|
||||||
|
|
Loading…
Reference in a new issue