From 9043087c80dac7eea761efcfd6f289efa5bd4a94 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Wed, 22 Feb 2023 23:23:13 -0500 Subject: [PATCH 1/9] Enhance appearance of room ping warning --- resources/qml/NotificationWarning.qml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/resources/qml/NotificationWarning.qml b/resources/qml/NotificationWarning.qml index cc318843..13d2cb23 100644 --- a/resources/qml/NotificationWarning.qml +++ b/resources/qml/NotificationWarning.qml @@ -8,7 +8,7 @@ import QtQuick.Layouts 1.2 import im.nheko 1.0 Item { - implicitHeight: warningRect.visible ? warningDisplay.implicitHeight : 0 + implicitHeight: warningRect.visible ? warningDisplay.implicitHeight + 2 * Nheko.paddingSmall : 0 height: implicitHeight Layout.fillWidth: true @@ -16,7 +16,11 @@ Item { id: warningRect visible: (room && room.permissions.canPingRoom() && room.input.containsAtRoom) - color: Nheko.colors.base + // 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) + border.width: 1 + border.color: Nheko.theme.error + radius: 3 anchors.fill: parent z: 3 @@ -24,11 +28,9 @@ Item { id: warningDisplay anchors.left: parent.left - anchors.leftMargin: 10 - anchors.right: parent.right - anchors.rightMargin: 10 - anchors.bottom: parent.bottom - color: Nheko.theme.red + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Nheko.paddingSmall + color: Nheko.colors.text text: qsTr("You are about to notify the whole room") textFormat: Text.PlainText } From b6ef00b5ee14bd0adc85c3a98bb8a127f79932ea Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Thu, 23 Feb 2023 21:57:53 -0500 Subject: [PATCH 2/9] Show warning when invalid command is entered Fixes #1363 Please note that this doesn't prompt when you try to send a message with a bad command. --- resources/qml/MessageInput.qml | 2 + ...ionWarning.qml => MessageInputWarning.qml} | 12 +++- resources/qml/TimelineView.qml | 19 +++++- resources/res.qrc | 2 +- src/CompletionProxyModel.h | 2 + src/timeline/InputBar.cpp | 2 + src/ui/NhekoGlobalObject.cpp | 60 +++++++++++++++++++ src/ui/NhekoGlobalObject.h | 3 + 8 files changed, 97 insertions(+), 5 deletions(-) rename resources/qml/{NotificationWarning.qml => MessageInputWarning.qml} (73%) diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index f31123e5..f6fe03c5 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -13,6 +13,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/NotificationWarning.qml b/resources/qml/MessageInputWarning.qml similarity index 73% rename from resources/qml/NotificationWarning.qml rename to resources/qml/MessageInputWarning.qml index 13d2cb23..c2db2daa 100644 --- a/resources/qml/NotificationWarning.qml +++ b/resources/qml/MessageInputWarning.qml @@ -8,14 +8,20 @@ import QtQuick.Layouts 1.2 import im.nheko 1.0 Item { - implicitHeight: warningRect.visible ? warningDisplay.implicitHeight + 2 * Nheko.paddingSmall : 0 + id: warningRoot + + required property string text + required property bool isVisible + + implicitHeight: isVisible ? warningDisplay.implicitHeight + 2 * Nheko.paddingSmall : 0 height: implicitHeight Layout.fillWidth: true + Layout.margins: isVisible ? Nheko.paddingSmall : 0 Rectangle { id: warningRect - visible: (room && room.permissions.canPingRoom() && room.input.containsAtRoom) + visible: warningRoot.isVisible // 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) border.width: 1 @@ -31,7 +37,7 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.margins: Nheko.paddingSmall color: Nheko.colors.text - text: qsTr("You are about to notify the whole room") + text: warningRoot.text textFormat: Text.PlainText } diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index e836f60f..5c982270 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -153,13 +153,30 @@ Item { UploadBox { } - NotificationWarning { + MessageInputWarning { + text: qsTr("You are about to notify the whole room") + isVisible: (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(Nheko.getCommandFromText(input.text)) + isVisible: { + if (!input.text) + return false; + + let command = Nheko.getCommandFromText(input.text); + if (Nheko.isInvalidCommand(command) && ("/" + command !== input.text)) + return true; + else + return false; + } } ReplyPopup { } MessageInput { + id: input } } diff --git a/resources/res.qrc b/resources/res.qrc index 88159d40..5fbd00d7 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -123,7 +123,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/CompletionProxyModel.h b/src/CompletionProxyModel.h index e0f00788..90daf7ad 100644 --- a/src/CompletionProxyModel.h +++ b/src/CompletionProxyModel.h @@ -184,6 +184,8 @@ public slots: void setSearchString(const QString &s); QString searchString() const { return searchString_; } + bool hasCompletion() const { return rowCount() > 0; } + signals: void newSearchString(QString); diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index 7d964bb5..5184ba94 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -846,6 +846,8 @@ 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 { + message("/" + command + " " + args); } } diff --git a/src/ui/NhekoGlobalObject.cpp b/src/ui/NhekoGlobalObject.cpp index a6f9abe7..3cb6a8a8 100644 --- a/src/ui/NhekoGlobalObject.cpp +++ b/src/ui/NhekoGlobalObject.cpp @@ -190,3 +190,63 @@ Nheko::setWindowRole([[maybe_unused]] QWindow *win, [[maybe_unused]] QString new QXcbWindowFunctions::setWmWindowRole(win, newRole.toUtf8()); #endif } + +QString +Nheko::getCommandFromText(const QString &text) +{ + if (text.startsWith('/')) { + int command_end = text.indexOf(QRegularExpression(QStringLiteral("\\s"))); + if (command_end == -1) + command_end = text.size(); + auto command = text.mid(1, command_end - 1); + if (command.isEmpty() || command == QLatin1String("/")) + return {}; + else { + return command; + } + } else + return {}; +} + +bool +Nheko::isInvalidCommand(QString command) const +{ + if (command.size() <= 0) + return false; + + static const QStringList 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")}; + + if (!command.startsWith('/')) + command.prepend('/'); + return !validCommands.contains(command); +} diff --git a/src/ui/NhekoGlobalObject.h b/src/ui/NhekoGlobalObject.h index b7a7a637..1ea2c109 100644 --- a/src/ui/NhekoGlobalObject.h +++ b/src/ui/NhekoGlobalObject.h @@ -72,6 +72,9 @@ public: Q_INVOKABLE void setTransientParent(QWindow *window, QWindow *parentWindow) const; Q_INVOKABLE void setWindowRole(QWindow *win, QString newRole) const; + Q_INVOKABLE QString getCommandFromText(const QString &text); + Q_INVOKABLE bool isInvalidCommand(QString command) const; + public slots: void updateUserProfile(); From 22ac5d861e487ff39749f5d2bdda9113689e4b63 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Mon, 27 Feb 2023 18:06:24 -0500 Subject: [PATCH 3/9] Move command calculation logic into InputBar --- resources/qml/TimelineView.qml | 13 +---- src/timeline/InputBar.cpp | 94 +++++++++++++++++++++++++++------- src/timeline/InputBar.h | 16 ++++-- src/ui/NhekoGlobalObject.cpp | 60 ---------------------- src/ui/NhekoGlobalObject.h | 3 -- 5 files changed, 91 insertions(+), 95 deletions(-) diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 5c982270..100ed1d7 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -159,17 +159,8 @@ Item { } MessageInputWarning { - text: qsTr("The command /%1 is not recognized and will be sent as part of your message").arg(Nheko.getCommandFromText(input.text)) - isVisible: { - if (!input.text) - return false; - - let command = Nheko.getCommandFromText(input.text); - if (Nheko.isInvalidCommand(command) && ("/" + command !== input.text)) - return true; - else - return false; - } + text: qsTr("The command /%1 is not recognized and will be sent as part of your message").arg(room ? room.input.currentCommand : "") + isVisible: room ? room.input.containsInvalidCommand : false } ReplyPopup { diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index 5184ba94..6c882cd4 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,54 @@ InputBar::updateAtRoom(const QString &t) this->containsAtRoom_ = roomMention; emit containsAtRoomChanged(); } + + // check for invalid commands + auto commandName = getCommandAndArgs().first; + bool hasInvalidCommand{}; + if (!commandName.isNull() && '/' + commandName != text()) { + static const QStringList 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")}; + hasInvalidCommand = !validCommands.contains(commandName); + } else + hasInvalidCommand = false; + + if (containsInvalidCommand_ != hasInvalidCommand) { + containsInvalidCommand_ = hasInvalidCommand; + emit containsInvalidCommandChanged(); + } + if (currentCommand_ != commandName) { + currentCommand_ = commandName; + emit currentCommandChanged(); + } } void @@ -263,7 +312,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 +333,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 +361,7 @@ InputBar::previousText() else if (text().isEmpty()) history_index_--; - updateAtRoom(text()); + updateTextContentProperties(text()); return text(); } @@ -323,7 +372,7 @@ InputBar::nextText() if (history_index_ >= INPUT_HISTORY_SIZE) history_index_ = 0; - updateAtRoom(text()); + updateTextContentProperties(text()); return text(); } @@ -341,20 +390,11 @@ 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(); + if (commandName.isNull()) message(text()); - } + else + command(commandName, args); if (!wasEdit) { history_.push_front(QLatin1String("")); @@ -716,6 +756,24 @@ InputBar::video(const QString &filename, room->sendMessageEvent(video, mtx::events::EventType::RoomMessage); } +QPair +InputBar::getCommandAndArgs() const +{ + if (!text().startsWith('/')) + return {{}, text()}; + + 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("/")) { + return {{}, text()}; + } else { + return {name, args}; + } +} + void InputBar::sticker(CombinedImagePackModel *model, int row) { diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h index 80ad7f47..7385d272 100644 --- a/src/timeline/InputBar.h +++ b/src/timeline/InputBar.h @@ -173,6 +173,9 @@ 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(QString currentCommand READ currentCommand NOTIFY currentCommandChanged) Q_PROPERTY(QString text READ text NOTIFY textChanged) Q_PROPERTY(QVariantList uploads READ uploads NOTIFY uploadsChanged) @@ -198,6 +201,8 @@ public slots: void setText(const QString &newText); [[nodiscard]] bool containsAtRoom() const { return containsAtRoom_; } + bool containsInvalidCommand() const { return containsInvalidCommand_; } + QString currentCommand() const { return currentCommand_; } void send(); bool tryPasteAttachment(bool fromMouse); @@ -225,6 +230,8 @@ signals: void textChanged(QString newText); void uploadingChanged(bool value); void containsAtRoomChanged(); + void containsInvalidCommandChanged(); + void currentCommandChanged(); void uploadsChanged(); private: @@ -267,6 +274,7 @@ private: const QSize &thumbnailDimensions, const QString &blurhash); + QPair getCommandAndArgs() const; mtx::common::Relations generateRelations() const; void startUploadFromPath(const QString &path); @@ -280,7 +288,7 @@ private: } } - void updateAtRoom(const QString &t); + void updateTextContentProperties(const QString &t); QTimer typingRefresh_; QTimer typingTimeout_; @@ -288,8 +296,10 @@ 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; + QString currentCommand_; using UploadHandle = std::unique_ptr; std::vector unconfirmedUploads; diff --git a/src/ui/NhekoGlobalObject.cpp b/src/ui/NhekoGlobalObject.cpp index 3cb6a8a8..a6f9abe7 100644 --- a/src/ui/NhekoGlobalObject.cpp +++ b/src/ui/NhekoGlobalObject.cpp @@ -190,63 +190,3 @@ Nheko::setWindowRole([[maybe_unused]] QWindow *win, [[maybe_unused]] QString new QXcbWindowFunctions::setWmWindowRole(win, newRole.toUtf8()); #endif } - -QString -Nheko::getCommandFromText(const QString &text) -{ - if (text.startsWith('/')) { - int command_end = text.indexOf(QRegularExpression(QStringLiteral("\\s"))); - if (command_end == -1) - command_end = text.size(); - auto command = text.mid(1, command_end - 1); - if (command.isEmpty() || command == QLatin1String("/")) - return {}; - else { - return command; - } - } else - return {}; -} - -bool -Nheko::isInvalidCommand(QString command) const -{ - if (command.size() <= 0) - return false; - - static const QStringList 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")}; - - if (!command.startsWith('/')) - command.prepend('/'); - return !validCommands.contains(command); -} diff --git a/src/ui/NhekoGlobalObject.h b/src/ui/NhekoGlobalObject.h index 1ea2c109..b7a7a637 100644 --- a/src/ui/NhekoGlobalObject.h +++ b/src/ui/NhekoGlobalObject.h @@ -72,9 +72,6 @@ public: Q_INVOKABLE void setTransientParent(QWindow *window, QWindow *parentWindow) const; Q_INVOKABLE void setWindowRole(QWindow *win, QString newRole) const; - Q_INVOKABLE QString getCommandFromText(const QString &text); - Q_INVOKABLE bool isInvalidCommand(QString command) const; - public slots: void updateUserProfile(); From b4f8d4947ff517ae1498d5787ebb34ca3803ad13 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Mon, 27 Feb 2023 18:12:19 -0500 Subject: [PATCH 4/9] Simplify code a bit --- resources/qml/TimelineView.qml | 1 - src/CompletionProxyModel.h | 2 -- src/timeline/InputBar.cpp | 3 +-- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 100ed1d7..cc615fd3 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -167,7 +167,6 @@ Item { } MessageInput { - id: input } } diff --git a/src/CompletionProxyModel.h b/src/CompletionProxyModel.h index 90daf7ad..e0f00788 100644 --- a/src/CompletionProxyModel.h +++ b/src/CompletionProxyModel.h @@ -184,8 +184,6 @@ public slots: void setSearchString(const QString &s); QString searchString() const { return searchString_; } - bool hasCompletion() const { return rowCount() > 0; } - signals: void newSearchString(QString); diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index 6c882cd4..2491b5c1 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -390,8 +390,7 @@ InputBar::send() auto wasEdit = !room->edit().isEmpty(); - auto [commandName, args] = getCommandAndArgs(); - if (commandName.isNull()) + if (auto [commandName, args] = getCommandAndArgs(); commandName.isNull()) message(text()); else command(commandName, args); From c27407bfab9160266162864f71f2bca27b87a9e9 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Mon, 27 Feb 2023 18:33:27 -0500 Subject: [PATCH 5/9] Don't show timeline behind warnings --- resources/qml/MessageInputWarning.qml | 10 +++++----- resources/qml/TimelineView.qml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/resources/qml/MessageInputWarning.qml b/resources/qml/MessageInputWarning.qml index c2db2daa..af65ebc1 100644 --- a/resources/qml/MessageInputWarning.qml +++ b/resources/qml/MessageInputWarning.qml @@ -7,27 +7,27 @@ import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 import im.nheko 1.0 -Item { +Rectangle { id: warningRoot required property string text - required property bool isVisible - implicitHeight: isVisible ? warningDisplay.implicitHeight + 2 * Nheko.paddingSmall : 0 + implicitHeight: visible ? warningDisplay.implicitHeight + 4 * Nheko.paddingSmall : 0 height: implicitHeight Layout.fillWidth: true - Layout.margins: isVisible ? Nheko.paddingSmall : 0 + color: Nheko.colors.window // required to hide the timeline behind this warning Rectangle { id: warningRect - visible: warningRoot.isVisible + 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) border.width: 1 border.color: Nheko.theme.error radius: 3 anchors.fill: parent + anchors.margins: visible ? Nheko.paddingSmall : 0 z: 3 Label { diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index cc615fd3..f0e71c60 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -155,12 +155,12 @@ Item { MessageInputWarning { text: qsTr("You are about to notify the whole room") - isVisible: (room && room.permissions.canPingRoom() && room.input.containsAtRoom) + 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 : "") - isVisible: room ? room.input.containsInvalidCommand : false + visible: room ? room.input.containsInvalidCommand : false } ReplyPopup { From ec8820ac646ee20547ded7c9783bc5bbe602f92e Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Wed, 1 Mar 2023 17:04:17 -0500 Subject: [PATCH 6/9] Fix some nitpicks --- src/timeline/InputBar.cpp | 107 ++++++++++++++++++++------------------ src/timeline/InputBar.h | 2 +- 2 files changed, 57 insertions(+), 52 deletions(-) diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index 2491b5c1..693fe789 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -253,50 +253,51 @@ InputBar::updateTextContentProperties(const QString &t) // check for invalid commands auto commandName = getCommandAndArgs().first; - bool hasInvalidCommand{}; - if (!commandName.isNull() && '/' + commandName != text()) { - static const QStringList 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")}; - hasInvalidCommand = !validCommands.contains(commandName); - } else - hasInvalidCommand = false; + static const QStringList 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() && '/' + commandName != text() && !validCommands.contains(commandName); + bool signalsChanged{false}; if (containsInvalidCommand_ != hasInvalidCommand) { containsInvalidCommand_ = hasInvalidCommand; - emit containsInvalidCommandChanged(); + signalsChanged = true; } if (currentCommand_ != commandName) { currentCommand_ = commandName; + signalsChanged = true; + } + if (signalsChanged) { emit currentCommandChanged(); + emit containsInvalidCommandChanged(); } } @@ -390,10 +391,11 @@ InputBar::send() auto wasEdit = !room->edit().isEmpty(); - if (auto [commandName, args] = getCommandAndArgs(); commandName.isNull()) + if (auto [commandName, args] = getCommandAndArgs(); commandName.isEmpty()) message(text()); else - command(commandName, args); + if (!command(commandName, args)) + message(text()); if (!wasEdit) { history_.push_front(QLatin1String("")); @@ -758,16 +760,17 @@ InputBar::video(const QString &filename, QPair InputBar::getCommandAndArgs() const { - if (!text().startsWith('/')) - return {{}, text()}; + const auto currentText = text(); + if (!currentText.startsWith('/')) + return {{}, currentText}; - int command_end = text().indexOf(QRegularExpression(QStringLiteral("\\s"))); + int command_end = currentText.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); + 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 {{}, text()}; + return {{}, currentText}; } else { return {name, args}; } @@ -798,7 +801,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")) { @@ -886,16 +889,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")) { @@ -904,8 +907,10 @@ InputBar::command(const QString &command, QString args) } else if (command == QLatin1String("converttoroom")) { utils::removeDirectFromRoom(this->room->roomId()); } else { - message("/" + command + " " + args); + return false; } + + return true; } MediaUpload::MediaUpload(std::unique_ptr source_, diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h index 7385d272..94aedaf6 100644 --- a/src/timeline/InputBar.h +++ b/src/timeline/InputBar.h @@ -238,7 +238,7 @@ 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, From 9616ee51b3618838a93444733759e5697c75b371 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Wed, 1 Mar 2023 18:01:02 -0500 Subject: [PATCH 7/9] Use QSet instead of QStringList --- src/timeline/InputBar.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index 693fe789..b6c37fbc 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -253,7 +253,7 @@ InputBar::updateTextContentProperties(const QString &t) // check for invalid commands auto commandName = getCommandAndArgs().first; - static const QStringList validCommands{QStringLiteral("me"), + static const QSet validCommands{QStringLiteral("me"), QStringLiteral("react"), QStringLiteral("join"), QStringLiteral("knock"), From 2dc7492456b006b6c983ddd7357d29917d45843d Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Wed, 1 Mar 2023 18:04:36 -0500 Subject: [PATCH 8/9] make lint --- src/timeline/InputBar.cpp | 72 +++++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index b6c37fbc..fb61ce0d 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -254,46 +254,47 @@ InputBar::updateTextContentProperties(const QString &t) // check for invalid commands auto commandName = getCommandAndArgs().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() && '/' + commandName != text() && !validCommands.contains(commandName); + 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() && '/' + commandName != text() && !validCommands.contains(commandName); bool signalsChanged{false}; if (containsInvalidCommand_ != hasInvalidCommand) { containsInvalidCommand_ = hasInvalidCommand; - signalsChanged = true; + signalsChanged = true; } if (currentCommand_ != commandName) { currentCommand_ = commandName; - signalsChanged = true; + signalsChanged = true; } if (signalsChanged) { emit currentCommandChanged(); @@ -393,9 +394,8 @@ InputBar::send() if (auto [commandName, args] = getCommandAndArgs(); commandName.isEmpty()) message(text()); - else - if (!command(commandName, args)) - message(text()); + else if (!command(commandName, args)) + message(text()); if (!wasEdit) { history_.push_front(QLatin1String("")); From b266185ce83c81f0c120a9083867817a354107c2 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Tue, 7 Mar 2023 19:10:42 -0500 Subject: [PATCH 9/9] 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 af65ebc1..9b0b0907 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 f0e71c60..abda16b9 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 fb61ce0d..b27128e0 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 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 -InputBar::getCommandAndArgs() const +InputBar::getCommandAndArgs(const QString ¤tText) const { - const auto currentText = text(); if (!currentText.startsWith('/')) return {{}, currentText}; diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h index 94aedaf6..acafd964 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 getCommandAndArgs() const; + QPair getCommandAndArgs() const { return getCommandAndArgs(text()); } + QPair getCommandAndArgs(const QString ¤tText) const; mtx::common::Relations generateRelations() const; void startUploadFromPath(const QString &path); @@ -296,9 +301,10 @@ private: std::deque 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;