diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml
index 6029a31d..6ddbb32e 100644
--- a/resources/qml/MessageInput.qml
+++ b/resources/qml/MessageInput.qml
@@ -14,6 +14,8 @@ import im.nheko 1.0
Rectangle {
id: inputBar
+ readonly property string text: messageInput.text
+
color: Nheko.colors.window
Layout.fillWidth: true
Layout.preferredHeight: row.implicitHeight
diff --git a/resources/qml/MessageInputWarning.qml b/resources/qml/MessageInputWarning.qml
new file mode 100644
index 00000000..9b0b0907
--- /dev/null
+++ b/resources/qml/MessageInputWarning.qml
@@ -0,0 +1,47 @@
+// SPDX-FileCopyrightText: Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import QtQuick 2.9
+import QtQuick.Controls 2.3
+import QtQuick.Layouts 1.2
+import im.nheko 1.0
+
+Rectangle {
+ id: warningRoot
+
+ required property string text
+ property color bubbleColor: Nheko.theme.error
+
+ implicitHeight: visible ? warningDisplay.implicitHeight + 4 * Nheko.paddingSmall : 0
+ height: implicitHeight
+ Layout.fillWidth: true
+ color: Nheko.colors.window // required to hide the timeline behind this warning
+
+ Rectangle {
+ id: warningRect
+
+ visible: warningRoot.visible
+ // TODO: Qt.alpha() would make more sense but it wasn't working...
+ color: Qt.rgba(bubbleColor.r, bubbleColor.g, bubbleColor.b, 0.3)
+ border.width: 1
+ border.color: bubbleColor
+ radius: 3
+ anchors.fill: parent
+ anchors.margins: visible ? Nheko.paddingSmall : 0
+ z: 3
+
+ Label {
+ id: warningDisplay
+
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.margins: Nheko.paddingSmall
+ color: Nheko.colors.text
+ text: warningRoot.text
+ textFormat: Text.PlainText
+ }
+
+ }
+
+}
diff --git a/resources/qml/NotificationWarning.qml b/resources/qml/NotificationWarning.qml
deleted file mode 100644
index cc318843..00000000
--- a/resources/qml/NotificationWarning.qml
+++ /dev/null
@@ -1,38 +0,0 @@
-// SPDX-FileCopyrightText: Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-import QtQuick 2.9
-import QtQuick.Controls 2.3
-import QtQuick.Layouts 1.2
-import im.nheko 1.0
-
-Item {
- implicitHeight: warningRect.visible ? warningDisplay.implicitHeight : 0
- height: implicitHeight
- Layout.fillWidth: true
-
- Rectangle {
- id: warningRect
-
- visible: (room && room.permissions.canPingRoom() && room.input.containsAtRoom)
- color: Nheko.colors.base
- anchors.fill: parent
- z: 3
-
- Label {
- id: warningDisplay
-
- anchors.left: parent.left
- anchors.leftMargin: 10
- anchors.right: parent.right
- anchors.rightMargin: 10
- anchors.bottom: parent.bottom
- color: Nheko.theme.red
- text: qsTr("You are about to notify the whole room")
- textFormat: Text.PlainText
- }
-
- }
-
-}
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index c7b554b8..70347009 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -153,7 +153,20 @@ Item {
UploadBox {
}
- NotificationWarning {
+ MessageInputWarning {
+ text: qsTr("You are about to notify the whole room")
+ visible: (room && room.permissions.canPingRoom() && room.input.containsAtRoom)
+ }
+
+ 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 && !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/resources/res.qrc b/resources/res.qrc
index 9c7d0c87..faa90495 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -122,7 +122,7 @@
qml/ForwardCompleter.qml
qml/SelfVerificationCheck.qml
qml/TypingIndicator.qml
- qml/NotificationWarning.qml
+ qml/MessageInputWarning.qml
qml/components/AdaptiveLayout.qml
qml/components/AdaptiveLayoutElement.qml
qml/components/AvatarListTile.qml
diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index 7d964bb5..b27128e0 100644
--- a/src/timeline/InputBar.cpp
+++ b/src/timeline/InputBar.cpp
@@ -224,8 +224,9 @@ InputBar::insertMimeData(const QMimeData *md)
}
void
-InputBar::updateAtRoom(const QString &t)
+InputBar::updateTextContentProperties(const QString &t)
{
+ // check for @room
bool roomMention = false;
if (t.size() > 4) {
@@ -249,6 +250,61 @@ InputBar::updateAtRoom(const QString &t)
this->containsAtRoom_ = roomMention;
emit containsAtRoomChanged();
}
+
+ // check for invalid commands
+ auto commandName = getCommandAndArgs(t).first;
+ static const QSet validCommands{QStringLiteral("me"),
+ QStringLiteral("react"),
+ QStringLiteral("join"),
+ QStringLiteral("knock"),
+ QStringLiteral("part"),
+ QStringLiteral("leave"),
+ QStringLiteral("invite"),
+ QStringLiteral("kick"),
+ QStringLiteral("ban"),
+ QStringLiteral("unban"),
+ QStringLiteral("redact"),
+ QStringLiteral("roomnick"),
+ QStringLiteral("shrug"),
+ QStringLiteral("fliptable"),
+ QStringLiteral("unfliptable"),
+ QStringLiteral("sovietflip"),
+ QStringLiteral("clear-timeline"),
+ QStringLiteral("reset-state"),
+ QStringLiteral("rotate-megolm-session"),
+ QStringLiteral("md"),
+ QStringLiteral("cmark"),
+ QStringLiteral("plain"),
+ QStringLiteral("rainbow"),
+ QStringLiteral("rainbowme"),
+ QStringLiteral("notice"),
+ QStringLiteral("rainbownotice"),
+ QStringLiteral("confetti"),
+ QStringLiteral("rainbowconfetti"),
+ QStringLiteral("goto"),
+ QStringLiteral("converttodm"),
+ QStringLiteral("converttoroom")};
+ 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;
+ }
+ if (signalsChanged) {
+ emit currentCommandChanged();
+ emit containsInvalidCommandChanged();
+ emit containsIncompleteCommandChanged();
+ }
}
void
@@ -263,7 +319,7 @@ InputBar::setText(const QString &newText)
if (history_.size() == INPUT_HISTORY_SIZE)
history_.pop_back();
- updateAtRoom(QLatin1String(""));
+ updateTextContentProperties(QLatin1String(""));
emit textChanged(newText);
}
void
@@ -284,7 +340,7 @@ InputBar::updateState(int selectionStart_,
history_.front() = text_;
history_index_ = 0;
- updateAtRoom(text_);
+ updateTextContentProperties(text_);
// disabled, as it moves the cursor to the end
// emit textChanged(text_);
}
@@ -312,7 +368,7 @@ InputBar::previousText()
else if (text().isEmpty())
history_index_--;
- updateAtRoom(text());
+ updateTextContentProperties(text());
return text();
}
@@ -323,7 +379,7 @@ InputBar::nextText()
if (history_index_ >= INPUT_HISTORY_SIZE)
history_index_ = 0;
- updateAtRoom(text());
+ updateTextContentProperties(text());
return text();
}
@@ -341,20 +397,12 @@ InputBar::send()
auto wasEdit = !room->edit().isEmpty();
- if (text().startsWith('/')) {
- int command_end = text().indexOf(QRegularExpression(QStringLiteral("\\s")));
- if (command_end == -1)
- command_end = text().size();
- auto name = text().mid(1, command_end - 1);
- auto args = text().mid(command_end + 1);
- if (name.isEmpty() || name == QLatin1String("/")) {
- message(args);
- } else {
- command(name, args);
- }
- } else {
+ auto [commandName, args] = getCommandAndArgs();
+ updateTextContentProperties(text());
+ if (containsIncompleteCommand_)
+ return;
+ if (commandName.isEmpty() || !command(commandName, args))
message(text());
- }
if (!wasEdit) {
history_.push_front(QLatin1String(""));
@@ -716,6 +764,24 @@ InputBar::video(const QString &filename,
room->sendMessageEvent(video, mtx::events::EventType::RoomMessage);
}
+QPair
+InputBar::getCommandAndArgs(const QString ¤tText) const
+{
+ if (!currentText.startsWith('/'))
+ return {{}, currentText};
+
+ int command_end = currentText.indexOf(QRegularExpression(QStringLiteral("\\s")));
+ if (command_end == -1)
+ command_end = currentText.size();
+ auto name = currentText.mid(1, command_end - 1);
+ auto args = currentText.mid(command_end + 1);
+ if (name.isEmpty() || name == QLatin1String("/")) {
+ return {{}, currentText};
+ } else {
+ return {name, args};
+ }
+}
+
void
InputBar::sticker(CombinedImagePackModel *model, int row)
{
@@ -741,7 +807,7 @@ InputBar::sticker(CombinedImagePackModel *model, int row)
room->sendMessageEvent(sticker, mtx::events::EventType::Sticker);
}
-void
+bool
InputBar::command(const QString &command, QString args)
{
if (command == QLatin1String("me")) {
@@ -829,16 +895,16 @@ InputBar::command(const QString &command, QString args)
// 1 - Going directly to a given event ID
if (args[0] == '$') {
room->showEvent(args);
- return;
+ return true;
}
// 2 - Going directly to a given message index
if (args[0] >= '0' && args[0] <= '9') {
room->showEvent(args);
- return;
+ return true;
}
// 3 - Matrix URI handler, as if you clicked the URI
if (ChatPage::instance()->handleMatrixUri(args)) {
- return;
+ return true;
}
nhlog::net()->error("Could not resolve goto: {}", args.toStdString());
} else if (command == QLatin1String("converttodm")) {
@@ -846,7 +912,11 @@ InputBar::command(const QString &command, QString args)
cache::getMembers(this->room->roomId().toStdString(), 0, -1));
} else if (command == QLatin1String("converttoroom")) {
utils::removeDirectFromRoom(this->room->roomId());
+ } else {
+ return false;
}
+
+ return true;
}
MediaUpload::MediaUpload(std::unique_ptr source_,
diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h
index 80ad7f47..acafd964 100644
--- a/src/timeline/InputBar.h
+++ b/src/timeline/InputBar.h
@@ -173,6 +173,11 @@ class InputBar final : public QObject
Q_OBJECT
Q_PROPERTY(bool uploading READ uploading NOTIFY uploadingChanged)
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)
@@ -198,6 +203,9 @@ public slots:
void setText(const QString &newText);
[[nodiscard]] bool containsAtRoom() const { return containsAtRoom_; }
+ bool containsInvalidCommand() const { return containsInvalidCommand_; }
+ bool containsIncompleteCommand() const { return containsIncompleteCommand_; }
+ QString currentCommand() const { return currentCommand_; }
void send();
bool tryPasteAttachment(bool fromMouse);
@@ -225,13 +233,16 @@ signals:
void textChanged(QString newText);
void uploadingChanged(bool value);
void containsAtRoomChanged();
+ void containsInvalidCommandChanged();
+ void containsIncompleteCommandChanged();
+ void currentCommandChanged();
void uploadsChanged();
private:
void emote(const QString &body, bool rainbowify);
void notice(const QString &body, bool rainbowify);
void confetti(const QString &body, bool rainbowify);
- void command(const QString &name, QString args);
+ bool command(const QString &name, QString args);
void image(const QString &filename,
const std::optional &file,
const QString &url,
@@ -267,6 +278,8 @@ private:
const QSize &thumbnailDimensions,
const QString &blurhash);
+ QPair getCommandAndArgs() const { return getCommandAndArgs(text()); }
+ QPair getCommandAndArgs(const QString ¤tText) const;
mtx::common::Relations generateRelations() const;
void startUploadFromPath(const QString &path);
@@ -280,7 +293,7 @@ private:
}
}
- void updateAtRoom(const QString &t);
+ void updateTextContentProperties(const QString &t);
QTimer typingRefresh_;
QTimer typingTimeout_;
@@ -288,8 +301,11 @@ private:
std::deque history_;
std::size_t history_index_ = 0;
int selectionStart = 0, selectionEnd = 0, cursorPosition = 0;
- bool uploading_ = false;
- bool containsAtRoom_ = false;
+ bool uploading_ = false;
+ bool containsAtRoom_ = false;
+ bool containsInvalidCommand_ = false;
+ bool containsIncompleteCommand_ = false;
+ QString currentCommand_;
using UploadHandle = std::unique_ptr;
std::vector unconfirmedUploads;