From b266185ce83c81f0c120a9083867817a354107c2 Mon Sep 17 00:00:00 2001
From: Loren Burkholder <computersemiexpert@outlook.com>
Date: Tue, 7 Mar 2023 19:10:42 -0500
Subject: [PATCH] Handle incomplete commands better

---
 resources/qml/MessageInputWarning.qml |  5 +++--
 resources/qml/TimelineView.qml        |  8 +++++++-
 src/timeline/InputBar.cpp             | 22 ++++++++++++++--------
 src/timeline/InputBar.h               | 14 ++++++++++----
 4 files changed, 34 insertions(+), 15 deletions(-)

diff --git a/resources/qml/MessageInputWarning.qml b/resources/qml/MessageInputWarning.qml
index af65ebc1b..9b0b09074 100644
--- a/resources/qml/MessageInputWarning.qml
+++ b/resources/qml/MessageInputWarning.qml
@@ -11,6 +11,7 @@ Rectangle {
     id: warningRoot
 
     required property string text
+    property color bubbleColor: Nheko.theme.error
 
     implicitHeight: visible ? warningDisplay.implicitHeight + 4 * Nheko.paddingSmall : 0
     height: implicitHeight
@@ -22,9 +23,9 @@ Rectangle {
 
         visible: warningRoot.visible
         // TODO: Qt.alpha() would make more sense but it wasn't working...
-        color: Qt.rgba(Nheko.theme.error.r, Nheko.theme.error.g, Nheko.theme.error.b, 0.3)
+        color: Qt.rgba(bubbleColor.r, bubbleColor.g, bubbleColor.b, 0.3)
         border.width: 1
-        border.color: Nheko.theme.error
+        border.color: bubbleColor
         radius: 3
         anchors.fill: parent
         anchors.margins: visible ? Nheko.paddingSmall : 0
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index f0e71c602..abda16b92 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -160,7 +160,13 @@ Item {
 
         MessageInputWarning {
             text: qsTr("The command /%1 is not recognized and will be sent as part of your message").arg(room ? room.input.currentCommand : "")
-            visible: room ? room.input.containsInvalidCommand : false
+            visible: room ? room.input.containsInvalidCommand && !room.input.containsIncompleteCommand : false
+        }
+
+        MessageInputWarning {
+            text: qsTr("/%1 looks like an incomplete command. To send it anyway, add a space to the end of your message.").arg(room ? room.input.currentCommand : "")
+            visible: room ? room.input.containsIncompleteCommand : false
+            bubbleColor: Nheko.theme.orange
         }
 
         ReplyPopup {
diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index fb61ce0d6..b27128e0d 100644
--- a/src/timeline/InputBar.cpp
+++ b/src/timeline/InputBar.cpp
@@ -252,7 +252,7 @@ InputBar::updateTextContentProperties(const QString &t)
     }
 
     // check for invalid commands
-    auto commandName = getCommandAndArgs().first;
+    auto commandName = getCommandAndArgs(t).first;
     static const QSet<QString> validCommands{QStringLiteral("me"),
                                              QStringLiteral("react"),
                                              QStringLiteral("join"),
@@ -284,14 +284,18 @@ InputBar::updateTextContentProperties(const QString &t)
                                              QStringLiteral("goto"),
                                              QStringLiteral("converttodm"),
                                              QStringLiteral("converttoroom")};
-    bool hasInvalidCommand =
-      !commandName.isNull() && '/' + commandName != text() && !validCommands.contains(commandName);
+    bool hasInvalidCommand    = !commandName.isNull() && !validCommands.contains(commandName);
+    bool hasIncompleteCommand = hasInvalidCommand && '/' + commandName == t;
 
     bool signalsChanged{false};
     if (containsInvalidCommand_ != hasInvalidCommand) {
         containsInvalidCommand_ = hasInvalidCommand;
         signalsChanged          = true;
     }
+    if (containsIncompleteCommand_ != hasIncompleteCommand) {
+        containsIncompleteCommand_ = hasIncompleteCommand;
+        signalsChanged             = true;
+    }
     if (currentCommand_ != commandName) {
         currentCommand_ = commandName;
         signalsChanged  = true;
@@ -299,6 +303,7 @@ InputBar::updateTextContentProperties(const QString &t)
     if (signalsChanged) {
         emit currentCommandChanged();
         emit containsInvalidCommandChanged();
+        emit containsIncompleteCommandChanged();
     }
 }
 
@@ -392,9 +397,11 @@ InputBar::send()
 
     auto wasEdit = !room->edit().isEmpty();
 
-    if (auto [commandName, args] = getCommandAndArgs(); commandName.isEmpty())
-        message(text());
-    else if (!command(commandName, args))
+    auto [commandName, args] = getCommandAndArgs();
+    updateTextContentProperties(text());
+    if (containsIncompleteCommand_)
+        return;
+    if (commandName.isEmpty() || !command(commandName, args))
         message(text());
 
     if (!wasEdit) {
@@ -758,9 +765,8 @@ InputBar::video(const QString &filename,
 }
 
 QPair<QString, QString>
-InputBar::getCommandAndArgs() const
+InputBar::getCommandAndArgs(const QString &currentText) const
 {
-    const auto currentText = text();
     if (!currentText.startsWith('/'))
         return {{}, currentText};
 
diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h
index 94aedaf60..acafd964d 100644
--- a/src/timeline/InputBar.h
+++ b/src/timeline/InputBar.h
@@ -175,6 +175,8 @@ class InputBar final : public QObject
     Q_PROPERTY(bool containsAtRoom READ containsAtRoom NOTIFY containsAtRoomChanged)
     Q_PROPERTY(
       bool containsInvalidCommand READ containsInvalidCommand NOTIFY containsInvalidCommandChanged)
+    Q_PROPERTY(bool containsIncompleteCommand READ containsIncompleteCommand NOTIFY
+                 containsIncompleteCommandChanged)
     Q_PROPERTY(QString currentCommand READ currentCommand NOTIFY currentCommandChanged)
     Q_PROPERTY(QString text READ text NOTIFY textChanged)
     Q_PROPERTY(QVariantList uploads READ uploads NOTIFY uploadsChanged)
@@ -202,6 +204,7 @@ public slots:
 
     [[nodiscard]] bool containsAtRoom() const { return containsAtRoom_; }
     bool containsInvalidCommand() const { return containsInvalidCommand_; }
+    bool containsIncompleteCommand() const { return containsIncompleteCommand_; }
     QString currentCommand() const { return currentCommand_; }
 
     void send();
@@ -231,6 +234,7 @@ signals:
     void uploadingChanged(bool value);
     void containsAtRoomChanged();
     void containsInvalidCommandChanged();
+    void containsIncompleteCommandChanged();
     void currentCommandChanged();
     void uploadsChanged();
 
@@ -274,7 +278,8 @@ private:
                const QSize &thumbnailDimensions,
                const QString &blurhash);
 
-    QPair<QString, QString> getCommandAndArgs() const;
+    QPair<QString, QString> getCommandAndArgs() const { return getCommandAndArgs(text()); }
+    QPair<QString, QString> getCommandAndArgs(const QString &currentText) const;
     mtx::common::Relations generateRelations() const;
 
     void startUploadFromPath(const QString &path);
@@ -296,9 +301,10 @@ private:
     std::deque<QString> history_;
     std::size_t history_index_ = 0;
     int selectionStart = 0, selectionEnd = 0, cursorPosition = 0;
-    bool uploading_              = false;
-    bool containsAtRoom_         = false;
-    bool containsInvalidCommand_ = false;
+    bool uploading_                 = false;
+    bool containsAtRoom_            = false;
+    bool containsInvalidCommand_    = false;
+    bool containsIncompleteCommand_ = false;
     QString currentCommand_;
 
     using UploadHandle = std::unique_ptr<MediaUpload, DeleteLaterDeleter>;
-- 
GitLab