diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0ca85577..302fad3f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -451,6 +451,8 @@ set(SRC_FILES
src/ColorImageProvider.h
src/CombinedImagePackModel.cpp
src/CombinedImagePackModel.h
+ src/CommandCompleter.cpp
+ src/CommandCompleter.h
src/CompletionModelRoles.h
src/CompletionProxyModel.cpp
src/CompletionProxyModel.h
diff --git a/resources/qml/Completer.qml b/resources/qml/Completer.qml
index c0435dff..abf37486 100644
--- a/resources/qml/Completer.qml
+++ b/resources/qml/Completer.qml
@@ -210,6 +210,30 @@ Control {
}
DelegateChoice {
+ roleValue: "command"
+
+ RowLayout {
+ id: del
+
+ anchors.centerIn: parent
+ spacing: rowSpacing
+
+ Label {
+ text: model.name
+ color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.text
+ font.bold: true
+ }
+
+ Label {
+ text: model.description
+ color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.buttonText
+ }
+
+ }
+
+ }
+
+ DelegateChoice {
roleValue: "customEmoji"
RowLayout {
diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml
index 0800260f..7f5f63ec 100644
--- a/resources/qml/MessageInput.qml
+++ b/resources/qml/MessageInput.qml
@@ -168,6 +168,8 @@ Rectangle {
messageInput.openCompleter(selectionStart-1, "roomAliases");
} else if (lastChar == "~") {
messageInput.openCompleter(selectionStart-1, "customEmoji");
+ } else if (lastChar == "/" && cursorPosition == 1) {
+ messageInput.openCompleter(selectionStart-1, "command");
}
}
onCursorPositionChanged: {
diff --git a/src/CommandCompleter.cpp b/src/CommandCompleter.cpp
new file mode 100644
index 00000000..96dfeace
--- /dev/null
+++ b/src/CommandCompleter.cpp
@@ -0,0 +1,222 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+// SPDX-FileCopyrightText: 2022 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "CommandCompleter.h"
+
+#include "CompletionModelRoles.h"
+
+CommandCompleter::CommandCompleter(QObject *parent)
+ : QAbstractListModel(parent)
+{
+}
+
+QHash<int, QByteArray>
+CommandCompleter::roleNames() const
+{
+ return {
+ {CompletionModel::CompletionRole, "completionRole"},
+ {CompletionModel::SearchRole, "searchRole"},
+ {CompletionModel::SearchRole2, "searchRole2"},
+ {Roles::Name, "name"},
+ {Roles::Description, "description"},
+ };
+}
+
+QVariant
+CommandCompleter::data(const QModelIndex &index, int role) const
+{
+ if (hasIndex(index.row(), index.column(), index.parent())) {
+ switch (role) {
+ case CompletionModel::CompletionRole:
+ // append space where applicable in a completion
+ switch (index.row()) {
+ case Me:
+ return QString("/me ");
+ case React:
+ return QString("/react ");
+ case Join:
+ return QString("/join ");
+ case Knock:
+ return QString("/knock ");
+ case Part:
+ return QString("/part ");
+ case Leave:
+ return QString("/leave ");
+ case Invite:
+ return QString("/invite @");
+ case Kick:
+ return QString("/kick @");
+ case Ban:
+ return QString("/ban @");
+ case Unban:
+ return QString("/unban @");
+ case Redact:
+ return QString("/redact ");
+ case Roomnick:
+ return QString("/roomnick ");
+ case Shrug:
+ return QString("/shrug");
+ case Fliptable:
+ return QString("/fliptable");
+ case Unfliptable:
+ return QString("/unfliptable");
+ case Sovietflip:
+ return QString("/sovietflip");
+ case ClearTimeline:
+ return QString("/clear-timeline");
+ case ResetState:
+ return QString("/reset-state");
+ case RotateMegolmSession:
+ return QString("/rotate-megolm-session");
+ case Md:
+ return QString("/md ");
+ case Plain:
+ return QString("/plain ");
+ case Rainbow:
+ return QString("/rainbow ");
+ case RainbowMe:
+ return QString("/rainbowme ");
+ case Notice:
+ return QString("/notice ");
+ case RainbowNotice:
+ return QString("/rainbownotice ");
+ case Goto:
+ return QString("/goto ");
+ case ConvertToDm:
+ return QString("/converttodm");
+ case ConvertToRoom:
+ return QString("/converttoroom");
+ default:
+ return {};
+ }
+ case CompletionModel::SearchRole:
+ case Qt::DisplayRole:
+ case Roles::Name:
+ switch (index.row()) {
+ case Me:
+ return tr("/me <message>");
+ case React:
+ return tr("/react <text>");
+ case Join:
+ return tr("/join (!roomid|#alias) [reason]");
+ case Knock:
+ return tr("/knock (!roomid|#alias) [reason]");
+ case Part:
+ return tr("/part [reason]");
+ case Leave:
+ return tr("/leave [reason]");
+ case Invite:
+ return tr("/invite @userid [reason]");
+ case Kick:
+ return tr("/kick @userid [reason]");
+ case Ban:
+ return tr("/ban @userid [reason]");
+ case Unban:
+ return tr("/unban @userid [reason]");
+ case Redact:
+ return tr("/redact ($eventid|@userid)");
+ case Roomnick:
+ return tr("/roomnick <displayname>");
+ case Shrug:
+ return tr("/shrug [message]");
+ case Fliptable:
+ return tr("/fliptable");
+ case Unfliptable:
+ return tr("/unfliptable");
+ case Sovietflip:
+ return tr("/sovietflip");
+ case ClearTimeline:
+ return tr("/clear-timeline");
+ case ResetState:
+ return tr("/reset-state");
+ case RotateMegolmSession:
+ return tr("/rotate-megolm-session");
+ case Md:
+ return tr("/md [message]");
+ case Plain:
+ return tr("/plain [message]");
+ case Rainbow:
+ return tr("/rainbow [message]");
+ case RainbowMe:
+ return tr("/rainbowme [message]");
+ case Notice:
+ return tr("/notice [message]");
+ case RainbowNotice:
+ return tr("/rainbownotice [message]");
+ case Goto:
+ return tr("/goto ($eventid|message index|matrix:r/room/e/event)");
+ case ConvertToDm:
+ return tr("/converttodm");
+ case ConvertToRoom:
+ return tr("/converttoroom");
+ default:
+ return {};
+ }
+ case CompletionModel::SearchRole2:
+ case Roles::Description:
+ switch (index.row()) {
+ case Me:
+ return tr("Send a message expressing an action.");
+ case React:
+ return tr("Send <text> as a reaction when you’re replying to a message.");
+ case Join:
+ return tr("Join a room. Reason is optional.");
+ case Knock:
+ return tr("Ask to join a room. Reason is optional.");
+ case Part:
+ return tr("Leave a room. Reason is optional.");
+ case Leave:
+ return tr("Leave a room. Reason is optional.");
+ case Invite:
+ return tr("Invite a user into the current room. Reason is optional.");
+ case Kick:
+ return tr("Kick a user from the current room. Reason is optional.");
+ case Ban:
+ return tr("Ban a user from the current room. Reason is optional.");
+ case Unban:
+ return tr("Unban a user in the current room. Reason is optional.");
+ case Redact:
+ return tr("Redact an event or all locally cached messages of a user.");
+ case Roomnick:
+ return tr("Change your displayname in this room.");
+ case Shrug:
+ return tr("¯\\_(ツ)_/¯ with an optional message.");
+ case Fliptable:
+ return tr("(╯°□°)╯︵ ┻━┻");
+ case Unfliptable:
+ return tr("┯━┯╭( º _ º╭)");
+ case Sovietflip:
+ return tr("ノ┬─┬ノ ︵ ( \\o°o)\\");
+ case ClearTimeline:
+ return tr("Clear the currently cached messages in this room.");
+ case ResetState:
+ return tr("Refetch the state in this room.");
+ case RotateMegolmSession:
+ return tr("Rotate the current symmetric encryption key.");
+ case Md:
+ return tr("Send a markdown formatted message (ignoring the global setting).");
+ case Plain:
+ return tr("Send an unformatted message (ignoring the global setting).");
+ case Rainbow:
+ return tr("Send a message in rainbow colors.");
+ case RainbowMe:
+ return tr("Send /me in rainbow colors.");
+ case Notice:
+ return tr("Send a bot message.");
+ case RainbowNotice:
+ return tr("Send a bot message in rainbow colors.");
+ case Goto:
+ return tr("Go to this event or link.");
+ case ConvertToDm:
+ return tr("Convert this room to a direct chat.");
+ case ConvertToRoom:
+ return tr("Convert this direct chat into a room.");
+ default:
+ return {};
+ }
+ }
+ }
+ return {};
+}
diff --git a/src/CommandCompleter.h b/src/CommandCompleter.h
new file mode 100644
index 00000000..360bff73
--- /dev/null
+++ b/src/CommandCompleter.h
@@ -0,0 +1,61 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+// SPDX-FileCopyrightText: 2022 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <QAbstractListModel>
+#include <QString>
+
+class CommandCompleter final : public QAbstractListModel
+{
+public:
+ enum Roles
+ {
+ Name = Qt::UserRole,
+ Description,
+ };
+
+ enum Commands
+ {
+ Me,
+ React,
+ Join,
+ Knock,
+ Part,
+ Leave,
+ Invite,
+ Kick,
+ Ban,
+ Unban,
+ Redact,
+ Roomnick,
+ Shrug,
+ Fliptable,
+ Unfliptable,
+ Sovietflip,
+ ClearTimeline,
+ ResetState,
+ RotateMegolmSession,
+ Md,
+ Plain,
+ Rainbow,
+ RainbowMe,
+ Notice,
+ RainbowNotice,
+ Goto,
+ ConvertToDm,
+ ConvertToRoom,
+ COUNT,
+ };
+
+ CommandCompleter(QObject *parent = nullptr);
+ QHash<int, QByteArray> roleNames() const override;
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override
+ {
+ (void)parent;
+ return (int)Commands::COUNT;
+ }
+ QVariant data(const QModelIndex &index, int role) const override;
+};
diff --git a/src/RoomsModel.cpp b/src/RoomsModel.cpp
index 8abcb32e..3d13ef8b 100644
--- a/src/RoomsModel.cpp
+++ b/src/RoomsModel.cpp
@@ -7,6 +7,7 @@
#include <QUrl>
+#include "Cache.h"
#include "Cache_p.h"
#include "CompletionModelRoles.h"
#include "UserSettingsPage.h"
diff --git a/src/RoomsModel.h b/src/RoomsModel.h
index 8571e4bb..0b7371db 100644
--- a/src/RoomsModel.h
+++ b/src/RoomsModel.h
@@ -5,7 +5,7 @@
#pragma once
-#include "Cache.h"
+#include "CacheStructs.h"
#include <QAbstractListModel>
#include <QString>
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 9c46d201..ed86414d 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -12,6 +12,7 @@
#include "ChatPage.h"
#include "CombinedImagePackModel.h"
+#include "CommandCompleter.h"
#include "CompletionProxyModel.h"
#include "EventAccessors.h"
#include "ImagePackListModel.h"
@@ -443,6 +444,11 @@ TimelineViewManager::completerFor(const QString &completerName, const QString &r
auto proxy = new CompletionProxyModel(stickerModel);
stickerModel->setParent(proxy);
return proxy;
+ } else if (completerName == QLatin1String("command")) {
+ static auto commandCompleter = new CommandCompleter();
+ auto proxy = new CompletionProxyModel(commandCompleter);
+ commandCompleter->setParent(proxy);
+ return proxy;
}
return nullptr;
}
|