summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNicolas Werner <nicolas.werner@hotmail.de>2022-09-28 02:09:04 +0200
committerNicolas Werner <nicolas.werner@hotmail.de>2022-09-28 02:09:04 +0200
commit051c25d5b87c2351df46173f19b907cea436fa3b (patch)
tree7e68ff95c1a678cc614f908e67cef5fe0c6a624c
parent0752f9477e1beb6cfbcec608b60c6418cd3dccf4 (diff)
Allow editing permissions in spaces recursively
-rw-r--r--resources/qml/Root.qml16
-rw-r--r--resources/qml/dialogs/PowerLevelEditor.qml11
-rw-r--r--resources/qml/dialogs/PowerLevelSpacesApplyDialog.qml148
-rw-r--r--resources/res.qrc1
-rw-r--r--src/PowerlevelsEditModels.cpp239
-rw-r--r--src/PowerlevelsEditModels.h95
6 files changed, 484 insertions, 26 deletions
diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml
index 063284c1..dd1dfe1e 100644
--- a/resources/qml/Root.qml
+++ b/resources/qml/Root.qml
@@ -79,6 +79,22 @@ Pane {
}
Component {
+ id: plApplyPrompt
+
+ PowerLevelSpacesApplyDialog {
+ }
+ }
+
+ function showSpacePLApplyPrompt(settings, editingModel) {
+ var dialog = plApplyPrompt.createObject(timelineRoot, {
+ "roomSettings": settings,
+ "editingModel": editingModel
+ });
+ dialog.show();
+ destroyOnClose(dialog);
+ }
+
+ Component {
id: plEditor
PowerLevelEditor {
diff --git a/resources/qml/dialogs/PowerLevelEditor.qml b/resources/qml/dialogs/PowerLevelEditor.qml
index bfb337ff..4c23d9af 100644
--- a/resources/qml/dialogs/PowerLevelEditor.qml
+++ b/resources/qml/dialogs/PowerLevelEditor.qml
@@ -397,8 +397,15 @@ ApplicationWindow {
standardButtons: DialogButtonBox.Ok | DialogButtonBox.Cancel
onAccepted: {
- editingModel.commit();
- plEditorW.close();
+ if (editingModel.isSpace) {
+ // TODO(Nico): Replace with showing a list of spaces to apply to
+ editingModel.updateSpacesModel();
+ plEditorW.close();
+ timelineRoot.showSpacePLApplyPrompt(roomSettings, editingModel)
+ } else {
+ editingModel.commit();
+ plEditorW.close();
+ }
}
onRejected: plEditorW.close();
}
diff --git a/resources/qml/dialogs/PowerLevelSpacesApplyDialog.qml b/resources/qml/dialogs/PowerLevelSpacesApplyDialog.qml
new file mode 100644
index 00000000..83af00f7
--- /dev/null
+++ b/resources/qml/dialogs/PowerLevelSpacesApplyDialog.qml
@@ -0,0 +1,148 @@
+// SPDX-FileCopyrightText: 2022 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import ".."
+import "../ui"
+import Qt.labs.platform 1.1 as Platform
+import QtQuick 2.15
+import QtQuick.Controls 2.3
+import QtQuick.Layouts 1.2
+import QtQuick.Window 2.13
+import im.nheko 1.0
+
+ApplicationWindow {
+ id: applyDialog
+
+ property RoomSettings roomSettings
+ property PowerlevelEditingModels editingModel
+
+ minimumWidth: 340
+ minimumHeight: 450
+ width: 450
+ height: 680
+ palette: Nheko.colors
+ color: Nheko.colors.window
+ modality: Qt.NonModal
+ flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
+ title: qsTr("Apply permission changes")
+
+ Shortcut {
+ sequence: StandardKey.Cancel
+ onActivated: roomSettingsDialog.close()
+ }
+
+ ColumnLayout {
+ anchors.margins: Nheko.paddingMedium
+ anchors.fill: parent
+ spacing: Nheko.paddingLarge
+
+
+ MatrixText {
+ text: qsTr("Which of the subcommunities and rooms should these permissions be applied to?")
+ font.pixelSize: Math.floor(fontMetrics.font.pixelSize * 1.1)
+ Layout.fillWidth: true
+ Layout.fillHeight: false
+ color: Nheko.colors.text
+ Layout.bottomMargin: Nheko.paddingMedium
+ }
+
+ GridLayout {
+ Layout.fillWidth: true
+ Layout.fillHeight: false
+ columns: 2
+
+ Label {
+ text: qsTr("Apply permissions recursively")
+ Layout.fillWidth: true
+ color: Nheko.colors.text
+ }
+
+ ToggleButton {
+ checked: editingModel.spaces.applyToChildren
+ Layout.alignment: Qt.AlignRight
+ onCheckedChanged: editingModel.spaces.applyToChildren = checked
+ }
+
+ Label {
+ text: qsTr("Overwrite exisiting modifications in rooms")
+ Layout.fillWidth: true
+ color: Nheko.colors.text
+ }
+
+ ToggleButton {
+ checked: editingModel.spaces.overwriteDiverged
+ Layout.alignment: Qt.AlignRight
+ onCheckedChanged: editingModel.spaces.overwriteDiverged = checked
+ }
+ }
+
+ ListView {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ id: view
+
+ clip: true
+
+ ScrollHelper {
+ flickable: parent
+ anchors.fill: parent
+ }
+
+ model: editingModel.spaces
+ spacing: 4
+ cacheBuffer: 50
+
+ delegate: RowLayout {
+ anchors.left: parent.left
+ anchors.right: parent.right
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ Text {
+ Layout.fillWidth: true
+ text: model.displayName
+ color: Nheko.colors.text
+ textFormat: Text.PlainText
+ elide: Text.ElideRight
+ }
+
+ Text {
+ Layout.fillWidth: true
+ text: {
+ if (!model.isEditable) return qsTr("No permissions to apply the new permissions here");
+ if (model.isAlreadyUpToDate) return qsTr("No changes needed");
+ if (model.isDifferentFromBase) return qsTr("Existing modifications to the permissions in this room will be overwritten");
+ return qsTr("Permissions synchronized with community")
+ }
+ elide: Text.ElideRight
+ color: Nheko.colors.buttonText
+ textFormat: Text.PlainText
+ }
+ }
+
+ ToggleButton {
+ checked: model.applyPermissions
+ Layout.alignment: Qt.AlignRight
+ onCheckedChanged: model.applyPermissions = checked
+ enabled: model.isEditable
+ }
+ }
+ }
+
+
+ }
+
+ footer: DialogButtonBox {
+ id: dbb
+
+ standardButtons: DialogButtonBox.Ok | DialogButtonBox.Cancel
+ onAccepted: {
+ editingModel.spaces.commit();
+ applyDialog.close();
+ }
+ onRejected: applyDialog.close()
+ }
+
+}
diff --git a/resources/res.qrc b/resources/res.qrc
index 27d9c081..7affe702 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -162,6 +162,7 @@
<file>qml/dialogs/LogoutDialog.qml</file>
<file>qml/dialogs/PhoneNumberInputDialog.qml</file>
<file>qml/dialogs/PowerLevelEditor.qml</file>
+ <file>qml/dialogs/PowerLevelSpacesApplyDialog.qml</file>
<file>qml/dialogs/RawMessageDialog.qml</file>
<file>qml/dialogs/ReadReceipts.qml</file>
<file>qml/dialogs/RoomDirectory.qml</file>
diff --git a/src/PowerlevelsEditModels.cpp b/src/PowerlevelsEditModels.cpp
index fcfde26e..09e5b05d 100644
--- a/src/PowerlevelsEditModels.cpp
+++ b/src/PowerlevelsEditModels.cpp
@@ -4,8 +4,12 @@
#include "PowerlevelsEditModels.h"
+#include <QCoreApplication>
+#include <QTimer>
+
#include <algorithm>
#include <set>
+#include <unordered_set>
#include "Cache.h"
#include "Cache_p.h"
@@ -76,7 +80,7 @@ PowerlevelsTypeListModel::PowerlevelsTypeListModel(const std::string &rid,
}
std::map<std::string, mtx::events::state::power_level_t, std::less<>>
-PowerlevelsTypeListModel::toEvents()
+PowerlevelsTypeListModel::toEvents() const
{
std::map<std::string, mtx::events::state::power_level_t, std::less<>> m;
for (const auto &[key, pl] : qAsConst(types))
@@ -85,7 +89,7 @@ PowerlevelsTypeListModel::toEvents()
return m;
}
mtx::events::state::power_level_t
-PowerlevelsTypeListModel::kick()
+PowerlevelsTypeListModel::kick() const
{
for (const auto &[key, pl] : qAsConst(types))
if (key == "kick")
@@ -93,7 +97,7 @@ PowerlevelsTypeListModel::kick()
return powerLevels_.users_default;
}
mtx::events::state::power_level_t
-PowerlevelsTypeListModel::invite()
+PowerlevelsTypeListModel::invite() const
{
for (const auto &[key, pl] : qAsConst(types))
if (key == "invite")
@@ -101,7 +105,7 @@ PowerlevelsTypeListModel::invite()
return powerLevels_.users_default;
}
mtx::events::state::power_level_t
-PowerlevelsTypeListModel::ban()
+PowerlevelsTypeListModel::ban() const
{
for (const auto &[key, pl] : qAsConst(types))
if (key == "ban")
@@ -109,7 +113,7 @@ PowerlevelsTypeListModel::ban()
return powerLevels_.users_default;
}
mtx::events::state::power_level_t
-PowerlevelsTypeListModel::eventsDefault()
+PowerlevelsTypeListModel::eventsDefault() const
{
for (const auto &[key, pl] : qAsConst(types))
if (key == "zdefault_events")
@@ -117,7 +121,7 @@ PowerlevelsTypeListModel::eventsDefault()
return powerLevels_.users_default;
}
mtx::events::state::power_level_t
-PowerlevelsTypeListModel::stateDefault()
+PowerlevelsTypeListModel::stateDefault() const
{
for (const auto &[key, pl] : qAsConst(types))
if (key == "zdefault_states")
@@ -390,7 +394,7 @@ PowerlevelsUserListModel::PowerlevelsUserListModel(const std::string &rid,
}
std::map<std::string, mtx::events::state::power_level_t, std::less<>>
-PowerlevelsUserListModel::toUsers()
+PowerlevelsUserListModel::toUsers() const
{
std::map<std::string, mtx::events::state::power_level_t, std::less<>> m;
for (const auto &[key, pl] : qAsConst(users))
@@ -399,7 +403,7 @@ PowerlevelsUserListModel::toUsers()
return m;
}
mtx::events::state::power_level_t
-PowerlevelsUserListModel::usersDefault()
+PowerlevelsUserListModel::usersDefault() const
{
for (const auto &[key, pl] : qAsConst(users))
if (key == "default")
@@ -565,6 +569,7 @@ PowerlevelEditingModels::PowerlevelEditingModels(QString room_id, QObject *paren
.content)
, types_(room_id.toStdString(), powerLevels_, this)
, users_(room_id.toStdString(), powerLevels_, this)
+ , spaces_(room_id.toStdString(), powerLevels_, this)
, room_id_(room_id.toStdString())
{
connect(&types_,
@@ -581,17 +586,31 @@ PowerlevelEditingModels::PowerlevelEditingModels(QString room_id, QObject *paren
&PowerlevelEditingModels::defaultUserLevelChanged);
}
+bool
+PowerlevelEditingModels::isSpace() const
+{
+ return cache::singleRoomInfo(room_id_).is_space;
+}
+
+mtx::events::state::PowerLevels
+PowerlevelEditingModels::calculateNewPowerlevel() const
+{
+ auto newPl = powerLevels_;
+ newPl.events = types_.toEvents();
+ newPl.kick = types_.kick();
+ newPl.invite = types_.invite();
+ newPl.ban = types_.ban();
+ newPl.events_default = types_.eventsDefault();
+ newPl.state_default = types_.stateDefault();
+ newPl.users = users_.toUsers();
+ newPl.users_default = users_.usersDefault();
+ return newPl;
+}
+
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();
+ powerLevels_ = calculateNewPowerlevel();
http::client()->send_state_event(
room_id_, powerLevels_, [](const mtx::responses::EventId &, mtx::http::RequestErr e) {
@@ -605,6 +624,13 @@ PowerlevelEditingModels::commit()
}
void
+PowerlevelEditingModels::updateSpacesModel()
+{
+ powerLevels_ = calculateNewPowerlevel();
+ spaces_.newPowerlevels_ = powerLevels_;
+}
+
+void
PowerlevelEditingModels::addRole(int pl)
{
for (const auto &e : qAsConst(types_.types))
@@ -614,3 +640,184 @@ PowerlevelEditingModels::addRole(int pl)
types_.addRole(pl);
users_.addRole(pl);
}
+
+static bool
+samePl(const mtx::events::state::PowerLevels &a, const mtx::events::state::PowerLevels &b)
+{
+ return std::tie(a.events,
+ a.users_default,
+ a.users,
+ a.state_default,
+ a.users_default,
+ a.events_default,
+ a.ban,
+ a.kick,
+ a.invite,
+ a.notifications,
+ a.redact) == std::tie(b.events,
+ b.users_default,
+ b.users,
+ b.state_default,
+ b.users_default,
+ b.events_default,
+ b.ban,
+ b.kick,
+ b.invite,
+ b.notifications,
+ b.redact);
+}
+
+PowerlevelsSpacesListModel::PowerlevelsSpacesListModel(const std::string &room_id_,
+ const mtx::events::state::PowerLevels &pl,
+ QObject *parent)
+ : QAbstractListModel(parent)
+ , room_id(std::move(room_id_))
+ , oldPowerLevels_(std::move(pl))
+{
+ beginResetModel();
+
+ spaces.push_back(Entry{room_id, oldPowerLevels_, true});
+
+ std::unordered_set<std::string> visited;
+
+ std::function<void(const std::string &)> addChildren;
+ addChildren = [this, &addChildren, &visited](const std::string &space) {
+ if (visited.count(space))
+ return;
+ else
+ visited.insert(space);
+
+ for (const auto &s : cache::client()->getChildRoomIds(space)) {
+ auto parent =
+ cache::client()->getStateEvent<mtx::events::state::space::Parent>(s, space);
+ if (parent && parent->content.via && !parent->content.via->empty() &&
+ parent->content.canonical) {
+ auto parent = cache::client()->getStateEvent<mtx::events::state::PowerLevels>(s);
+
+ spaces.push_back(
+ Entry{s, parent ? parent->content : mtx::events::state::PowerLevels{}, false});
+ addChildren(s);
+ }
+ }
+ };
+
+ addChildren(room_id);
+
+ endResetModel();
+
+ updateToDefaults();
+}
+
+struct PowerLevelApplier
+{
+ std::vector<std::string> spaces;
+ mtx::events::state::PowerLevels pl;
+
+ void next()
+ {
+ if (spaces.empty())
+ return;
+
+ auto room_id_ = spaces.back();
+ http::client()->send_state_event(
+ room_id_,
+ pl,
+ [self = *this](const mtx::responses::EventId &, mtx::http::RequestErr e) mutable {
+ if (e) {
+ if (e->status_code == 429 && e->matrix_error.retry_after.count() != 0) {
+ QTimer::singleShot(e->matrix_error.retry_after,
+ [self = std::move(self)]() mutable { self.next(); });
+ return;
+ }
+
+ nhlog::net()->error("Failed to send PL event: {}", *e);
+ ChatPage::instance()->showNotification(
+ QCoreApplication::translate("PowerLevels", "Failed to update powerlevel: %1")
+ .arg(QString::fromStdString(e->matrix_error.error)));
+ }
+ self.spaces.pop_back();
+ self.next();
+ });
+ }
+};
+
+void
+PowerlevelsSpacesListModel::commit()
+{
+ std::vector<std::string> spacesToApplyTo;
+
+ for (const auto &s : spaces)
+ if (s.apply)
+ spacesToApplyTo.push_back(s.roomid);
+
+ PowerLevelApplier context{std::move(spacesToApplyTo), newPowerlevels_};
+ context.next();
+}
+
+void
+PowerlevelsSpacesListModel::updateToDefaults()
+{
+ for (int i = 1; i < spaces.size(); i++) {
+ spaces[i].apply =
+ applyToChildren_ && data(index(i), Roles::IsEditable).toBool() &&
+ !data(index(i), Roles::IsAlreadyUpToDate).toBool() &&
+ (overwriteDiverged_ || !data(index(i), Roles::IsDifferentFromBase).toBool());
+ }
+
+ if (spaces.size() > 1)
+ emit dataChanged(index(1), index(spaces.size() - 1), {Roles::ApplyPermissions});
+}
+
+bool
+PowerlevelsSpacesListModel::setData(const QModelIndex &index, const QVariant &value, int role)
+{
+ if (role != Roles::ApplyPermissions || index.row() < 0 || index.row() >= spaces.size())
+ return false;
+
+ spaces[index.row()].apply = value.toBool();
+ return true;
+}
+
+QVariant
+PowerlevelsSpacesListModel::data(QModelIndex const &index, int role) const
+{
+ auto row = index.row();
+ if (row >= spaces.size() && row < 0)
+ return {};
+
+ if (role == Roles::DisplayName || role == Roles::AvatarUrl || role == Roles::IsSpace) {
+ auto info = cache::singleRoomInfo(spaces.at(row).roomid);
+ if (role == Roles::DisplayName)
+ return QString::fromStdString(info.name);
+ else if (role == Roles::AvatarUrl)
+ return QString::fromStdString(info.avatar_url);
+ else
+ return info.is_space;
+ }
+
+ auto entry = spaces.at(row);
+ switch (role) {
+ case Roles::IsEditable:
+ return entry.pl.user_level(http::client()->user_id().to_string()) >=
+ entry.pl.state_level(to_string(mtx::events::EventType::RoomPowerLevels));
+ case Roles::IsDifferentFromBase:
+ return !samePl(entry.pl, oldPowerLevels_);
+ case Roles::IsAlreadyUpToDate:
+ return samePl(entry.pl, newPowerlevels_);
+ case Roles::ApplyPermissions:
+ return entry.apply;
+ }
+ return {};
+}
+QHash<int, QByteArray>
+PowerlevelsSpacesListModel::roleNames() const
+{
+ return {
+ {DisplayName, "displayName"},
+ {AvatarUrl, "avatarUrl"},
+ {IsEditable, "isEditable"},
+ {IsDifferentFromBase, "isDifferentFromBase"},
+ {IsAlreadyUpToDate, "isAlreadyUpToDate"},
+ {ApplyPermissions, "applyPermissions"},
+ };
+}
diff --git a/src/PowerlevelsEditModels.h b/src/PowerlevelsEditModels.h
index 9aa955d2..515fdb56 100644
--- a/src/PowerlevelsEditModels.h
+++ b/src/PowerlevelsEditModels.h
@@ -48,12 +48,12 @@ public:
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();
+ std::map<std::string, mtx::events::state::power_level_t, std::less<>> toEvents() const;
+ mtx::events::state::power_level_t kick() const;
+ mtx::events::state::power_level_t invite() const;
+ mtx::events::state::power_level_t ban() const;
+ mtx::events::state::power_level_t eventsDefault() const;
+ mtx::events::state::power_level_t stateDefault() const;
struct Entry
{
@@ -106,8 +106,8 @@ public:
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();
+ std::map<std::string, mtx::events::state::power_level_t, std::less<>> toUsers() const;
+ mtx::events::state::power_level_t usersDefault() const;
struct Entry
{
@@ -122,38 +122,117 @@ public:
mtx::events::state::PowerLevels powerLevels_;
};
+class PowerlevelsSpacesListModel : public QAbstractListModel
+{
+ Q_OBJECT
+ Q_PROPERTY(bool applyToChildren READ applyToChildren WRITE setApplyToChildren NOTIFY
+ applyToChildrenChanged)
+ Q_PROPERTY(bool overwriteDiverged READ overwriteDiverged WRITE setOverwriteDiverged NOTIFY
+ overwriteDivergedChanged)
+
+signals:
+ void applyToChildrenChanged();
+ void overwriteDivergedChanged();
+
+public:
+ enum Roles
+ {
+ DisplayName,
+ AvatarUrl,
+ IsSpace,
+ IsEditable,
+ IsDifferentFromBase,
+ IsAlreadyUpToDate,
+ ApplyPermissions,
+ };
+
+ explicit PowerlevelsSpacesListModel(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>(spaces.size()); }
+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+ bool
+ setData(const QModelIndex &index, const QVariant &value, int role = Qt::DisplayRole) override;
+
+ bool applyToChildren() const { return applyToChildren_; }
+ bool overwriteDiverged() const { return overwriteDiverged_; }
+
+ void setApplyToChildren(bool val)
+ {
+ applyToChildren_ = val;
+ emit applyToChildrenChanged();
+ updateToDefaults();
+ }
+ void setOverwriteDiverged(bool val)
+ {
+ overwriteDiverged_ = val;
+ emit overwriteDivergedChanged();
+ updateToDefaults();
+ }
+
+ void updateToDefaults();
+
+ Q_INVOKABLE void commit();
+
+ struct Entry
+ {
+ ~Entry() = default;
+
+ std::string roomid;
+ mtx::events::state::PowerLevels pl;
+ bool apply = false;
+ };
+
+ std::string room_id;
+ QVector<Entry> spaces;
+ mtx::events::state::PowerLevels oldPowerLevels_, newPowerlevels_;
+
+ bool applyToChildren_ = true, overwriteDiverged_ = false;
+};
+
class PowerlevelEditingModels : public QObject
{
Q_OBJECT
Q_PROPERTY(PowerlevelsUserListModel *users READ users CONSTANT)
Q_PROPERTY(PowerlevelsTypeListModel *types READ types CONSTANT)
+ Q_PROPERTY(PowerlevelsSpacesListModel *spaces READ spaces CONSTANT)
Q_PROPERTY(qlonglong adminLevel READ adminLevel NOTIFY adminLevelChanged)
Q_PROPERTY(qlonglong moderatorLevel READ moderatorLevel NOTIFY moderatorLevelChanged)
Q_PROPERTY(qlonglong defaultUserLevel READ defaultUserLevel NOTIFY defaultUserLevelChanged)
+ Q_PROPERTY(bool isSpace READ isSpace CONSTANT)
signals:
void adminLevelChanged();
void moderatorLevelChanged();
void defaultUserLevelChanged();
+private:
+ mtx::events::state::PowerLevels calculateNewPowerlevel() const;
+
public:
explicit PowerlevelEditingModels(QString room_id, QObject *parent = nullptr);
PowerlevelsUserListModel *users() { return &users_; }
PowerlevelsTypeListModel *types() { return &types_; }
+ PowerlevelsSpacesListModel *spaces() { return &spaces_; }
qlonglong adminLevel() const
{
return powerLevels_.state_level(to_string(mtx::events::EventType::RoomPowerLevels));
}
qlonglong moderatorLevel() const { return powerLevels_.redact; }
qlonglong defaultUserLevel() const { return powerLevels_.users_default; }
+ bool isSpace() const;
Q_INVOKABLE void commit();
+ Q_INVOKABLE void updateSpacesModel();
Q_INVOKABLE void addRole(int pl);
mtx::events::state::PowerLevels powerLevels_;
PowerlevelsTypeListModel types_;
PowerlevelsUserListModel users_;
+ PowerlevelsSpacesListModel spaces_;
std::string room_id_;
};