diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 1bd269158..19909922f 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -53,12 +53,6 @@ ecm_add_test( TEST_NAME messageeventmodeltest ) -ecm_add_test( - actionshandlertest.cpp - LINK_LIBRARIES neochat Qt::Test - TEST_NAME actionshandlertest -) - ecm_add_test( windowcontrollertest.cpp LINK_LIBRARIES neochat Qt::Test diff --git a/autotests/actionshandlertest.cpp b/autotests/actionshandlertest.cpp deleted file mode 100644 index 4f0fb8ab1..000000000 --- a/autotests/actionshandlertest.cpp +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-FileCopyrightText: 2023 James Graham -// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - -#include - -#include "actionshandler.h" -#include "chatbarcache.h" - -#include "testutils.h" - -class ActionsHandlerTest : public QObject -{ - Q_OBJECT - -private: - Quotient::Connection *connection = Quotient::Connection::makeMockConnection(QStringLiteral("@bob:kde.org")); - -private Q_SLOTS: - void nullObject(); -}; - -void ActionsHandlerTest::nullObject() -{ - QTest::ignoreMessage(QtWarningMsg, "ActionsHandler::handleMessageEvent - called with m_room and/or chatBarCache set to nullptr."); - ActionsHandler::handleMessageEvent(nullptr, nullptr); - - auto chatBarCache = new ChatBarCache(this); - QTest::ignoreMessage(QtWarningMsg, "ActionsHandler::handleMessageEvent - called with m_room and/or chatBarCache set to nullptr."); - ActionsHandler::handleMessageEvent(nullptr, chatBarCache); - - auto room = new TestUtils::TestRoom(connection, QStringLiteral("#myroom:kde.org")); - QTest::ignoreMessage(QtWarningMsg, "ActionsHandler::handleMessageEvent - called with m_room and/or chatBarCache set to nullptr."); - ActionsHandler::handleMessageEvent(room, nullptr); - - // The final one should throw no warning so we make sure. - QTest::failOnWarning("ActionsHandler::handleMessageEvent - called with m_room and/or chatBarCache set to nullptr."); - ActionsHandler::handleMessageEvent(room, chatBarCache); -} - -QTEST_GUILESS_MAIN(ActionsHandlerTest) -#include "actionshandlertest.moc" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 354b8202e..58ac2a468 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -10,8 +10,6 @@ endif() add_library(neochat STATIC controller.cpp controller.h - actionshandler.cpp - actionshandler.h models/emojimodel.cpp models/emojimodel.h emojitones.cpp diff --git a/src/actionshandler.cpp b/src/actionshandler.cpp deleted file mode 100644 index 6da36a9b8..000000000 --- a/src/actionshandler.cpp +++ /dev/null @@ -1,144 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Carl Schwan -// SPDX-FileCopyrightText: 2024 James Graham -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "actionshandler.h" - -#include "chatbarcache.h" -#include "models/actionsmodel.h" -#include "neochatconfig.h" -#include "texthandler.h" - -using namespace Quotient; -using namespace Qt::StringLiterals; - -void ActionsHandler::handleMessageEvent(NeoChatRoom *room, ChatBarCache *chatBarCache) -{ - if (room == nullptr || chatBarCache == nullptr) { - qWarning() << "ActionsHandler::handleMessageEvent - called with m_room and/or chatBarCache set to nullptr."; - return; - } - - if (!chatBarCache->attachmentPath().isEmpty()) { - QUrl url(chatBarCache->attachmentPath()); - auto path = url.isLocalFile() ? url.toLocalFile() : url.toString(); - room->uploadFile(QUrl(path), chatBarCache->text().isEmpty() ? path.mid(path.lastIndexOf(u'/') + 1) : chatBarCache->text()); - chatBarCache->setAttachmentPath({}); - chatBarCache->setText({}); - return; - } - - const auto handledText = handleMentions(chatBarCache); - const auto result = handleQuickEdit(room, handledText); - if (!result) { - handleMessage(room, handledText, chatBarCache); - } -} - -QString ActionsHandler::handleMentions(ChatBarCache *chatBarCache) -{ - const auto mentions = chatBarCache->mentions(); - std::sort(mentions->begin(), mentions->end(), [](const auto &a, const auto &b) -> bool { - return a.cursor.anchor() > b.cursor.anchor(); - }); - - auto handledText = chatBarCache->text(); - for (const auto &mention : *mentions) { - if (mention.text.isEmpty() || mention.id.isEmpty()) { - continue; - } - handledText = handledText.replace(mention.cursor.anchor(), - mention.cursor.position() - mention.cursor.anchor(), - QStringLiteral("[%1](https://matrix.to/#/%2)").arg(mention.text.toHtmlEscaped(), mention.id)); - } - mentions->clear(); - - return handledText; -} - -bool ActionsHandler::handleQuickEdit(NeoChatRoom *room, const QString &handledText) -{ - if (room == nullptr) { - return false; - } - - if (NeoChatConfig::allowQuickEdit()) { - QRegularExpression sed(QStringLiteral("^s/([^/]*)/([^/]*)(/g)?$")); - auto match = sed.match(handledText); - if (match.hasMatch()) { - const QString regex = match.captured(1); - const QString replacement = match.captured(2).toHtmlEscaped(); - const QString flags = match.captured(3); - - for (auto it = room->messageEvents().crbegin(); it != room->messageEvents().crend(); it++) { - if (const auto event = eventCast(&**it)) { -#if Quotient_VERSION_MINOR > 8 - if (event->senderId() == room->localMember().id() && event->has()) { -#else - if (event->senderId() == room->localMember().id() && event->hasTextContent()) { -#endif - QString originalString; - if (event->content()) { -#if Quotient_VERSION_MINOR > 8 - originalString = event->get()->body; -#else - originalString = static_cast(event->content())->body; -#endif - } else { - originalString = event->plainBody(); - } - if (flags == "/g"_L1) { - room->postHtmlMessage(handledText, originalString.replace(regex, replacement), event->msgtype(), {}, event->id()); - } else { - room->postHtmlMessage(handledText, - originalString.replace(originalString.indexOf(regex), regex.size(), replacement), - event->msgtype(), - {}, - event->id()); - } - return true; - } - } - } - } - } - return false; -} - -void ActionsHandler::handleMessage(NeoChatRoom *room, QString handledText, ChatBarCache *chatBarCache) -{ - if (room == nullptr) { - return; - } - - auto messageType = RoomMessageEvent::MsgType::Text; - - if (handledText.startsWith(QLatin1Char('/'))) { - for (const auto &action : ActionsModel::instance().allActions()) { - if (handledText.indexOf(action.prefix) == 1 - && (handledText.indexOf(" "_ls) == action.prefix.length() + 1 || handledText.length() == action.prefix.length() + 1)) { - handledText = action.handle(handledText.mid(action.prefix.length() + 1).trimmed(), room, chatBarCache); - if (action.messageType.has_value()) { - messageType = *action.messageType; - } - if (action.messageAction) { - break; - } else { - return; - } - } - } - } - - TextHandler textHandler; - textHandler.setData(handledText); - handledText = textHandler.handleSendText(); - - if (handledText.length() == 0) { - return; - } - - room->postMessage(chatBarCache->text(), handledText, messageType, chatBarCache->replyId(), chatBarCache->editId(), chatBarCache->threadId()); -} - -#include "moc_actionshandler.cpp" diff --git a/src/actionshandler.h b/src/actionshandler.h deleted file mode 100644 index 2f3c508e9..000000000 --- a/src/actionshandler.h +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Carl Schwan -// SPDX-FileCopyrightText: 2024 James Graham -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include - -class ChatBarCache; -class NeoChatRoom; - -/** - * @class ActionsHandler - * - * This class contains functions to handle chat messages ready for posting to a room. - * - * Everything that needs to be done to prepare the message for posting in a room - * including: - * - File handling - * - User mentions - * - Quick edits - * - Chat actions - * - Custom emojis - * - * @note A chat action is a message starting with /, resulting in something other - * than a normal message being sent (e.g. /me, /join). - * - * @sa ActionsModel, NeoChatRoom - */ -class ActionsHandler -{ -public: - /** - * @brief Pre-process text and send message event. - */ - static void handleMessageEvent(NeoChatRoom *room, ChatBarCache *chatBarCache); - -private: - static QString handleMentions(ChatBarCache *chatBarCache); - static bool handleQuickEdit(NeoChatRoom *room, const QString &handledText); - - static void handleMessage(NeoChatRoom *room, QString handledText, ChatBarCache *chatBarCache); -}; diff --git a/src/chatbar/ChatBar.qml b/src/chatbar/ChatBar.qml index 1d1fa29cd..7220d30a0 100644 --- a/src/chatbar/ChatBar.qml +++ b/src/chatbar/ChatBar.qml @@ -405,7 +405,6 @@ QQC2.Control { repeatTimer.stop(); root.currentRoom.markAllMessagesAsRead(); textField.clear(); - _private.chatBarCache.clearRelations(); messageSent(); } diff --git a/src/chatbarcache.cpp b/src/chatbarcache.cpp index 79c4ed047..e87d27294 100644 --- a/src/chatbarcache.cpp +++ b/src/chatbarcache.cpp @@ -5,10 +5,11 @@ #include -#include "actionshandler.h" #include "chatdocumenthandler.h" #include "eventhandler.h" +#include "models/actionsmodel.h" #include "neochatroom.h" +#include "texthandler.h" ChatBarCache::ChatBarCache(QObject *parent) : QObject(parent) @@ -29,6 +30,37 @@ void ChatBarCache::setText(const QString &text) Q_EMIT textChanged(); } +QString ChatBarCache::sendText() const +{ + if (!attachmentPath().isEmpty()) { + QUrl url(attachmentPath()); + auto path = url.isLocalFile() ? url.toLocalFile() : url.toString(); + return text().isEmpty() ? path.mid(path.lastIndexOf(u'/') + 1) : text(); + } + + return formatMentions(); +} + +QString ChatBarCache::formatMentions() const +{ + auto mentions = m_mentions; + std::sort(mentions.begin(), mentions.end(), [](const auto &a, const auto &b) { + return a.cursor.anchor() > b.cursor.anchor(); + }); + + auto formattedText = text(); + for (const auto &mention : mentions) { + if (mention.text.isEmpty() || mention.id.isEmpty()) { + continue; + } + formattedText = formattedText.replace(mention.cursor.anchor(), + mention.cursor.position() - mention.cursor.anchor(), + QStringLiteral("[%1](https://matrix.to/#/%2)").arg(mention.text.toHtmlEscaped(), mention.id)); + } + + return formattedText; +} + bool ChatBarCache::isReplying() const { return m_relationType == Reply && !m_relationId.isEmpty(); @@ -268,7 +300,35 @@ void ChatBarCache::postMessage() return; } - ActionsHandler::handleMessageEvent(room, this); + if (!attachmentPath().isEmpty()) { + room->uploadFile(QUrl(attachmentPath()), sendText()); + clearCache(); + return; + } + + const auto result = ActionsModel::handleAction(room, this); + if (!result.first.has_value()) { + return; + } + + TextHandler textHandler; + textHandler.setData(*std::get>(result)); + const auto sendText = textHandler.handleSendText(); + + if (sendText.length() == 0) { + return; + } + + room->postMessage(text(), sendText, *std::get>(result), replyId(), editId(), threadId()); + clearCache(); +} + +void ChatBarCache::clearCache() +{ + setText({}); + m_mentions.clear(); + m_savedText = QString(); + clearRelations(); } #include "moc_chatbarcache.cpp" diff --git a/src/chatbarcache.h b/src/chatbarcache.h index 64cd1a367..0924c11ed 100644 --- a/src/chatbarcache.h +++ b/src/chatbarcache.h @@ -153,6 +153,7 @@ public: explicit ChatBarCache(QObject *parent = nullptr); QString text() const; + QString sendText() const; void setText(const QString &text); bool isReplying() const; @@ -215,6 +216,8 @@ Q_SIGNALS: private: QString m_text = QString(); + QString formatMentions() const; + QString m_relationId = QString(); RelationType m_relationType = RelationType::None; QString m_threadId = QString(); @@ -223,4 +226,6 @@ private: QString m_savedText; QPointer m_relationContentModel; + + void clearCache(); }; diff --git a/src/models/actionsmodel.cpp b/src/models/actionsmodel.cpp index 7862a9359..94368abba 100644 --- a/src/models/actionsmodel.cpp +++ b/src/models/actionsmodel.cpp @@ -5,6 +5,7 @@ #include "chatbarcache.h" #include "enums/messagetype.h" +#include "neochatconfig.h" #include "neochatconnection.h" #include "neochatroom.h" #include "roommanager.h" @@ -17,6 +18,7 @@ using Action = ActionsModel::Action; using namespace Quotient; +using namespace Qt::StringLiterals; QStringList rainbowColors{"#ff2b00"_ls, "#ff5500"_ls, "#ff8000"_ls, "#ffaa00"_ls, "#ffd500"_ls, "#ffff00"_ls, "#d4ff00"_ls, "#aaff00"_ls, "#80ff00"_ls, "#55ff00"_ls, "#2bff00"_ls, "#00ff00"_ls, "#00ff2b"_ls, "#00ff55"_ls, "#00ff80"_ls, "#00ffaa"_ls, "#00ffd5"_ls, "#00ffff"_ls, @@ -574,3 +576,82 @@ QList &ActionsModel::allActions() const { return actions; } + +bool ActionsModel::handleQuickEditAction(NeoChatRoom *room, const QString &messageText) +{ + if (room == nullptr) { + return false; + } + + if (NeoChatConfig::allowQuickEdit()) { + QRegularExpression sed(QStringLiteral("^s/([^/]*)/([^/]*)(/g)?$")); + auto match = sed.match(messageText); + if (match.hasMatch()) { + const QString regex = match.captured(1); + const QString replacement = match.captured(2).toHtmlEscaped(); + const QString flags = match.captured(3); + + for (auto it = room->messageEvents().crbegin(); it != room->messageEvents().crend(); it++) { + if (const auto event = eventCast(&**it)) { +#if Quotient_VERSION_MINOR > 8 + if (event->senderId() == room->localMember().id() && event->has()) { +#else + if (event->senderId() == room->localMember().id() && event->hasTextContent()) { +#endif + + QString originalString; + if (event->content()) { +#if Quotient_VERSION_MINOR > 8 + originalString = static_cast(event->content().get())->body; +#else + originalString = static_cast(event->content())->body; +#endif + } else { + originalString = event->plainBody(); + } + if (flags == "/g"_L1) { + room->postHtmlMessage(messageText, originalString.replace(regex, replacement), event->msgtype(), {}, event->id()); + } else { + room->postHtmlMessage(messageText, + originalString.replace(originalString.indexOf(regex), regex.size(), replacement), + event->msgtype(), + {}, + event->id()); + } + return true; + } + } + } + } + } + return false; +} + +std::pair, std::optional> ActionsModel::handleAction(NeoChatRoom *room, ChatBarCache *chatBarCache) +{ + auto sendText = chatBarCache->sendText(); + const auto edited = handleQuickEditAction(room, sendText); + if (edited) { + return std::make_pair(std::nullopt, std::nullopt); + } + + std::optional messageType = std::nullopt; + if (sendText.startsWith(QLatin1Char('/'))) { + for (const auto &action : ActionsModel::instance().allActions()) { + if (sendText.indexOf(action.prefix) == 1 + && (sendText.indexOf(" "_ls) == action.prefix.length() + 1 || sendText.length() == action.prefix.length() + 1)) { + sendText = action.handle(sendText.mid(action.prefix.length() + 1).trimmed(), room, chatBarCache); + if (action.messageType.has_value()) { + messageType = action.messageType; + } + if (action.messageAction) { + break; + } else { + return std::make_pair(std::nullopt, std::nullopt); + } + } + } + } + + return std::make_pair(sendText, messageType); +} diff --git a/src/models/actionsmodel.h b/src/models/actionsmodel.h index 49830aad5..535c9fbaa 100644 --- a/src/models/actionsmodel.h +++ b/src/models/actionsmodel.h @@ -90,6 +90,21 @@ public: */ QList &allActions() const; + /** + * @brief Handle special sed style edit action. + * + * @return True if the message has a sed edit which was actioned. False otherwise. + */ + static bool handleQuickEditAction(NeoChatRoom *room, const QString &messageText); + + /** + * @brief Handle any action within the message contained in the given ChatBarCache. + * + * @return A modified or unmodified string that needs to be sent or an empty string if + * the handled action replaces sending a normal message. + */ + static std::pair, std::optional> handleAction(NeoChatRoom *room, ChatBarCache *chatBarCache); + private: ActionsModel() = default; };