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 <QSharedPointer>
+
+#include <set>
+
+#include <mtx/responses/common.hpp>
+
+#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<mtx::events::state::CanonicalAlias>(room_id)
+ .value_or(mtx::events::StateEvent<mtx::events::state::CanonicalAlias>{})
+ .content)
+ , canSendStateEvent(
+ Permissions(QString::fromStdString(rid)).canChange(qml_mtx_events::CanonicalAlias))
+{
+ std::set<std::string> 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<FetchPublishedAliasesJob>::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<FetchPublishedAliasesJob>::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<int, QByteArray>
+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<FetchPublishedAliasesJob>::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<FetchPublishedAliasesJob>::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<std::string> 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 <QAbstractListModel>
+#include <QVector>
+
+#include <mtx/events/canonical_alias.hpp>
+
+#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<std::string> 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<int, QByteArray> roleNames() const override;
+ int rowCount(const QModelIndex &) const override { return static_cast<int>(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<std::string> 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<Entry> 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 <mtx/requests.hpp>
#include <mtx/responses/login.hpp>
+#include "AliasEditModel.h"
#include "BlurhashProvider.h"
#include "Cache.h"
#include "Cache_p.h"
@@ -179,6 +180,13 @@ MainWindow::registerQmlTypes()
qmlRegisterType<LoginPage>("im.nheko", 1, 0, "Login");
qmlRegisterType<RegisterPage>("im.nheko", 1, 0, "Registration");
qmlRegisterType<HiddenEvents>("im.nheko", 1, 0, "HiddenEvents");
+ qmlRegisterUncreatableType<AliasEditingModel>(
+ "im.nheko",
+ 1,
+ 0,
+ "AliasEditingModel",
+ QStringLiteral("Please use editAliases to create the models"));
+
qmlRegisterUncreatableType<PowerlevelEditingModels>(
"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 <QObject>
#include <QPalette>
+#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();
|