From 931855441a12b328afc0aecc1464af927199808b Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 8 Jul 2022 17:28:28 +0200 Subject: [PATCH] Allow editing aliases --- CMakeLists.txt | 4 +- io.github.NhekoReborn.Nheko.yaml | 2 +- resources/icons/ui/building-shop.svg | 1 + resources/icons/ui/room-directory.svg | 1 + resources/qml/RoomList.qml | 2 +- resources/qml/Root.qml | 27 +- resources/qml/dialogs/AliasEditor.qml | 172 +++++++++++++ resources/qml/dialogs/RoomSettings.qml | 12 + resources/res.qrc | 3 + src/AliasEditModel.cpp | 336 +++++++++++++++++++++++++ src/AliasEditModel.h | 79 ++++++ src/MainWindow.cpp | 8 + src/ui/NhekoGlobalObject.h | 5 + 13 files changed, 643 insertions(+), 9 deletions(-) create mode 100644 resources/icons/ui/building-shop.svg create mode 100644 resources/icons/ui/room-directory.svg create mode 100644 resources/qml/dialogs/AliasEditor.qml create mode 100644 src/AliasEditModel.cpp create mode 100644 src/AliasEditModel.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ac07562f..ef57e213 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -420,6 +420,8 @@ set(SRC_FILES src/dock/Dock.cpp src/dock/Dock.h + src/AliasEditModel.cpp + src/AliasEditModel.h src/AvatarProvider.cpp src/AvatarProvider.h src/BlurhashProvider.cpp @@ -579,7 +581,7 @@ if(USE_BUNDLED_MTXCLIENT) FetchContent_Declare( MatrixClient GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git - GIT_TAG e93779692fcc00de136234dd48d0af354717b0a1 + GIT_TAG 842e10c4ae36aba23a20849e766f0c54b19fd4b6 ) set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "") set(BUILD_LIB_TESTS OFF CACHE INTERNAL "") diff --git a/io.github.NhekoReborn.Nheko.yaml b/io.github.NhekoReborn.Nheko.yaml index db3ecbed..3d7b7d39 100644 --- a/io.github.NhekoReborn.Nheko.yaml +++ b/io.github.NhekoReborn.Nheko.yaml @@ -203,7 +203,7 @@ modules: buildsystem: cmake-ninja name: mtxclient sources: - - commit: e93779692fcc00de136234dd48d0af354717b0a1 + - commit: 842e10c4ae36aba23a20849e766f0c54b19fd4b6 #tag: v0.7.0 type: git url: https://github.com/Nheko-Reborn/mtxclient.git diff --git a/resources/icons/ui/building-shop.svg b/resources/icons/ui/building-shop.svg new file mode 100644 index 00000000..32e1d9d9 --- /dev/null +++ b/resources/icons/ui/building-shop.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/ui/room-directory.svg b/resources/icons/ui/room-directory.svg new file mode 100644 index 00000000..30820243 --- /dev/null +++ b/resources/icons/ui/room-directory.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml index f3509f06..a86ca725 100644 --- a/resources/qml/RoomList.qml +++ b/resources/qml/RoomList.qml @@ -750,7 +750,7 @@ Page { hoverEnabled: true width: 22 height: 22 - image: ":/icons/icons/ui/speech-bubbles.svg" + image: ":/icons/icons/ui/room-directory.svg" ToolTip.visible: hovered ToolTip.delay: Nheko.tooltipDelay ToolTip.text: qsTr("Room directory") diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml index 1ea26742..7cc41db9 100644 --- a/resources/qml/Root.qml +++ b/resources/qml/Root.qml @@ -55,13 +55,28 @@ Pane { } - function showPLEditor(settings) { - var dialog = plEditor.createObject(timelineRoot, { - "roomSettings": settings - }); - dialog.show(); - destroyOnClose(dialog); + function showAliasEditor(settings) { + var dialog = aliasEditor.createObject(timelineRoot, { + "roomSettings": settings + }); + dialog.show(); + destroyOnClose(dialog); + } + + Component { + id: aliasEditor + + AliasEditor { } + } + + function showPLEditor(settings) { + var dialog = plEditor.createObject(timelineRoot, { + "roomSettings": settings + }); + dialog.show(); + destroyOnClose(dialog); + } Component { id: plEditor diff --git a/resources/qml/dialogs/AliasEditor.qml b/resources/qml/dialogs/AliasEditor.qml new file mode 100644 index 00000000..0d7b73fd --- /dev/null +++ b/resources/qml/dialogs/AliasEditor.qml @@ -0,0 +1,172 @@ +// 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: aliasEditorW + + property var roomSettings + property var editingModel: Nheko.editAliases(roomSettings.roomId) + + modality: Qt.NonModal + flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint + minimumWidth: 300 + minimumHeight: 400 + height: 600 + width: 500 + + title: qsTr("Aliases to %1").arg(roomSettings.roomName); + + // Shortcut { + // sequence: StandardKey.Cancel + // onActivated: dbb.rejected() + // } + + ColumnLayout { + anchors.margins: Nheko.paddingMedium + anchors.fill: parent + spacing: 0 + + + MatrixText { + text: qsTr("List of aliases to this room. Usually you can only add aliases on your server. You can have one canonical alias and many alternate aliases.") + font.pixelSize: Math.floor(fontMetrics.font.pixelSize * 1.1) + Layout.fillWidth: true + Layout.fillHeight: false + color: Nheko.colors.text + Layout.bottomMargin: Nheko.paddingMedium + } + + ListView { + Layout.fillWidth: true + Layout.fillHeight: true + + id: view + + clip: true + + ScrollHelper { + flickable: parent + anchors.fill: parent + } + + model: editingModel + spacing: 4 + cacheBuffer: 50 + + delegate: RowLayout { + anchors.left: parent.left + anchors.right: parent.right + + Text { + Layout.fillWidth: true + text: model.name + color: model.isPublished ? Nheko.colors.text : Nheko.theme.error + textFormat: Text.PlainText + } + + ImageButton { + Layout.alignment: Qt.AlignRight + Layout.margins: 2 + image: ":/icons/icons/ui/star.svg" + hoverEnabled: true + buttonTextColor: model.isCanonical ? Nheko.colors.highlight : Nheko.colors.text + highlightColor: editingModel.canAdvertize ? Nheko.colors.highlight : buttonTextColor + + ToolTip.visible: hovered + ToolTip.text: model.isCanonical ? qsTr("Primary alias") : qsTr("Make primary alias") + + onClicked: editingModel.makeCanonical(model.index) + } + + ImageButton { + Layout.alignment: Qt.AlignRight + Layout.margins: 2 + image: ":/icons/icons/ui/building-shop.svg" + hoverEnabled: true + buttonTextColor: model.isAdvertized ? Nheko.colors.highlight : Nheko.colors.text + highlightColor: editingModel.canAdvertize ? Nheko.colors.highlight : buttonTextColor + + ToolTip.visible: hovered + ToolTip.text: qsTr("Advertise as an alias in this room") + + onClicked: editingModel.toggleAdvertize(model.index) + } + + ImageButton { + Layout.alignment: Qt.AlignRight + Layout.margins: 2 + image: ":/icons/icons/ui/room-directory.svg" + hoverEnabled: true + buttonTextColor: model.isPublished ? Nheko.colors.highlight : Nheko.colors.text + + ToolTip.visible: hovered + ToolTip.text: qsTr("Publish in room directory") + + onClicked: editingModel.togglePublish(model.index) + } + + ImageButton { + Layout.alignment: Qt.AlignRight + Layout.margins: 2 + image: ":/icons/icons/ui/dismiss.svg" + hoverEnabled: true + + ToolTip.visible: hovered + ToolTip.text: qsTr("Remove this alias") + + onClicked: editingModel.deleteAlias(model.index) + } + } + } + + RowLayout { + spacing: Nheko.paddingMedium + Layout.fillWidth: true + + TextField { + id: newAliasVal + + Layout.fillWidth: true + + placeholderText: qsTr("#new-alias:server.tld") + + Keys.onPressed: { + if (event.matches(StandardKey.InsertParagraphSeparator)) { + editingModel.addAlias(newAliasVal.text); + newAliasVal.clear(); + } + } + } + + Button { + text: qsTr("Add") + Layout.preferredWidth: 100 + onClicked: { + editingModel.addAlias(newAliasVal.text); + newAliasVal.clear(); + } + } + } + } + + footer: DialogButtonBox { + id: dbb + + standardButtons: DialogButtonBox.Ok | DialogButtonBox.Cancel + onAccepted: { + editingModel.commit(); + aliasEditorW.close(); + } + onRejected: aliasEditorW.close(); + } + +} diff --git a/resources/qml/dialogs/RoomSettings.qml b/resources/qml/dialogs/RoomSettings.qml index 2818a79a..431c9dd6 100644 --- a/resources/qml/dialogs/RoomSettings.qml +++ b/resources/qml/dialogs/RoomSettings.qml @@ -352,6 +352,18 @@ ApplicationWindow { Layout.alignment: Qt.AlignRight } + Label { + text: qsTr("Addresses") + color: Nheko.colors.text + } + + Button { + text: qsTr("Configure") + ToolTip.text: qsTr("View and change the addresses/aliases of this room") + onClicked: timelineRoot.showAliasEditor(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 6e3023ea..3ec24238 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -4,6 +4,7 @@ icons/ui/angle-arrow-left.svg icons/ui/attach.svg icons/ui/ban.svg + icons/ui/building-shop.svg icons/ui/chat.svg icons/ui/checkmark.svg icons/ui/clock.svg @@ -36,6 +37,7 @@ icons/ui/reply.svg icons/ui/ribbon.svg icons/ui/ribbon_star.svg + icons/ui/room-directory.svg icons/ui/round-remove-button.svg icons/ui/screen-share.svg icons/ui/search.svg @@ -147,6 +149,7 @@ qml/device-verification/NewVerificationRequest.qml qml/device-verification/Success.qml qml/device-verification/Waiting.qml + qml/dialogs/AliasEditor.qml qml/dialogs/CreateDirect.qml qml/dialogs/CreateRoom.qml qml/dialogs/HiddenEventsDialog.qml diff --git a/src/AliasEditModel.cpp b/src/AliasEditModel.cpp new file mode 100644 index 00000000..1ca7f5e6 --- /dev/null +++ b/src/AliasEditModel.cpp @@ -0,0 +1,336 @@ +// SPDX-FileCopyrightText: 2022 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "AliasEditModel.h" + +#include + +#include + +#include + +#include "Cache.h" +#include "Cache_p.h" +#include "ChatPage.h" +#include "Logging.h" +#include "MatrixClient.h" +#include "timeline/Permissions.h" +#include "timeline/TimelineModel.h" + +AliasEditingModel::AliasEditingModel(const std::string &rid, QObject *parent) + : QAbstractListModel(parent) + , room_id(rid) + , aliasEvent(cache::client() + ->getStateEvent(room_id) + .value_or(mtx::events::StateEvent{}) + .content) + , canSendStateEvent( + Permissions(QString::fromStdString(rid)).canChange(qml_mtx_events::CanonicalAlias)) +{ + std::set seen_aliases; + + if (!aliasEvent.alias.empty()) { + aliases.push_back(Entry{aliasEvent.alias, true, true, false}); + seen_aliases.insert(aliasEvent.alias); + } + + for (const auto &alias : aliasEvent.alt_aliases) { + if (!seen_aliases.count(alias)) { + aliases.push_back(Entry{aliasEvent.alias, false, true, false}); + seen_aliases.insert(aliasEvent.alias); + } + } + + for (const auto &alias : aliases) { + fetchAliasesStatus(alias.alias); + } + fetchPublishedAliases(); +} + +void +AliasEditingModel::fetchPublishedAliases() +{ + auto job = QSharedPointer::create(); + connect(job.data(), + &FetchPublishedAliasesJob::advertizedAliasesFetched, + this, + &AliasEditingModel::updatePublishedAliases); + http::client()->list_room_aliases( + room_id, [job](const mtx::responses::Aliases &aliasesFetched, mtx::http::RequestErr) { + emit job->advertizedAliasesFetched(std::move(aliasesFetched.aliases)); + }); +} + +void +AliasEditingModel::fetchAliasesStatus(const std::string &alias) +{ + auto job = QSharedPointer::create(); + connect( + job.data(), &FetchPublishedAliasesJob::aliasFetched, this, &AliasEditingModel::updateAlias); + http::client()->resolve_room_alias( + alias, [job, alias](const mtx::responses::RoomId &roomIdFetched, mtx::http::RequestErr e) { + if (!e) + emit job->aliasFetched(alias, std::move(roomIdFetched.room_id)); + }); +} + +QHash +AliasEditingModel::roleNames() const +{ + return { + {Name, "name"}, + {IsPublished, "isPublished"}, + {IsCanonical, "isCanonical"}, + {IsAdvertized, "isAdvertized"}, + }; +} + +QVariant +AliasEditingModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= aliases.size()) + return {}; + + const auto &entry = aliases.at(index.row()); + + switch (role) { + case Name: + return QString::fromStdString(entry.alias); + case IsPublished: + return entry.published; + case IsCanonical: + return entry.canonical; + case IsAdvertized: + return entry.advertized; + } + + return {}; +} + +bool +AliasEditingModel::deleteAlias(int row) +{ + if (row < 0 || row >= aliases.size() || aliases.at(row).alias.empty()) + return false; + + auto alias = aliases.at(row); + + beginRemoveRows(QModelIndex(), row, row); + aliases.remove(row); + endRemoveRows(); + + if (alias.published) + http::client()->delete_room_alias(alias.alias, [alias](mtx::http::RequestErr e) { + if (e) { + nhlog::net()->error("Failed to delete {}: {}", alias.alias, *e); + ChatPage::instance()->showNotification( + tr("Failed to unpublish alias %1: %2") + .arg(QString::fromStdString(alias.alias), + QString::fromStdString(e->matrix_error.error))); + } + }); + + if (aliasEvent.alias == alias.alias) + aliasEvent.alias.clear(); + + for (size_t i = 0; i < aliasEvent.alt_aliases.size(); i++) { + if (aliasEvent.alt_aliases[i] == alias.alias) { + aliasEvent.alt_aliases.erase(aliasEvent.alt_aliases.begin() + i); + break; + } + } + + return true; +} + +void +AliasEditingModel::addAlias(QString newAlias) +{ + const auto aliasStr = newAlias.toStdString(); + for (const auto &e : aliases) { + if (e.alias == aliasStr) { + return; + } + } + + beginInsertRows(QModelIndex(), aliases.length(), aliases.length()); + if (aliasEvent.alias.empty()) + aliasEvent.alias = aliasStr; + else + aliasEvent.alt_aliases.push_back(aliasStr); + aliases.push_back( + Entry{aliasStr, aliasEvent.alias.empty() && canSendStateEvent, canSendStateEvent, false}); + endInsertRows(); + + auto job = QSharedPointer::create(); + connect( + job.data(), &FetchPublishedAliasesJob::aliasFetched, this, &AliasEditingModel::updateAlias); + auto room = room_id; + http::client()->add_room_alias( + aliasStr, room_id, [job, aliasStr, room](mtx::http::RequestErr e) { + if (e) { + nhlog::net()->error("Failed to publish {}: {}", aliasStr, *e); + ChatPage::instance()->showNotification( + tr("Failed to unpublish alias %1: %2") + .arg(QString::fromStdString(aliasStr), + QString::fromStdString(e->matrix_error.error))); + emit job->aliasFetched(aliasStr, ""); + } else { + emit job->aliasFetched(aliasStr, room); + } + }); +} + +void +AliasEditingModel::makeCanonical(int row) +{ + if (!canSendStateEvent || row < 0 || row >= aliases.size() || aliases.at(row).alias.empty()) + return; + + auto moveAlias = aliases.at(row).alias; + + if (!aliasEvent.alias.empty()) { + for (qsizetype i = 0; i < aliases.size(); i++) { + if (moveAlias == aliases[i].alias) { + if (aliases[i].canonical) { + aliases[i].canonical = false; + aliasEvent.alt_aliases.push_back(aliasEvent.alias); + emit dataChanged(index(i), index(i), {IsCanonical}); + } + break; + } + } + } + + aliasEvent.alias = moveAlias; + for (auto i = aliasEvent.alt_aliases.begin(); i != aliasEvent.alt_aliases.end(); ++i) { + if (*i == moveAlias) { + aliasEvent.alt_aliases.erase(i); + break; + } + } + aliases[row].canonical = true; + aliases[row].advertized = true; + emit dataChanged(index(row), index(row), {IsCanonical, IsAdvertized}); +} + +void +AliasEditingModel::togglePublish(int row) +{ + if (row < 0 || row >= aliases.size() || aliases.at(row).alias.empty()) + return; + auto aliasStr = aliases[row].alias; + + auto job = QSharedPointer::create(); + connect( + job.data(), &FetchPublishedAliasesJob::aliasFetched, this, &AliasEditingModel::updateAlias); + auto room = room_id; + if (!aliases[row].published) + http::client()->add_room_alias( + aliasStr, room_id, [job, aliasStr, room](mtx::http::RequestErr e) { + if (e) { + nhlog::net()->error("Failed to publish {}: {}", aliasStr, *e); + ChatPage::instance()->showNotification( + tr("Failed to unpublish alias %1: %2") + .arg(QString::fromStdString(aliasStr), + QString::fromStdString(e->matrix_error.error))); + emit job->aliasFetched(aliasStr, ""); + } else { + emit job->aliasFetched(aliasStr, room); + } + }); + else + http::client()->delete_room_alias(aliasStr, [job, aliasStr, room](mtx::http::RequestErr e) { + if (e) { + nhlog::net()->error("Failed to unpublish {}: {}", aliasStr, *e); + ChatPage::instance()->showNotification( + tr("Failed to unpublish alias %1: %2") + .arg(QString::fromStdString(aliasStr), + QString::fromStdString(e->matrix_error.error))); + emit job->aliasFetched(aliasStr, room); + } else { + emit job->aliasFetched(aliasStr, ""); + } + }); +} + +void +AliasEditingModel::toggleAdvertize(int row) +{ + if (!canSendStateEvent || row < 0 || row >= aliases.size() || aliases.at(row).alias.empty()) + return; + + auto &moveAlias = aliases[row]; + if (aliasEvent.alias == moveAlias.alias) { + moveAlias.canonical = false; + moveAlias.advertized = false; + aliasEvent.alias.clear(); + emit dataChanged(index(row), index(row), {IsAdvertized, IsCanonical}); + } else if (moveAlias.advertized) { + for (auto i = aliasEvent.alt_aliases.begin(); i != aliasEvent.alt_aliases.end(); ++i) { + if (*i == moveAlias.alias) { + aliasEvent.alt_aliases.erase(i); + moveAlias.advertized = false; + emit dataChanged(index(row), index(row), {IsAdvertized}); + break; + } + } + } else { + aliasEvent.alt_aliases.push_back(moveAlias.alias); + moveAlias.advertized = true; + emit dataChanged(index(row), index(row), {IsAdvertized}); + } +} + +void +AliasEditingModel::updateAlias(std::string alias, std::string target) +{ + for (qsizetype i = 0; i < aliases.size(); i++) { + auto &e = aliases[i]; + if (e.alias == alias) { + e.published = (target == room_id); + emit dataChanged(index(i), index(i), {IsPublished}); + } + } +} + +void +AliasEditingModel::updatePublishedAliases(std::vector advAliases) +{ + for (const auto &advAlias : advAliases) { + bool found = false; + for (qsizetype i = 0; i < aliases.size(); i++) { + auto &alias = aliases[i]; + if (alias.alias == advAlias) { + alias.published = true; + emit dataChanged(index(i), index(i), {IsPublished}); + found = true; + break; + } + + if (!found) { + beginInsertRows(QModelIndex(), aliases.size(), aliases.size()); + aliases.push_back(Entry{advAlias, false, false, true}); + endInsertRows(); + } + } + } +} + +void +AliasEditingModel::commit() +{ + if (!canSendStateEvent) + return; + + http::client()->send_state_event( + room_id, aliasEvent, [](const mtx::responses::EventId &, mtx::http::RequestErr e) { + if (e) { + nhlog::net()->error("Failed to send Alias event: {}", *e); + ChatPage::instance()->showNotification( + tr("Failed to update aliases: %1") + .arg(QString::fromStdString(e->matrix_error.error))); + } + }); +} diff --git a/src/AliasEditModel.h b/src/AliasEditModel.h new file mode 100644 index 00000000..4fccf9ce --- /dev/null +++ b/src/AliasEditModel.h @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2022 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +#include + +#include "CacheStructs.h" + +class FetchPublishedAliasesJob : public QObject +{ + Q_OBJECT + +public: + explicit FetchPublishedAliasesJob(QObject *p = nullptr) + : QObject(p) + {} + +signals: + void aliasFetched(std::string alias, std::string target); + void advertizedAliasesFetched(std::vector aliases); +}; + +class AliasEditingModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(bool canAdvertize READ canAdvertize CONSTANT) + +public: + enum Roles + { + Name, + IsPublished, + IsCanonical, + IsAdvertized, + }; + + explicit AliasEditingModel(const std::string &room_id_, QObject *parent = nullptr); + + QHash roleNames() const override; + int rowCount(const QModelIndex &) const override { return static_cast(aliases.size()); } + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + bool canAdvertize() const { return canSendStateEvent; } + + Q_INVOKABLE bool deleteAlias(int row); + Q_INVOKABLE void addAlias(QString newAlias); + Q_INVOKABLE void makeCanonical(int row); + Q_INVOKABLE void togglePublish(int row); + Q_INVOKABLE void toggleAdvertize(int row); + Q_INVOKABLE void commit(); + +private slots: + void updateAlias(std::string alias, std::string target); + void updatePublishedAliases(std::vector aliases); + +private: + void fetchAliasesStatus(const std::string &alias); + void fetchPublishedAliases(); + + struct Entry + { + ~Entry() = default; + + std::string alias; + bool canonical = false; + bool advertized = false; + bool published = false; + }; + + std::string room_id; + QVector aliases; + mtx::events::state::CanonicalAlias aliasEvent; + bool canSendStateEvent = false; +}; diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index f031c80f..d2e28277 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -10,6 +10,7 @@ #include #include +#include "AliasEditModel.h" #include "BlurhashProvider.h" #include "Cache.h" #include "Cache_p.h" @@ -179,6 +180,13 @@ MainWindow::registerQmlTypes() qmlRegisterType("im.nheko", 1, 0, "Login"); qmlRegisterType("im.nheko", 1, 0, "Registration"); qmlRegisterType("im.nheko", 1, 0, "HiddenEvents"); + qmlRegisterUncreatableType( + "im.nheko", + 1, + 0, + "AliasEditingModel", + QStringLiteral("Please use editAliases to create the models")); + qmlRegisterUncreatableType( "im.nheko", 1, diff --git a/src/ui/NhekoGlobalObject.h b/src/ui/NhekoGlobalObject.h index bd141f35..f9de489d 100644 --- a/src/ui/NhekoGlobalObject.h +++ b/src/ui/NhekoGlobalObject.h @@ -9,6 +9,7 @@ #include #include +#include "AliasEditModel.h" #include "PowerlevelsEditModels.h" #include "Theme.h" #include "UserProfile.h" @@ -59,6 +60,10 @@ public: { return new PowerlevelEditingModels(room_id_); } + Q_INVOKABLE AliasEditingModel *editAliases(QString room_id_) const + { + return new AliasEditingModel(room_id_.toStdString()); + } public slots: void updateUserProfile();