summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt4
-rw-r--r--io.github.NhekoReborn.Nheko.yaml2
-rw-r--r--resources/icons/ui/building-shop.svg1
-rw-r--r--resources/icons/ui/room-directory.svg1
-rw-r--r--resources/qml/RoomList.qml2
-rw-r--r--resources/qml/Root.qml27
-rw-r--r--resources/qml/dialogs/AliasEditor.qml172
-rw-r--r--resources/qml/dialogs/RoomSettings.qml12
-rw-r--r--resources/res.qrc3
-rw-r--r--src/AliasEditModel.cpp336
-rw-r--r--src/AliasEditModel.h79
-rw-r--r--src/MainWindow.cpp8
-rw-r--r--src/ui/NhekoGlobalObject.h5
13 files changed, 643 insertions, 9 deletions
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 @@
+<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M18 2a.75.75 0 0 1 .474.169l.076.07 3.272 3.53.03.038c.102.136.148.29.148.44L22 8.168c0 .994-.379 1.9-1 2.58V21.25a.75.75 0 0 1-.649.743L20.25 22H3.75a.75.75 0 0 1-.743-.648l-.007-.102V10.748a3.818 3.818 0 0 1-.993-2.353L2 8.167V6.29a.728.728 0 0 1 .096-.408l.065-.095.04-.046L5.45 2.24a.75.75 0 0 1 .447-.233L6 2h12Zm-2.918 8.442-.012.018A3.827 3.827 0 0 1 11.999 12a3.827 3.827 0 0 1-3.083-1.556A3.825 3.825 0 0 1 5.834 12c-.47 0-.919-.084-1.334-.238v8.738H6v-6.748a.75.75 0 0 1 .648-.743L6.75 13h4.496a.75.75 0 0 1 .743.648l.007.102v6.748h7.502v-8.737a3.827 3.827 0 0 1-4.416-1.32Zm-4.587 4.059H7.5v5.998h2.995v-5.998Zm6.76-1.5a.75.75 0 0 1 .743.648l.007.102v3.502a.75.75 0 0 1-.649.743l-.101.007h-3.502a.75.75 0 0 1-.743-.648l-.007-.102v-3.502a.75.75 0 0 1 .648-.743l.102-.007h3.502Zm-.751 1.5h-2.001v2.002h2v-2.002ZM8.166 7.002H3.5v1.165l.006.17.029.232.032.156.05.172.054.148.04.094c.032.068.066.134.104.198l.102.162.055.074.129.156.141.144.097.085.042.034c.314.25.695.422 1.111.483l.18.019.16.005c1.235 0 2.246-.959 2.328-2.173l.005-.16V7.003Zm6.165 0H9.666v1.165c0 1.18.878 2.157 2.016 2.311l.157.016.16.005c1.234 0 2.245-.959 2.327-2.173l.005-.16V7.003Zm6.167 0h-4.665v1.165c0 1.18.878 2.157 2.017 2.311l.156.016.16.005c.564 0 1.082-.2 1.485-.534l.09-.078.116-.113.146-.17c.054-.069.105-.14.15-.216l.104-.186.063-.138.058-.155.03-.096.038-.152.029-.157.018-.167.006-.17-.001-1.165ZM9.062 3.499H6.327L4.469 5.502h3.977l.616-2.003Zm4.306 0H10.63l-.616 2.003h3.97l-.616-2.003Zm4.304 0h-2.735l.617 2.003h3.976l-1.858-2.003Z" fill="#212121"/></svg>
\ 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 @@
+<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M8.5 5.5a1 1 0 1 0 0 2 1 1 0 0 0 0-2ZM7.5 13.5a1 1 0 1 1 2 0 1 1 0 0 1-2 0ZM8.5 9a1 1 0 1 0 0 2 1 1 0 0 0 0-2ZM11 6.5a1 1 0 1 1 2 0 1 1 0 0 1-2 0ZM12 12.5a1 1 0 1 0 0 2 1 1 0 0 0 0-2ZM14.5 13.5a1 1 0 1 1 2 0 1 1 0 0 1-2 0ZM12 9a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z" fill="#212121"/><path d="M6.25 2A2.25 2.25 0 0 0 4 4.25v16.5c0 .414.336.75.75.75h14.503a.75.75 0 0 0 .75-.75v-9a2.25 2.25 0 0 0-2.25-2.25H16.5V4.25A2.25 2.25 0 0 0 14.25 2h-8ZM5.5 4.25a.75.75 0 0 1 .75-.75h8a.75.75 0 0 1 .75.75v6c0 .414.336.75.75.75h2.003a.75.75 0 0 1 .75.75V20H16.5v-2.75a.75.75 0 0 0-.75-.75h-7.5a.75.75 0 0 0-.75.75V20h-2V4.25ZM15 18v2h-2.25v-2H15Zm-3.75 0v2H9v-2h2.25Z" fill="#212121"/></svg>
\ 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
@@ -353,6 +353,18 @@ ApplicationWindow {
                 }
 
                 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 @@
         <file>icons/ui/angle-arrow-left.svg</file>
         <file>icons/ui/attach.svg</file>
         <file>icons/ui/ban.svg</file>
+        <file>icons/ui/building-shop.svg</file>
         <file>icons/ui/chat.svg</file>
         <file>icons/ui/checkmark.svg</file>
         <file>icons/ui/clock.svg</file>
@@ -36,6 +37,7 @@
         <file>icons/ui/reply.svg</file>
         <file>icons/ui/ribbon.svg</file>
         <file>icons/ui/ribbon_star.svg</file>
+        <file>icons/ui/room-directory.svg</file>
         <file>icons/ui/round-remove-button.svg</file>
         <file>icons/ui/screen-share.svg</file>
         <file>icons/ui/search.svg</file>
@@ -147,6 +149,7 @@
         <file>qml/device-verification/NewVerificationRequest.qml</file>
         <file>qml/device-verification/Success.qml</file>
         <file>qml/device-verification/Waiting.qml</file>
+        <file>qml/dialogs/AliasEditor.qml</file>
         <file>qml/dialogs/CreateDirect.qml</file>
         <file>qml/dialogs/CreateRoom.qml</file>
         <file>qml/dialogs/HiddenEventsDialog.qml</file>
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();