summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorNicolas Werner <nicolas.werner@hotmail.de>2022-05-27 16:31:54 +0200
committerNicolas Werner <nicolas.werner@hotmail.de>2022-05-27 17:01:58 +0200
commit6c6d43691d98aa02513350b52fe736fff6d6071d (patch)
tree6155225e4005f22c2a613a77912227b05fb48ff7 /src
parentTranslated using Weblate (Russian) (diff)
downloadnheko-6c6d43691d98aa02513350b52fe736fff6d6071d.tar.xz
Add basic powerlevel editor
Diffstat (limited to 'src')
-rw-r--r--src/MainWindow.cpp7
-rw-r--r--src/PowerlevelsEditModels.cpp534
-rw-r--r--src/PowerlevelsEditModels.h140
-rw-r--r--src/timeline/TimelineModel.h1
-rw-r--r--src/ui/NhekoGlobalObject.h5
5 files changed, 687 insertions, 0 deletions
diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp

index 6cae64b2..c700294c 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp
@@ -29,6 +29,7 @@ #include "MatrixClient.h" #include "MemberList.h" #include "MxcImageProvider.h" +#include "PowerlevelsEditModels.h" #include "ReadReceiptsModel.h" #include "RegisterPage.h" #include "RoomDirectoryModel.h" @@ -174,6 +175,12 @@ MainWindow::registerQmlTypes() qmlRegisterType<LoginPage>("im.nheko", 1, 0, "Login"); qmlRegisterType<RegisterPage>("im.nheko", 1, 0, "Registration"); qmlRegisterType<HiddenEvents>("im.nheko", 1, 0, "HiddenEvents"); + qmlRegisterUncreatableType<PowerlevelEditingModels>( + "im.nheko", + 1, + 0, + "PowerlevelEditingModels", + QStringLiteral("Please use editPowerlevels to create the models")); qmlRegisterUncreatableType<DeviceVerificationFlow>( "im.nheko", 1, diff --git a/src/PowerlevelsEditModels.cpp b/src/PowerlevelsEditModels.cpp new file mode 100644
index 00000000..b0244b08 --- /dev/null +++ b/src/PowerlevelsEditModels.cpp
@@ -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))); + } + }); +} diff --git a/src/PowerlevelsEditModels.h b/src/PowerlevelsEditModels.h new file mode 100644
index 00000000..7bc797ea --- /dev/null +++ b/src/PowerlevelsEditModels.h
@@ -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_; +}; diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index c52473b1..dae64094 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h
@@ -282,6 +282,7 @@ public: Q_INVOKABLE bool saveMedia(const QString &eventId) const; Q_INVOKABLE void showEvent(QString eventId); Q_INVOKABLE void copyLinkToEvent(const QString &eventId) const; + void cacheMedia(const QString &eventId, const std::function<void(const QString filename)> &callback); Q_INVOKABLE void sendReset() diff --git a/src/ui/NhekoGlobalObject.h b/src/ui/NhekoGlobalObject.h
index cfcf31fb..bd141f35 100644 --- a/src/ui/NhekoGlobalObject.h +++ b/src/ui/NhekoGlobalObject.h
@@ -9,6 +9,7 @@ #include <QObject> #include <QPalette> +#include "PowerlevelsEditModels.h" #include "Theme.h" #include "UserProfile.h" @@ -54,6 +55,10 @@ public: Q_INVOKABLE void logout() const; Q_INVOKABLE void 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: void updateUserProfile();