mirror of https://github.com/Nheko-Reborn/nheko
parent
ff6e4826e4
commit
6c6d43691d
@ -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 |
||||
} |
||||
|
||||
|
||||
} |
||||
|
||||
|
@ -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(); |
||||
} |
||||
|
||||
} |
@ -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))); |
||||
} |
||||
}); |
||||
} |
@ -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_; |
||||
}; |
Loading…
Reference in new issue