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)));
+ }
+ });
+}
|