diff --git a/resources/qml/Completer.qml b/resources/qml/Completer.qml
index a16ffa65..7e14e734 100644
--- a/resources/qml/Completer.qml
+++ b/resources/qml/Completer.qml
@@ -13,6 +13,7 @@ Control {
id: popup
property alias currentIndex: listView.currentIndex
+ property string roomId
property string completerName
property var completer
property bool bottomToTop: true
@@ -24,6 +25,10 @@ Control {
property int rowSpacing: 5
property alias count: listView.count
+ Component.onCompleted: {
+ console.log("RRRRRRRRRR: " + roomId);
+ }
+
signal completionClicked(string completion)
signal completionSelected(string id)
@@ -65,18 +70,22 @@ Control {
function finishCompletion() {
if (popup.completerName == "room")
popup.completionSelected(listView.itemAtIndex(currentIndex).modelData.roomid);
+ else if (popup.completerName == "user")
+ popup.completionSelected(listView.itemAtIndex(currentIndex).modelData.userid);
}
- onCompleterNameChanged: {
+ function changeCompleter() {
if (completerName) {
- completer = TimelineManager.completerFor(completerName, completerName == "room" ? "" : room.roomId);
+ completer = TimelineManager.completerFor(completerName, completerName == "room" ? "" : (popup.roomId != "" ? popup.roomId : room.roomId));
completer.setSearchString("");
} else {
completer = undefined;
}
currentIndex = -1
}
+ onCompleterNameChanged: changeCompleter()
+ onRoomIdChanged: changeCompleter()
bottomPadding: 1
leftPadding: 1
@@ -131,6 +140,8 @@ Control {
popup.completionClicked(completer.completionAt(model.index));
if (popup.completerName == "room")
popup.completionSelected(model.roomid);
+ else if (popup.completerName == "user")
+ popup.completionSelected(model.userid);
}
}
Ripple {
@@ -151,7 +162,7 @@ Control {
RowLayout {
id: del
- anchors.centerIn: parent
+ anchors.centerIn: centerRowContent ? parent : undefined
spacing: rowSpacing
Avatar {
@@ -160,7 +171,7 @@ Control {
displayName: model.displayName
userid: model.userid
url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
- onClicked: popup.completionClicked(completer.completionAt(model.index))
+ enabled: false
}
Label {
@@ -216,7 +227,7 @@ Control {
displayName: model.shortcode
//userid: model.shortcode
url: model.url.replace("mxc://", "image://MxcImage/")
- onClicked: popup.completionClicked(completer.completionAt(model.index))
+ enabled: false
crop: false
}
@@ -249,10 +260,7 @@ Control {
displayName: model.roomName
roomid: model.roomid
url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
- onClicked: {
- popup.completionClicked(completer.completionAt(model.index));
- popup.completionSelected(model.roomid);
- }
+ enabled: false
}
Label {
@@ -281,7 +289,7 @@ Control {
displayName: model.roomName
roomid: model.roomid
url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
- onClicked: popup.completionClicked(completer.completionAt(model.index))
+ enabled: false
}
Label {
diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml
index 00600508..86ddc649 100644
--- a/resources/qml/Root.qml
+++ b/resources/qml/Root.qml
@@ -51,6 +51,22 @@ Pane {
}
+ function showPLEditor(settings) {
+ var dialog = plEditor.createObject(timelineRoot, {
+ "roomSettings": settings
+ });
+ dialog.show();
+ destroyOnClose(dialog);
+ }
+
+ Component {
+ id: plEditor
+
+ PowerLevelEditor {
+ }
+ }
+
+
Component {
id: roomSettingsComponent
diff --git a/resources/qml/components/ReorderableListview.qml b/resources/qml/components/ReorderableListview.qml
new file mode 100644
index 00000000..7e9ae05d
--- /dev/null
+++ b/resources/qml/components/ReorderableListview.qml
@@ -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
+ }
+
+
+ }
+
+
diff --git a/resources/qml/dialogs/PowerLevelEditor.qml b/resources/qml/dialogs/PowerLevelEditor.qml
new file mode 100644
index 00000000..241585f9
--- /dev/null
+++ b/resources/qml/dialogs/PowerLevelEditor.qml
@@ -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();
+ }
+
+ }
diff --git a/resources/qml/dialogs/RoomSettings.qml b/resources/qml/dialogs/RoomSettings.qml
index 4a7b24fe..332a7b09 100644
--- a/resources/qml/dialogs/RoomSettings.qml
+++ b/resources/qml/dialogs/RoomSettings.qml
@@ -336,6 +336,18 @@ ApplicationWindow {
}
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 {
text: qsTr("Sticker & Emote Settings")
color: Nheko.colors.text
}
diff --git a/resources/res.qrc b/resources/res.qrc
index 35b06704..6e3023ea 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -127,6 +127,7 @@
<file>qml/components/AvatarListTile.qml</file>
<file>qml/components/FlatButton.qml</file>
<file>qml/components/MainWindowDialog.qml</file>
+ <file>qml/components/ReorderableListview.qml</file>
<file>qml/components/TextButton.qml</file>
<file>qml/delegates/Encrypted.qml</file>
<file>qml/delegates/FileMessage.qml</file>
@@ -148,22 +149,23 @@
<file>qml/device-verification/Waiting.qml</file>
<file>qml/dialogs/CreateDirect.qml</file>
<file>qml/dialogs/CreateRoom.qml</file>
+ <file>qml/dialogs/HiddenEventsDialog.qml</file>
<file>qml/dialogs/ImageOverlay.qml</file>
<file>qml/dialogs/ImagePackEditorDialog.qml</file>
<file>qml/dialogs/ImagePackSettingsDialog.qml</file>
- <file>qml/dialogs/PhoneNumberInputDialog.qml</file>
<file>qml/dialogs/InputDialog.qml</file>
<file>qml/dialogs/InviteDialog.qml</file>
<file>qml/dialogs/JoinRoomDialog.qml</file>
<file>qml/dialogs/LeaveRoomDialog.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/ReadReceipts.qml</file>
<file>qml/dialogs/RoomDirectory.qml</file>
<file>qml/dialogs/RoomMembers.qml</file>
<file>qml/dialogs/RoomSettings.qml</file>
<file>qml/dialogs/UserProfile.qml</file>
- <file>qml/dialogs/HiddenEventsDialog.qml</file>
<file>qml/emoji/EmojiPicker.qml</file>
<file>qml/emoji/StickerPicker.qml</file>
<file>qml/ui/NhekoSlider.qml</file>
|