From a57744891a8d06100bc0c40a17149f98426f01f1 Mon Sep 17 00:00:00 2001 From: James Graham Date: Sun, 15 Oct 2023 12:55:56 +0000 Subject: [PATCH] ChatCache Move the functionality to cache the contents of a chat bar from the room directly and to a new ChatCache object. This works pretty much the same with a few extra check and balances, this also made it easy to put a test suite around the functionality so I did. The current functionality should be identical to what exists. This is in prep for threads which will require managing even more caches if we create one per thread. --- .reuse/dep5 | 4 + autotests/CMakeLists.txt | 8 ++ autotests/chatbarcachetest.cpp | 157 +++++++++++++++++++++ autotests/data/test-min-sync.json | 87 ++++++++++++ src/CMakeLists.txt | 2 + src/actionshandler.cpp | 50 +++---- src/actionshandler.h | 15 +- src/chatbarcache.cpp | 153 ++++++++++++++++++++ src/chatbarcache.h | 185 +++++++++++++++++++++++++ src/chatdocumenthandler.cpp | 40 +++--- src/chatdocumenthandler.h | 11 ++ src/models/actionsmodel.cpp | 64 ++++----- src/models/actionsmodel.h | 3 +- src/neochatroom.cpp | 130 ++--------------- src/neochatroom.h | 158 ++------------------- src/qml/ChatBar.qml | 47 ++++--- src/qml/FileDelegateContextMenu.qml | 4 +- src/qml/MessageDelegateContextMenu.qml | 8 +- src/qml/MessageEditComponent.qml | 29 ++-- src/qml/TextDelegate.qml | 4 +- src/qml/TimelineView.qml | 11 +- src/roommanager.cpp | 15 +- 22 files changed, 775 insertions(+), 410 deletions(-) create mode 100644 autotests/chatbarcachetest.cpp create mode 100644 autotests/data/test-min-sync.json create mode 100644 src/chatbarcache.cpp create mode 100644 src/chatbarcache.h diff --git a/.reuse/dep5 b/.reuse/dep5 index 532526354..cb155e003 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -41,3 +41,7 @@ License: CC0-1.0 Files: .flatpak-manifest.json Copyright: 2020-2022 Tobias Fella License: BSD-2-Clause + +Files: autotests/data/* +Copyright: none +License: CC0-1.0 diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 74a041508..f12a8e67d 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -3,6 +3,8 @@ enable_testing() +add_definitions(-DDATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data" ) + ecm_add_test( neochatroomtest.cpp LINK_LIBRARIES neochat Qt::Test @@ -32,3 +34,9 @@ ecm_add_test( LINK_LIBRARIES neochat Qt::Test TEST_NAME eventhandlertest ) + +ecm_add_test( + chatbarcachetest.cpp + LINK_LIBRARIES neochat Qt::Test + TEST_NAME chatbarcachetest +) diff --git a/autotests/chatbarcachetest.cpp b/autotests/chatbarcachetest.cpp new file mode 100644 index 000000000..2157ad6b9 --- /dev/null +++ b/autotests/chatbarcachetest.cpp @@ -0,0 +1,157 @@ +// SPDX-FileCopyrightText: 2023 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#include +#include +#include +#include + +#include +#include + +#include "chatbarcache.h" +#include "neochatroom.h" + +using namespace Quotient; + +class TestRoom : public NeoChatRoom +{ +public: + using NeoChatRoom::NeoChatRoom; + + void update(SyncRoomData &&data, bool fromCache = false) + { + Room::updateData(std::move(data), fromCache); + } +}; + +class ChatBarCacheTest : public QObject +{ + Q_OBJECT + +private: + Connection *connection = nullptr; + TestRoom *room = nullptr; + +private Q_SLOTS: + void initTestCase(); + + void empty(); + void noRoom(); + void badParent(); + void reply(); + void edit(); + void attachment(); +}; + +void ChatBarCacheTest::initTestCase() +{ + connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org")); + room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join); + + QFile testMinSyncFile; + testMinSyncFile.setFileName(QLatin1String(DATA_DIR) + u'/' + QLatin1String("test-min-sync.json")); + testMinSyncFile.open(QIODevice::ReadOnly); + const auto testMinSyncJson = QJsonDocument::fromJson(testMinSyncFile.readAll()); + SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, testMinSyncJson.object()); + room->update(std::move(roomData)); +} + +void ChatBarCacheTest::empty() +{ + QScopedPointer chatBarCache(new ChatBarCache(room)); + + QCOMPARE(chatBarCache->text(), QString()); + QCOMPARE(chatBarCache->isReplying(), false); + QCOMPARE(chatBarCache->replyId(), QString()); + QCOMPARE(chatBarCache->isEditing(), false); + QCOMPARE(chatBarCache->editId(), QString()); + QCOMPARE(chatBarCache->relationUser(), room->getUser(nullptr)); + QCOMPARE(chatBarCache->relationMessage(), QString()); + QCOMPARE(chatBarCache->attachmentPath(), QString()); +} + +void ChatBarCacheTest::noRoom() +{ + QScopedPointer chatBarCache(new ChatBarCache()); + chatBarCache->setReplyId(QLatin1String("$153456789:example.org")); + + // These should return empty even though a reply ID has been set because the + // ChatBarCache has no parent. + + QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation."); + QCOMPARE(chatBarCache->relationUser(), QVariantMap()); + + QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation."); + QCOMPARE(chatBarCache->relationMessage(), QString()); +} + +void ChatBarCacheTest::badParent() +{ + QScopedPointer badParent(new QObject()); + QScopedPointer chatBarCache(new ChatBarCache(badParent.get())); + chatBarCache->setReplyId(QLatin1String("$153456789:example.org")); + + // These should return empty even though a reply ID has been set because the + // ChatBarCache has no parent. + + QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation."); + QCOMPARE(chatBarCache->relationUser(), QVariantMap()); + + QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation."); + QCOMPARE(chatBarCache->relationMessage(), QString()); +} + +void ChatBarCacheTest::reply() +{ + QScopedPointer chatBarCache(new ChatBarCache(room)); + chatBarCache->setText(QLatin1String("some text")); + chatBarCache->setAttachmentPath(QLatin1String("some/path")); + chatBarCache->setReplyId(QLatin1String("$153456789:example.org")); + + QCOMPARE(chatBarCache->text(), QLatin1String("some text")); + QCOMPARE(chatBarCache->isReplying(), true); + QCOMPARE(chatBarCache->replyId(), QLatin1String("$153456789:example.org")); + QCOMPARE(chatBarCache->isEditing(), false); + QCOMPARE(chatBarCache->editId(), QString()); + QCOMPARE(chatBarCache->relationUser(), room->getUser(room->user(QLatin1String("@example:example.org")))); + QCOMPARE(chatBarCache->relationMessage(), QLatin1String("This is an example\ntext message")); + QCOMPARE(chatBarCache->attachmentPath(), QString()); +} + +void ChatBarCacheTest::edit() +{ + QScopedPointer chatBarCache(new ChatBarCache(room)); + chatBarCache->setText(QLatin1String("some text")); + chatBarCache->setAttachmentPath(QLatin1String("some/path")); + chatBarCache->setEditId(QLatin1String("$153456789:example.org")); + + QCOMPARE(chatBarCache->text(), QLatin1String("some text")); + QCOMPARE(chatBarCache->isReplying(), false); + QCOMPARE(chatBarCache->replyId(), QString()); + QCOMPARE(chatBarCache->isEditing(), true); + QCOMPARE(chatBarCache->editId(), QLatin1String("$153456789:example.org")); + QCOMPARE(chatBarCache->relationUser(), room->getUser(room->user(QLatin1String("@example:example.org")))); + QCOMPARE(chatBarCache->relationMessage(), QLatin1String("This is an example\ntext message")); + QCOMPARE(chatBarCache->attachmentPath(), QString()); +} + +void ChatBarCacheTest::attachment() +{ + QScopedPointer chatBarCache(new ChatBarCache(room)); + chatBarCache->setText(QLatin1String("some text")); + chatBarCache->setEditId(QLatin1String("$153456789:example.org")); + chatBarCache->setAttachmentPath(QLatin1String("some/path")); + + QCOMPARE(chatBarCache->text(), QLatin1String("some text")); + QCOMPARE(chatBarCache->isReplying(), false); + QCOMPARE(chatBarCache->replyId(), QString()); + QCOMPARE(chatBarCache->isEditing(), false); + QCOMPARE(chatBarCache->editId(), QString()); + QCOMPARE(chatBarCache->relationUser(), room->getUser(nullptr)); + QCOMPARE(chatBarCache->relationMessage(), QString()); + QCOMPARE(chatBarCache->attachmentPath(), QLatin1String("some/path")); +} + +QTEST_MAIN(ChatBarCacheTest) +#include "chatbarcachetest.moc" diff --git a/autotests/data/test-min-sync.json b/autotests/data/test-min-sync.json new file mode 100644 index 000000000..572651828 --- /dev/null +++ b/autotests/data/test-min-sync.json @@ -0,0 +1,87 @@ +{ + "account_data": { + "events": [ + { + "content": { + "tags": { + "u.work": { + "order": 0.9 + } + } + }, + "type": "m.tag" + }, + { + "content": { + "custom_config_key": "custom_config_value" + }, + "type": "org.example.custom.room.config" + } + ] + }, + "ephemeral": { + "events": [ + { + "content": { + "user_ids": [ + "@alice:matrix.org", + "@bob:example.com" + ] + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.typing" + } + ] + }, + "state": { + "events": [ + { + "content": { + "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF", + "displayname": "Alice Margatroid", + "membership": "join", + "reason": "Looking for support" + }, + "event_id": "$143273582443PhrSn:example.org", + "origin_server_ts": 1432735824653, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "sender": "@example:example.org", + "state_key": "@alice:example.org", + "type": "m.room.member", + "unsigned": { + "age": 1234 + } + } + ] + }, + "summary": { + "m.heroes": [ + "@alice:example.com", + "@bob:example.com" + ], + "m.invited_member_count": 0, + "m.joined_member_count": 2 + }, + "timeline": { + "events": [ + { + "content": { + "body": "This is an example\ntext message", + "format": "org.matrix.custom.html", + "formatted_body": "This is an example
text message
", + "msgtype": "m.text" + }, + "event_id": "$153456789:example.org", + "origin_server_ts": 1432735824654, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "sender": "@example:example.org", + "type": "m.room.message", + "unsigned": { + "age": 1232 + } + } + ], + "limited": true, + "prev_batch": "t34-23535_0_0" + } +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3fd29355e..08e7bdb6b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -135,6 +135,8 @@ add_library(neochat STATIC enums/delegatetype.h roomlastmessageprovider.cpp roomlastmessageprovider.h + chatbarcache.cpp + chatbarcache.h ) qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN diff --git a/src/actionshandler.cpp b/src/actionshandler.cpp index 54c804b9e..68cf613f1 100644 --- a/src/actionshandler.cpp +++ b/src/actionshandler.cpp @@ -38,45 +38,33 @@ void ActionsHandler::setRoom(NeoChatRoom *room) Q_EMIT roomChanged(); } -void ActionsHandler::handleNewMessage() +void ActionsHandler::handleMessageEvent(ChatBarCache *chatBarCache) { - checkEffects(m_room->chatBoxText()); - if (!m_room->chatBoxAttachmentPath().isEmpty()) { - QUrl url(m_room->chatBoxAttachmentPath()); - auto path = url.isLocalFile() ? url.toLocalFile() : url.toString(); - m_room->uploadFile(QUrl(path), m_room->chatBoxText().isEmpty() ? path.mid(path.lastIndexOf(u'/') + 1) : m_room->chatBoxText()); - m_room->setChatBoxAttachmentPath({}); - m_room->setChatBoxText({}); + if (!chatBarCache) { return; } - QString handledText = m_room->chatBoxText(); - handledText = handleMentions(handledText); - handleMessage(m_room->chatBoxText(), handledText); + checkEffects(chatBarCache->text()); + if (!chatBarCache->attachmentPath().isEmpty()) { + QUrl url(chatBarCache->attachmentPath()); + auto path = url.isLocalFile() ? url.toLocalFile() : url.toString(); + m_room->uploadFile(QUrl(path), chatBarCache->text().isEmpty() ? path.mid(path.lastIndexOf(u'/') + 1) : chatBarCache->text()); + chatBarCache->setAttachmentPath({}); + chatBarCache->setText({}); + return; + } + + QString handledText = chatBarCache->text(); + handledText = handleMentions(handledText, chatBarCache->mentions()); + handleMessage(m_room->mainCache()->text(), handledText, chatBarCache); } -void ActionsHandler::handleEdit() -{ - checkEffects(m_room->editText()); - - QString handledText = m_room->editText(); - handledText = handleMentions(handledText, true); - handleMessage(m_room->editText(), handledText, true); -} - -QString ActionsHandler::handleMentions(QString handledText, const bool &isEdit) +QString ActionsHandler::handleMentions(QString handledText, QVector *mentions) { if (!m_room) { return QString(); } - QVector *mentions; - if (isEdit) { - mentions = m_room->editMentions(); - } else { - mentions = m_room->mentions(); - } - std::sort(mentions->begin(), mentions->end(), [](const auto &a, const auto &b) -> bool { return a.cursor.anchor() > b.cursor.anchor(); }); @@ -94,7 +82,7 @@ QString ActionsHandler::handleMentions(QString handledText, const bool &isEdit) return handledText; } -void ActionsHandler::handleMessage(const QString &text, QString handledText, const bool &isEdit) +void ActionsHandler::handleMessage(const QString &text, QString handledText, ChatBarCache *chatBarCache) { if (NeoChatConfig::allowQuickEdit()) { QRegularExpression sed(QStringLiteral("^s/([^/]*)/([^/]*)(/g)?$")); @@ -134,7 +122,7 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, con 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(), m_room); + handledText = action.handle(handledText.mid(action.prefix.length() + 1).trimmed(), m_room, chatBarCache); if (action.messageType.has_value()) { messageType = *action.messageType; } @@ -161,7 +149,7 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, con return; } - m_room->postMessage(text, handledText, messageType, m_room->chatBoxReplyId(), isEdit ? m_room->chatBoxEditId() : QString()); + m_room->postMessage(text, handledText, messageType, chatBarCache->replyId(), chatBarCache->editId()); } void ActionsHandler::checkEffects(const QString &text) diff --git a/src/actionshandler.h b/src/actionshandler.h index 69b0fe66d..f6c7cff43 100644 --- a/src/actionshandler.h +++ b/src/actionshandler.h @@ -8,6 +8,7 @@ #include +#include "chatbarcache.h" #include "neochatroom.h" class NeoChatRoom; @@ -51,21 +52,15 @@ Q_SIGNALS: void showEffect(const QString &effect); public Q_SLOTS: - /** - * @brief Pre-process text and send message. + * @brief Pre-process text and send message event. */ - void handleNewMessage(); - - /** - * @brief Pre-process text and send edit. - */ - void handleEdit(); + void handleMessageEvent(ChatBarCache *chatBarCache); private: NeoChatRoom *m_room = nullptr; void checkEffects(const QString &text); - QString handleMentions(QString handledText, const bool &isEdit = false); - void handleMessage(const QString &text, QString handledText, const bool &isEdit = false); + QString handleMentions(QString handledText, QVector *mentions); + void handleMessage(const QString &text, QString handledText, ChatBarCache *chatBarCache); }; diff --git a/src/chatbarcache.cpp b/src/chatbarcache.cpp new file mode 100644 index 000000000..34d766dc8 --- /dev/null +++ b/src/chatbarcache.cpp @@ -0,0 +1,153 @@ +// SPDX-FileCopyrightText: 2023 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#include "chatbarcache.h" + +#include "eventhandler.h" +#include "neochatroom.h" + +ChatBarCache::ChatBarCache(QObject *parent) + : QObject(parent) +{ +} + +QString ChatBarCache::text() const +{ + return m_text; +} + +void ChatBarCache::setText(const QString &text) +{ + if (text == m_text) { + return; + } + m_text = text; + Q_EMIT textChanged(); +} + +bool ChatBarCache::isReplying() const +{ + return m_relationType == Reply && !m_relationId.isEmpty(); +} + +QString ChatBarCache::replyId() const +{ + if (m_relationType != Reply) { + return {}; + } + return m_relationId; +} + +void ChatBarCache::setReplyId(const QString &replyId) +{ + if (m_relationType == Reply && m_relationId == replyId) { + return; + } + m_relationId = replyId; + if (m_relationId.isEmpty()) { + m_relationType = None; + } else { + m_relationType = Reply; + } + m_attachmentPath = QString(); + Q_EMIT relationIdChanged(); +} + +bool ChatBarCache::isEditing() const +{ + return m_relationType == Edit && !m_relationId.isEmpty(); +} + +QString ChatBarCache::editId() const +{ + if (m_relationType != Edit) { + return {}; + } + return m_relationId; +} + +void ChatBarCache::setEditId(const QString &editId) +{ + if (m_relationType == Edit && m_relationId == editId) { + return; + } + m_relationId = editId; + if (m_relationId.isEmpty()) { + m_relationType = None; + } else { + m_relationType = Edit; + } + m_attachmentPath = QString(); + Q_EMIT relationIdChanged(); +} + +QVariantMap ChatBarCache::relationUser() const +{ + if (parent() == nullptr) { + qWarning() << "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation."; + return {}; + } + auto room = dynamic_cast(parent()); + if (room == nullptr) { + qWarning() << "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation."; + return {}; + } + if (m_relationId.isEmpty()) { + return room->getUser(nullptr); + } + return room->getUser(room->user((*room->findInTimeline(m_relationId))->senderId())); +} + +QString ChatBarCache::relationMessage() const +{ + if (parent() == nullptr) { + qWarning() << "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation."; + return {}; + } + if (m_relationId.isEmpty()) { + return {}; + } + auto room = dynamic_cast(parent()); + if (room == nullptr) { + qWarning() << "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation."; + return {}; + } + EventHandler eventhandler; + eventhandler.setRoom(room); + if (auto event = room->findInTimeline(m_relationId); event != room->historyEdge()) { + eventhandler.setEvent(&**event); + return eventhandler.getPlainBody(); + } + return {}; +} + +QString ChatBarCache::attachmentPath() const +{ + return m_attachmentPath; +} + +void ChatBarCache::setAttachmentPath(const QString &attachmentPath) +{ + if (attachmentPath == m_attachmentPath) { + return; + } + m_attachmentPath = attachmentPath; + m_relationType = None; + m_relationId = QString(); + Q_EMIT attachmentPathChanged(); +} + +QVector *ChatBarCache::mentions() +{ + return &m_mentions; +} + +QString ChatBarCache::savedText() const +{ + return m_savedText; +} + +void ChatBarCache::setSavedText(const QString &savedText) +{ + m_savedText = savedText; +} diff --git a/src/chatbarcache.h b/src/chatbarcache.h new file mode 100644 index 000000000..988ce38d6 --- /dev/null +++ b/src/chatbarcache.h @@ -0,0 +1,185 @@ +// SPDX-FileCopyrightText: 2023 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#pragma once + +#include +#include +#include + +/** + * @brief Defines a user mention in the current chat or edit text. + */ +struct Mention { + QTextCursor cursor; /**< Contains the mention's text and position in the text. */ + QString text; /**< The inserted text of the mention. */ + int start = 0; /**< Start position of the mention. */ + int position = 0; /**< End position of the mention. */ + QString id; /**< The id the mention (used to create link when sending the message). */ +}; + +/** + * @class ChatBarCache + * + * A class to cache data from a chat bar. + * + * A chat bar can be anything that allows users to compose or edit message, it doesn't + * necessarily have to use the ChatBar component, e.g. MessageEditComponent. + * + * This object is intended to allow the current contents of a chat bar to be cached + * between different rooms, i.e. there is an expectation that each NeoChatRoom could + * have a separate cache for each chat bar. + * + * @note The NeoChatRoom which this component is created in is expected to be set + * as it's parent. This is necessary for certain functions which need to get + * relevant room information. + * + * @sa ChatBar, MessageEditComponent, NeoChatRoom + */ +class ChatBarCache : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("") + + /** + * @brief The text in the chat bar. + * + * Due to problems with QTextDocument, unlike the other properties here, + * text is *not* used to store the text when switching rooms. + */ + Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged) + + /** + * @brief Whether the chat bar is currently replying to a message. + */ + Q_PROPERTY(bool isReplying READ isReplying NOTIFY relationIdChanged) + + /** + * @brief The Matrix message ID of an event being replied to, if any. + * + * Will return empty if the RelationType is currently set to None or Edit. + * + * @note Replying, editing and attachments are exclusive so setting this will + * clear an edit or attachment. + * + * @sa RelationType + */ + Q_PROPERTY(QString replyId READ replyId WRITE setReplyId NOTIFY relationIdChanged) + + /** + * @brief Whether the chat bar is currently editing a message. + */ + Q_PROPERTY(bool isEditing READ isEditing NOTIFY relationIdChanged) + + /** + * @brief The Matrix message ID of an event being edited, if any. + * + * Will return empty if the RelationType is currently set to None or Reply. + * + * @note Replying, editing and attachments are exclusive so setting this will + * clear an reply or attachment. + * + * @sa RelationType + */ + Q_PROPERTY(QString editId READ editId WRITE setEditId NOTIFY relationIdChanged) + + /** + * @brief Get the user for the message being replied to. + * + * This is different to getting a Quotient::User object + * as neither of those can provide details like the displayName or avatarMediaId + * without the room context as these can vary from room to room. + * + * Returns an empty user if not replying to a message. + * + * The user QVariantMap has the following properties: + * - isLocalUser - Whether the user is the local user. + * - id - The matrix ID of the user. + * - displayName - Display name in the context of this room. + * - avatarSource - The mxc URL for the user's avatar in the current room. + * - avatarMediaId - Avatar id in the context of this room. + * - color - Color for the user. + * - object - The Quotient::User object for the user. + * + * @sa getUser, Quotient::User + */ + Q_PROPERTY(QVariantMap relationUser READ relationUser NOTIFY relationIdChanged) + + /** + * @brief The content of the related message. + * + * Will be QString() if no related message. + */ + Q_PROPERTY(QString relationMessage READ relationMessage NOTIFY relationIdChanged) + + /** + * @brief The local path for a file to send, if any. + * + * @note Replying, editing and attachments are exclusive so setting this will + * clear an edit or reply. + */ + Q_PROPERTY(QString attachmentPath READ attachmentPath WRITE setAttachmentPath NOTIFY attachmentPathChanged) + +public: + /** + * @brief Describes the type of relation which relationId can refer to. + * + * A chat bar can only be relating to a single message at a time making these + * exclusive. + */ + enum RelationType { + Reply, /**< The current relation is a message being replied to. */ + Edit, /**< The current relation is a message being edited. */ + None, /**< There is currently no relation event */ + }; + Q_ENUM(RelationType) + + explicit ChatBarCache(QObject *parent = nullptr); + + QString text() const; + void setText(const QString &text); + + bool isReplying() const; + QString replyId() const; + void setReplyId(const QString &replyId); + + bool isEditing() const; + QString editId() const; + void setEditId(const QString &editId); + + QVariantMap relationUser() const; + + QString relationMessage() const; + + QString attachmentPath() const; + void setAttachmentPath(const QString &attachmentPath); + + /** + * @brief Retrieve the mentions for the current chat bar text. + */ + QVector *mentions(); + + /** + * @brief Get the saved chat bar text. + */ + QString savedText() const; + + /** + * @brief Save the chat bar text. + */ + void setSavedText(const QString &savedText); + +Q_SIGNALS: + void textChanged(); + void relationIdChanged(); + void attachmentPathChanged(); + +private: + QString m_text = QString(); + QString m_relationId = QString(); + RelationType m_relationType = RelationType::None; + QString m_attachmentPath = QString(); + QVector m_mentions; + QString m_savedText; +}; diff --git a/src/chatdocumenthandler.cpp b/src/chatdocumenthandler.cpp index b6d328d40..133739c27 100644 --- a/src/chatdocumenthandler.cpp +++ b/src/chatdocumenthandler.cpp @@ -57,11 +57,12 @@ public: setFormat(error.first, error.second.size(), errorFormat); } } - auto room = dynamic_cast(parent())->room(); + auto handler = dynamic_cast(parent()); + auto room = handler->room(); if (!room) { return; } - auto mentions = room->mentions(); + auto mentions = handler->chatBarCache()->mentions(); mentions->erase(std::remove_if(mentions->begin(), mentions->end(), [this](auto &mention) { @@ -103,15 +104,10 @@ ChatDocumentHandler::ChatDocumentHandler(QObject *parent) m_completionModel->setRoom(m_room); static QPointer previousRoom = nullptr; if (previousRoom) { - disconnect(previousRoom, &NeoChatRoom::chatBoxTextChanged, this, nullptr); - disconnect(previousRoom, &NeoChatRoom::editTextChanged, this, nullptr); + disconnect(m_chatBarCache, &ChatBarCache::textChanged, this, nullptr); } previousRoom = m_room; - connect(m_room, &NeoChatRoom::chatBoxTextChanged, this, [this]() { - int start = completionStartIndex(); - m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start)); - }); - connect(m_room, &NeoChatRoom::editTextChanged, this, [this]() { + connect(m_chatBarCache, &ChatBarCache::textChanged, this, [this]() { int start = completionStartIndex(); m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start)); }); @@ -215,6 +211,20 @@ void ChatDocumentHandler::setRoom(NeoChatRoom *room) Q_EMIT roomChanged(); } +ChatBarCache *ChatDocumentHandler::chatBarCache() const +{ + return m_chatBarCache; +} + +void ChatDocumentHandler::setChatBarCache(ChatBarCache *chatBarCache) +{ + if (m_chatBarCache == chatBarCache) { + return; + } + m_chatBarCache = chatBarCache; + Q_EMIT chatBarCacheChanged(); +} + void ChatDocumentHandler::complete(int index) { if (m_completionModel->autoCompletionType() == CompletionModel::User) { @@ -303,11 +313,7 @@ QString ChatDocumentHandler::getText() const if (!m_room) { return QString(); } - if (m_isEdit) { - return m_room->editText(); - } else { - return m_room->chatBoxText(); - } + return m_chatBarCache->text(); } void ChatDocumentHandler::pushMention(const Mention mention) const @@ -315,11 +321,7 @@ void ChatDocumentHandler::pushMention(const Mention mention) const if (!m_room) { return; } - if (m_isEdit) { - m_room->editMentions()->push_back(mention); - } else { - m_room->mentions()->push_back(mention); - } + m_chatBarCache->mentions()->push_back(mention); } QColor ChatDocumentHandler::mentionColor() const diff --git a/src/chatdocumenthandler.h b/src/chatdocumenthandler.h index 4e10be110..a262fad01 100644 --- a/src/chatdocumenthandler.h +++ b/src/chatdocumenthandler.h @@ -8,6 +8,7 @@ #include #include +#include "chatbarcache.h" #include "models/completionmodel.h" #include "neochatroom.h" @@ -102,6 +103,11 @@ class ChatDocumentHandler : public QObject */ Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged) + /** + * @brief The cache for the chat bar the text document is being handled for. + */ + Q_PROPERTY(ChatBarCache *chatBarCache READ chatBarCache WRITE setChatBarCache NOTIFY chatBarCacheChanged) + /** * @brief The color to highlight user mentions. */ @@ -133,6 +139,9 @@ public: [[nodiscard]] NeoChatRoom *room() const; void setRoom(NeoChatRoom *room); + [[nodiscard]] ChatBarCache *chatBarCache() const; + void setChatBarCache(ChatBarCache *chatBarCache); + Q_INVOKABLE void complete(int index); void updateCompletions(); @@ -149,6 +158,7 @@ Q_SIGNALS: void documentChanged(); void cursorPositionChanged(); void roomChanged(); + void chatBarCacheChanged(); void completionModelChanged(); void selectionStartChanged(); void selectionEndChanged(); @@ -163,6 +173,7 @@ private: QPointer m_document; QPointer m_room; + QPointer m_chatBarCache; bool completionVisible = false; QColor m_mentionColor; diff --git a/src/models/actionsmodel.cpp b/src/models/actionsmodel.cpp index 6e397e8c4..5ac9a74d7 100644 --- a/src/models/actionsmodel.cpp +++ b/src/models/actionsmodel.cpp @@ -3,6 +3,7 @@ #include "actionsmodel.h" +#include "chatbarcache.h" #include "controller.h" #include "neochatroom.h" #include "roommanager.h" @@ -19,7 +20,7 @@ QStringList rainbowColors{"#ff2b00"_ls, "#ff5500"_ls, "#ff8000"_ls, "#ffaa00"_ls "#00d4ff"_ls, "#00aaff"_ls, "#007fff"_ls, "#0055ff"_ls, "#002bff"_ls, "#0000ff"_ls, "#2a00ff"_ls, "#5500ff"_ls, "#7f00ff"_ls, "#aa00ff"_ls, "#d400ff"_ls, "#ff00ff"_ls, "#ff00d4"_ls, "#ff00aa"_ls, "#ff0080"_ls, "#ff0055"_ls, "#ff002b"_ls, "#ff0000"_ls}; -auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room) { +auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *) { if (text.isEmpty()) { Q_EMIT room->showMessage(NeoChatRoom::Info, i18n("Leaving this room.")); room->connection()->leaveRoom(room); @@ -45,7 +46,7 @@ auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room) { return QString(); }; -auto roomNickLambda = [](const QString &text, NeoChatRoom *room) { +auto roomNickLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *) { if (text.isEmpty()) { Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("No new nickname provided, no changes will happen.")); } else { @@ -57,7 +58,7 @@ auto roomNickLambda = [](const QString &text, NeoChatRoom *room) { QVector actions{ Action{ QStringLiteral("shrug"), - [](const QString &message, NeoChatRoom *) { + [](const QString &message, NeoChatRoom *, ChatBarCache *) { return QStringLiteral("¯\\\\_(ツ)_/¯ %1").arg(message); }, true, @@ -67,7 +68,7 @@ QVector actions{ }, Action{ QStringLiteral("lenny"), - [](const QString &message, NeoChatRoom *) { + [](const QString &message, NeoChatRoom *, ChatBarCache *) { return QStringLiteral("( ͡° ͜ʖ ͡°) %1").arg(message); }, true, @@ -77,7 +78,7 @@ QVector actions{ }, Action{ QStringLiteral("tableflip"), - [](const QString &message, NeoChatRoom *) { + [](const QString &message, NeoChatRoom *, ChatBarCache *) { return QStringLiteral("(╯°□°)╯︵ ┻━┻ %1").arg(message); }, true, @@ -87,7 +88,7 @@ QVector actions{ }, Action{ QStringLiteral("unflip"), - [](const QString &message, NeoChatRoom *) { + [](const QString &message, NeoChatRoom *, ChatBarCache *) { return QStringLiteral("┬──┬ ノ( ゜-゜ノ) %1").arg(message); }, true, @@ -97,7 +98,7 @@ QVector actions{ }, Action{ QStringLiteral("rainbow"), - [](const QString &text, NeoChatRoom *room) { + [](const QString &text, NeoChatRoom *room, ChatBarCache *chatBarCache) { QString rainbowText; for (int i = 0; i < text.length(); i++) { rainbowText += QStringLiteral("%3").arg(rainbowColors[i % rainbowColors.length()], text.at(i)); @@ -106,8 +107,8 @@ QVector actions{ room->postMessage(QStringLiteral("/rainbow %1").arg(text), rainbowText, RoomMessageEvent::MsgType::Text, - room->chatBoxReplyId(), - room->chatBoxEditId()); + chatBarCache->replyId(), + chatBarCache->editId()); return QString(); }, false, @@ -117,7 +118,7 @@ QVector actions{ }, Action{ QStringLiteral("rainbowme"), - [](const QString &text, NeoChatRoom *room) { + [](const QString &text, NeoChatRoom *room, ChatBarCache *chatBarCache) { QString rainbowText; for (int i = 0; i < text.length(); i++) { rainbowText += QStringLiteral("%3").arg(rainbowColors[i % rainbowColors.length()], text.at(i)); @@ -126,8 +127,8 @@ QVector actions{ room->postMessage(QStringLiteral("/rainbow %1").arg(text), rainbowText, RoomMessageEvent::MsgType::Emote, - room->chatBoxReplyId(), - room->chatBoxEditId()); + chatBarCache->replyId(), + chatBarCache->editId()); return QString(); }, false, @@ -137,7 +138,7 @@ QVector actions{ }, Action{ QStringLiteral("plain"), - [](const QString &text, NeoChatRoom *room) { + [](const QString &text, NeoChatRoom *room, ChatBarCache *) { room->postMessage(text, text.toHtmlEscaped(), RoomMessageEvent::MsgType::Text, {}, {}); return QString(); }, @@ -148,13 +149,13 @@ QVector actions{ }, Action{ QStringLiteral("spoiler"), - [](const QString &text, NeoChatRoom *room) { + [](const QString &text, NeoChatRoom *room, ChatBarCache *chatBarCache) { // Ideally, we would just return rainbowText and let that do the rest, but the colors don't survive markdownToHTML. room->postMessage(QStringLiteral("/spoiler %1").arg(text), QStringLiteral("%1").arg(text), RoomMessageEvent::MsgType::Text, - room->chatBoxReplyId(), - room->chatBoxEditId()); + chatBarCache->replyId(), + chatBarCache->editId()); return QString(); }, false, @@ -164,7 +165,7 @@ QVector actions{ }, Action{ QStringLiteral("me"), - [](const QString &text, NeoChatRoom *) { + [](const QString &text, NeoChatRoom *, ChatBarCache *) { return text; }, true, @@ -174,7 +175,7 @@ QVector actions{ }, Action{ QStringLiteral("notice"), - [](const QString &text, NeoChatRoom *) { + [](const QString &text, NeoChatRoom *, ChatBarCache *) { return text; }, true, @@ -184,7 +185,7 @@ QVector actions{ }, Action{ QStringLiteral("invite"), - [](const QString &text, NeoChatRoom *room) { + [](const QString &text, NeoChatRoom *room, ChatBarCache *) { static const QRegularExpression mxidRegex( QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))")); auto regexMatch = mxidRegex.match(text); @@ -220,7 +221,7 @@ QVector actions{ }, Action{ QStringLiteral("join"), - [](const QString &text, NeoChatRoom *room) { + [](const QString &text, NeoChatRoom *room, ChatBarCache *) { QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)")); auto regexMatch = roomRegex.match(text); if (!regexMatch.hasMatch()) { @@ -244,7 +245,7 @@ QVector actions{ }, Action{ QStringLiteral("knock"), - [](const QString &text, NeoChatRoom *room) { + [](const QString &text, NeoChatRoom *room, ChatBarCache *) { auto parts = text.split(QLatin1String(" ")); QString roomName = parts[0]; QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)")); @@ -276,7 +277,7 @@ QVector actions{ }, Action{ QStringLiteral("j"), - [](const QString &text, NeoChatRoom *room) { + [](const QString &text, NeoChatRoom *room, ChatBarCache *) { QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)")); auto regexMatch = roomRegex.match(text); if (!regexMatch.hasMatch()) { @@ -315,7 +316,7 @@ QVector actions{ }, Action{ QStringLiteral("nick"), - [](const QString &text, NeoChatRoom *room) { + [](const QString &text, NeoChatRoom *room, ChatBarCache *) { if (text.isEmpty()) { Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("No new nickname provided, no changes will happen.")); } else { @@ -346,7 +347,7 @@ QVector actions{ }, Action{ QStringLiteral("ignore"), - [](const QString &text, NeoChatRoom *room) { + [](const QString &text, NeoChatRoom *room, ChatBarCache *) { static const QRegularExpression mxidRegex( QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))")); auto regexMatch = mxidRegex.match(text); @@ -374,7 +375,7 @@ QVector actions{ }, Action{ QStringLiteral("unignore"), - [](const QString &text, NeoChatRoom *room) { + [](const QString &text, NeoChatRoom *room, ChatBarCache *) { static const QRegularExpression mxidRegex( QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))")); auto regexMatch = mxidRegex.match(text); @@ -402,9 +403,8 @@ QVector actions{ }, Action{ QStringLiteral("react"), - [](const QString &text, NeoChatRoom *room) { - QString replyEventId = room->chatBoxReplyId(); - if (replyEventId.isEmpty()) { + [](const QString &text, NeoChatRoom *room, ChatBarCache *chatBarCache) { + if (chatBarCache->replyId().isEmpty()) { for (auto it = room->messageEvents().crbegin(); it != room->messageEvents().crend(); it++) { const auto &evt = **it; if (const auto event = eventCast(&evt)) { @@ -413,7 +413,7 @@ QVector actions{ } } } - room->toggleReaction(replyEventId, text); + room->toggleReaction(chatBarCache->replyId(), text); return QString(); }, false, @@ -423,7 +423,7 @@ QVector actions{ }, Action{ QStringLiteral("ban"), - [](const QString &text, NeoChatRoom *room) { + [](const QString &text, NeoChatRoom *room, ChatBarCache *) { auto parts = text.split(QLatin1String(" ")); static const QRegularExpression mxidRegex( QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))")); @@ -462,7 +462,7 @@ QVector actions{ }, Action{ QStringLiteral("unban"), - [](const QString &text, NeoChatRoom *room) { + [](const QString &text, NeoChatRoom *room, ChatBarCache *) { static const QRegularExpression mxidRegex( QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))")); auto regexMatch = mxidRegex.match(text); @@ -495,7 +495,7 @@ QVector actions{ }, Action{ QStringLiteral("kick"), - [](const QString &text, NeoChatRoom *room) { + [](const QString &text, NeoChatRoom *room, ChatBarCache *) { auto parts = text.split(QLatin1String(" ")); static const QRegularExpression mxidRegex( QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))")); diff --git a/src/models/actionsmodel.h b/src/models/actionsmodel.h index 7805df336..ab7c23fce 100644 --- a/src/models/actionsmodel.h +++ b/src/models/actionsmodel.h @@ -7,6 +7,7 @@ #include #include +class ChatBarCache; class NeoChatRoom; /** @@ -28,7 +29,7 @@ public: /** * @brief The function to execute when the action is triggered. */ - std::function handle; + std::function handle; /** * @brief Whether the action is a message type action. * diff --git a/src/neochatroom.cpp b/src/neochatroom.cpp index f76c4edc8..ff7821e29 100644 --- a/src/neochatroom.cpp +++ b/src/neochatroom.cpp @@ -39,6 +39,7 @@ #include #include +#include "chatbarcache.h" #include "clipboard.h" #include "controller.h" #include "eventhandler.h" @@ -65,6 +66,9 @@ using namespace Quotient; NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinState) : Room(connection, std::move(roomId), joinState) { + m_mainCache = new ChatBarCache(this); + m_editCache = new ChatBarCache(this); + connect(connection, &Connection::accountDataChanged, this, &NeoChatRoom::updatePushNotificationState); connect(this, &Room::fileTransferCompleted, this, [this] { setFileUploadingProgress(0); @@ -1571,128 +1575,14 @@ void NeoChatRoom::copyEventMedia(const QString &eventId) } } -QString NeoChatRoom::chatBoxText() const +ChatBarCache *NeoChatRoom::mainCache() const { - return m_chatBoxText; + return m_mainCache; } -void NeoChatRoom::setChatBoxText(const QString &text) +ChatBarCache *NeoChatRoom::editCache() const { - m_chatBoxText = text; - Q_EMIT chatBoxTextChanged(); -} - -QString NeoChatRoom::editText() const -{ - return m_editText; -} - -void NeoChatRoom::setEditText(const QString &text) -{ - m_editText = text; - Q_EMIT editTextChanged(); -} - -QString NeoChatRoom::chatBoxReplyId() const -{ - return m_chatBoxReplyId; -} - -void NeoChatRoom::setChatBoxReplyId(const QString &replyId) -{ - if (replyId == m_chatBoxReplyId) { - return; - } - m_chatBoxReplyId = replyId; - Q_EMIT chatBoxReplyIdChanged(); -} - -QString NeoChatRoom::chatBoxEditId() const -{ - return m_chatBoxEditId; -} - -void NeoChatRoom::setChatBoxEditId(const QString &editId) -{ - if (editId == m_chatBoxEditId) { - return; - } - m_chatBoxEditId = editId; - Q_EMIT chatBoxEditIdChanged(); -} - -QVariantMap NeoChatRoom::chatBoxReplyUser() const -{ - if (m_chatBoxReplyId.isEmpty()) { - return emptyUser; - } - return getUser(user((*findInTimeline(m_chatBoxReplyId))->senderId())); -} - -QString NeoChatRoom::chatBoxReplyMessage() const -{ - if (m_chatBoxReplyId.isEmpty()) { - return {}; - } - - EventHandler eventhandler; - eventhandler.setRoom(this); - eventhandler.setEvent(&**findInTimeline(m_chatBoxReplyId)); - return eventhandler.getPlainBody(); -} - -QVariantMap NeoChatRoom::chatBoxEditUser() const -{ - if (m_chatBoxEditId.isEmpty()) { - return emptyUser; - } - return getUser(user((*findInTimeline(m_chatBoxEditId))->senderId())); -} - -QString NeoChatRoom::chatBoxEditMessage() const -{ - if (m_chatBoxEditId.isEmpty()) { - return {}; - } - - EventHandler eventhandler; - eventhandler.setRoom(this); - if (auto event = findInTimeline(m_chatBoxEditId); event != historyEdge()) { - eventhandler.setEvent(&**event); - return eventhandler.getPlainBody(); - } - return {}; -} - -QString NeoChatRoom::chatBoxAttachmentPath() const -{ - return m_chatBoxAttachmentPath; -} - -void NeoChatRoom::setChatBoxAttachmentPath(const QString &attachmentPath) -{ - m_chatBoxAttachmentPath = attachmentPath; - Q_EMIT chatBoxAttachmentPathChanged(); -} - -QVector *NeoChatRoom::mentions() -{ - return &m_mentions; -} - -QVector *NeoChatRoom::editMentions() -{ - return &m_editMentions; -} - -QString NeoChatRoom::savedText() const -{ - return m_savedText; -} - -void NeoChatRoom::setSavedText(const QString &savedText) -{ - m_savedText = savedText; + return m_editCache; } void NeoChatRoom::replyLastMessage() @@ -1721,7 +1611,7 @@ void NeoChatRoom::replyLastMessage() // For any message that isn't an edit return the id of the current message eventId = (*it)->id(); } - setChatBoxReplyId(eventId); + mainCache()->setReplyId(eventId); return; } } @@ -1755,7 +1645,7 @@ void NeoChatRoom::editLastMessage() // For any message that isn't an edit return the id of the current message eventId = (*it)->id(); } - setChatBoxEditId(eventId); + editCache()->setEditId(eventId); return; } } diff --git a/src/neochatroom.h b/src/neochatroom.h index d9263485f..f324e6a32 100644 --- a/src/neochatroom.h +++ b/src/neochatroom.h @@ -20,6 +20,8 @@ namespace Quotient class User; } +class ChatBarCache; + class PushNotificationState : public QObject { Q_OBJECT @@ -40,17 +42,6 @@ public: Q_ENUM(State) }; -/** - * @brief Defines a user mention in the current chat or edit text. - */ -struct Mention { - QTextCursor cursor; /**< Contains the mention's text and position in the text. */ - QString text; /**< The inserted text of the mention. */ - int start = 0; /**< Start position of the mention. */ - int position = 0; /**< End position of the mention. */ - QString id; /**< The id the mention (used to create link when sending the message). */ -}; - /** * @class NeoChatRoom * @@ -311,94 +302,14 @@ class NeoChatRoom : public Quotient::Room Q_PROPERTY(int spaceParentPowerLevel READ spaceParentPowerLevel WRITE setSpaceParentPowerLevel NOTIFY spaceParentPowerLevelChanged) /** - * @brief The current text in the chatbox for the room. - * - * Due to problems with QTextDocument, unlike the other properties here, - * chatBoxText is *not* used to store the text when switching rooms. + * @brief The cache for the main chat bar in the room. */ - Q_PROPERTY(QString chatBoxText READ chatBoxText WRITE setChatBoxText NOTIFY chatBoxTextChanged) + Q_PROPERTY(ChatBarCache *mainCache READ mainCache CONSTANT) /** - * @brief The text for any message currently being edited in the room. + * @brief The cache for the edit chat bar in the room. */ - Q_PROPERTY(QString editText READ editText WRITE setEditText NOTIFY editTextChanged) - - /** - * @brief The event id of a message being replied to. - * - * Will be QString() if not replying to a message. - */ - Q_PROPERTY(QString chatBoxReplyId READ chatBoxReplyId WRITE setChatBoxReplyId NOTIFY chatBoxReplyIdChanged) - - /** - * @brief The event id of a message being edited. - * - * Will be QString() if not editing to a message. - */ - Q_PROPERTY(QString chatBoxEditId READ chatBoxEditId WRITE setChatBoxEditId NOTIFY chatBoxEditIdChanged) - - /** - * @brief Get the user for the message being replied to. - * - * This is different to getting a Quotient::User object - * as neither of those can provide details like the displayName or avatarMediaId - * without the room context as these can vary from room to room. - * - * Returns an empty user if not replying to a message. - * - * The user QVariantMap has the following properties: - * - isLocalUser - Whether the user is the local user. - * - id - The matrix ID of the user. - * - displayName - Display name in the context of this room. - * - avatarSource - The mxc URL for the user's avatar in the current room. - * - avatarMediaId - Avatar id in the context of this room. - * - color - Color for the user. - * - object - The Quotient::User object for the user. - * - * @sa getUser, Quotient::User - */ - Q_PROPERTY(QVariantMap chatBoxReplyUser READ chatBoxReplyUser NOTIFY chatBoxReplyIdChanged) - - /** - * @brief The content of the message being replied to. - * - * Will be QString() if not replying to a message. - */ - Q_PROPERTY(QString chatBoxReplyMessage READ chatBoxReplyMessage NOTIFY chatBoxReplyIdChanged) - - /** - * @brief Get the user for the message being edited. - * - * This is different to getting a Quotient::User object - * as neither of those can provide details like the displayName or avatarMediaId - * without the room context as these can vary from room to room. - * - * Returns an empty user if not replying to a message. - * - * The user QVariantMap has the following properties: - * - isLocalUser - Whether the user is the local user. - * - id - The matrix ID of the user. - * - displayName - Display name in the context of this room. - * - avatarSource - The mxc URL for the user's avatar in the current room. - * - avatarMediaId - Avatar id in the context of this room. - * - color - Color for the user. - * - object - The Quotient::User object for the user. - * - * @sa getUser, Quotient::User - */ - Q_PROPERTY(QVariantMap chatBoxEditUser READ chatBoxEditUser NOTIFY chatBoxEditIdChanged) - - /** - * @brief The content of the message being edited. - * - * Will be QString() if not editing a message. - */ - Q_PROPERTY(QString chatBoxEditMessage READ chatBoxEditMessage NOTIFY chatBoxEditIdChanged) - - /** - * @brief The file path of the attachment to be sent. - */ - Q_PROPERTY(QString chatBoxAttachmentPath READ chatBoxAttachmentPath WRITE setChatBoxAttachmentPath NOTIFY chatBoxAttachmentPathChanged) + Q_PROPERTY(ChatBarCache *editCache READ editCache CONSTANT) public: /** @@ -805,46 +716,9 @@ public: [[nodiscard]] int spaceParentPowerLevel() const; void setSpaceParentPowerLevel(const int &newPowerLevel); - QString chatBoxText() const; - void setChatBoxText(const QString &text); + ChatBarCache *mainCache() const; - QString editText() const; - void setEditText(const QString &text); - - QString chatBoxReplyId() const; - void setChatBoxReplyId(const QString &replyId); - - QVariantMap chatBoxReplyUser() const; - QString chatBoxReplyMessage() const; - - QString chatBoxEditId() const; - void setChatBoxEditId(const QString &editId); - - QVariantMap chatBoxEditUser() const; - QString chatBoxEditMessage() const; - - QString chatBoxAttachmentPath() const; - void setChatBoxAttachmentPath(const QString &attachmentPath); - - /** - * @brief Retrieve the mentions for the current chatbox text. - */ - QVector *mentions(); - - /** - * @brief Retrieve the mentions for the current edit text. - */ - QVector *editMentions(); - - /** - * @brief Get the saved chatbox text for the room. - */ - QString savedText() const; - - /** - * @brief Save the chatbox text for the room. - */ - void setSavedText(const QString &savedText); + ChatBarCache *editCache() const; /** * @brief Reply to the last message sent in the timeline. @@ -916,14 +790,9 @@ private: std::unique_ptr m_cachedEvent; - QString m_chatBoxText; - QString m_editText; - QString m_chatBoxReplyId; - QString m_chatBoxEditId; - QString m_chatBoxAttachmentPath; - QVector m_mentions; - QVector m_editMentions; - QString m_savedText; + ChatBarCache *m_mainCache; + ChatBarCache *m_editCache; + QCache m_polls; std::vector> m_extraEvents; @@ -946,11 +815,6 @@ Q_SIGNALS: void displayNameChanged(); void pushNotificationStateChanged(PushNotificationState::State state); void showMessage(MessageType messageType, const QString &message); - void chatBoxTextChanged(); - void editTextChanged(); - void chatBoxReplyIdChanged(); - void chatBoxEditIdChanged(); - void chatBoxAttachmentPathChanged(); void canEncryptRoomChanged(); void joinRuleChanged(); void historyVisibilityChanged(); diff --git a/src/qml/ChatBar.qml b/src/qml/ChatBar.qml index 4a6b979c0..ee9235786 100644 --- a/src/qml/ChatBar.qml +++ b/src/qml/ChatBar.qml @@ -39,6 +39,7 @@ QQC2.Control { * @brief The current room that user is viewing. */ required property NeoChatRoom currentRoom + onCurrentRoomChanged: _private.chatBarCache = currentRoom.mainCache /** * @brief The QQC2.TextArea object. @@ -62,7 +63,7 @@ QQC2.Control { property bool isBusy: root.currentRoom && root.currentRoom.hasFileUploading // Matrix does not allow sending attachments in replies - visible: root.currentRoom.chatBoxReplyId.length === 0 && root.currentRoom.chatBoxAttachmentPath.length === 0 + visible: _private.chatBarCache.isReplying && _private.chatBarCache.attachmentPath.length === 0 icon.name: "mail-attachment" text: i18n("Attach an image or file") displayHint: Kirigami.DisplayHint.IconOnly @@ -76,7 +77,7 @@ QQC2.Control { if (!path) { return; } - root.currentRoom.chatBoxAttachmentPath = path; + _private.chatBarCache.attachmentPath = path; }) fileDialog.open() } @@ -169,7 +170,7 @@ QQC2.Control { leftPadding: LayoutMirroring.enabled ? actionsRow.width : Kirigami.Units.largeSpacing rightPadding: LayoutMirroring.enabled ? Kirigami.Units.largeSpacing : actionsRow.width + x * 2 + Kirigami.Units.largeSpacing * 2 - placeholderText: root.currentRoom.usesEncryption ? i18n("Send an encrypted message…") : root.currentRoom.chatBoxAttachmentPath.length > 0 ? i18n("Set an attachment caption...") : i18n("Send a message…") + placeholderText: root.currentRoom.usesEncryption ? i18n("Send an encrypted message…") : _private.chatBarCache.attachmentPath.length > 0 ? i18n("Set an attachment caption...") : i18n("Send a message…") verticalAlignment: TextEdit.AlignVCenter wrapMode: Text.Wrap @@ -189,7 +190,7 @@ QQC2.Control { root.currentRoom.sendTypingNotification(textExists) textExists ? repeatTimer.start() : repeatTimer.stop() } - root.currentRoom.chatBoxText = text + _private.chatBarCache.text = text } onCursorRectangleChanged: chatBarScrollView.ensureVisible(cursorRectangle) onSelectedTextChanged: { @@ -285,25 +286,25 @@ QQC2.Control { anchors.rightMargin: root.width > chatBarSizeHelper.currentWidth ? 0 : (chatBarScrollView.QQC2.ScrollBar.vertical.visible ? Kirigami.Units.largeSpacing * 3.5 : Kirigami.Units.largeSpacing) active: visible - visible: root.currentRoom.chatBoxReplyId.length > 0 || root.currentRoom.chatBoxAttachmentPath.length > 0 - sourceComponent: root.currentRoom.chatBoxReplyId.length > 0 ? replyPane : attachmentPane + visible: _private.chatBarCache.isReplying || _private.chatBarCache.attachmentPath.length > 0 + sourceComponent: _private.chatBarCache.isReplying ? replyPane : attachmentPane } Component { id: replyPane ReplyPane { - userName: root.currentRoom.chatBoxReplyUser.displayName - userColor: root.currentRoom.chatBoxReplyUser.color - userAvatar: root.currentRoom.chatBoxReplyUser.avatarSource - text: root.currentRoom.chatBoxReplyMessage + userName: _private.chatBarCache.relationUser.displayName + userColor: _private.chatBarCache.relationUser.color + userAvatar: _private.chatBarCache.relationUser.avatarSource + text: _private.chatBarCache.relationMessage } } Component { id: attachmentPane AttachmentPane { - attachmentPath: root.currentRoom.chatBoxAttachmentPath + attachmentPath: _private.chatBarCache.attachmentPath onAttachmentCancelled: { - root.currentRoom.chatBoxAttachmentPath = ""; + _private.chatBarCache.attachmentPath = ""; root.forceActiveFocus() } } @@ -349,14 +350,14 @@ QQC2.Control { anchors.right: parent.right anchors.rightMargin: (root.width - chatBarSizeHelper.currentWidth) / 2 + Kirigami.Units.largeSpacing + (chatBarScrollView.QQC2.ScrollBar.vertical.visible && !(root.width > chatBarSizeHelper.currentWidth) ? Kirigami.Units.largeSpacing * 2.5 : 0) - visible: root.currentRoom.chatBoxReplyId.length > 0 + visible: _private.chatBarCache.isReplying display: QQC2.AbstractButton.IconOnly action: Kirigami.Action { text: i18nc("@action:button", "Cancel reply") icon.name: "dialog-close" onTriggered: { - root.currentRoom.chatBoxReplyId = ""; - root.currentRoom.chatBoxAttachmentPath = ""; + _private.chatBarCache.replyId = ""; + _private.chatBarCache.attachmentPath = ""; root.forceActiveFocus() } } @@ -472,16 +473,16 @@ QQC2.Control { if (localPath.length === 0) { return false; } - root.currentRoom.chatBoxAttachmentPath = localPath; + _private.chatBarCache.attachmentPath = localPath; return true; } function postMessage() { - actionsHandler.handleNewMessage(); + actionsHandler.handleMessageEvent(_private.chatBarCache); repeatTimer.stop() root.currentRoom.markAllMessagesAsRead(); textField.clear(); - root.currentRoom.chatBoxReplyId = ""; + _private.chatBarCache.replyId = ""; messageSent() } @@ -558,7 +559,7 @@ QQC2.Control { if (!path) { return; } - root.currentRoom.chatBoxAttachmentPath = path; + _private.chatBarCache.attachmentPath = path; }) fileDialog.open() @@ -581,7 +582,7 @@ QQC2.Control { if (!Clipboard.saveImage(localPath)) { return; } - root.currentRoom.chatBoxAttachmentPath = localPath; + _private.chatBarCache.attachmentPath = localPath; attachDialog.close(); } } @@ -595,4 +596,10 @@ QQC2.Control { parentWindow: Window.window } } + + QtObject { + id: _private + property ChatBarCache chatBarCache + onChatBarCacheChanged: documentHandler.chatBarCache = chatBarCache + } } diff --git a/src/qml/FileDelegateContextMenu.qml b/src/qml/FileDelegateContextMenu.qml index a8a498817..76bbbf5df 100644 --- a/src/qml/FileDelegateContextMenu.qml +++ b/src/qml/FileDelegateContextMenu.qml @@ -59,8 +59,8 @@ MessageDelegateContextMenu { text: i18n("Reply") icon.name: "mail-replied-symbolic" onTriggered: { - currentRoom.chatBoxReplyId = eventId - currentRoom.chatBoxEditId = "" + currentRoom.mainCache.replyId = eventId; + currentRoom.editCache.editId = ""; RoomManager.requestFullScreenClose() } }, diff --git a/src/qml/MessageDelegateContextMenu.qml b/src/qml/MessageDelegateContextMenu.qml index 611b18201..a346ae99a 100644 --- a/src/qml/MessageDelegateContextMenu.qml +++ b/src/qml/MessageDelegateContextMenu.qml @@ -93,8 +93,8 @@ Loader { text: i18n("Edit") icon.name: "document-edit" onTriggered: { - currentRoom.chatBoxEditId = eventId; - currentRoom.chatBoxReplyId = ""; + currentRoom.editCache.editId = eventId; + currentRoom.mainCache.replyId = ""; } visible: author.isLocalUser && (root.delegateType === DelegateType.Emote || root.delegateType === DelegateType.Message) }, @@ -102,8 +102,8 @@ Loader { text: i18n("Reply") icon.name: "mail-replied-symbolic" onTriggered: { - currentRoom.chatBoxReplyId = eventId; - currentRoom.chatBoxEditId = ""; + currentRoom.mainCache.replyId = eventId; + currentRoom.editCache.editId = ""; } }, Kirigami.Action { diff --git a/src/qml/MessageEditComponent.qml b/src/qml/MessageEditComponent.qml index 3b219d35d..7f4b2860c 100644 --- a/src/qml/MessageEditComponent.qml +++ b/src/qml/MessageEditComponent.qml @@ -13,7 +13,10 @@ QQC2.TextArea { id: root required property NeoChatRoom room - onRoomChanged: room.chatBoxEditIdChanged.connect(updateEditText) + onRoomChanged: { + _private.chatBarCache = room.editCache + _private.chatBarCache.relationIdChanged.connect(_private.updateEditText) + } property string messageId @@ -26,7 +29,7 @@ QQC2.TextArea { wrapMode: Text.Wrap onTextChanged: { - room.editText = text + _private.chatBarCache.text = text } Keys.onEnterPressed: { @@ -88,7 +91,7 @@ QQC2.TextArea { text: i18nc("@action:button", "Cancel edit") icon.name: "dialog-close" onTriggered: { - room.chatBoxEditId = ""; + _private.chatBarCache.editId = ""; } shortcut: "Escape" } @@ -134,16 +137,22 @@ QQC2.TextArea { } function postEdit() { - actionsHandler.handleEdit(); + actionsHandler.handleMessageEvent(_private.chatBarCache); root.clear(); - room.chatBoxEditId = ""; + _private.chatBarCache.editId = ""; } - function updateEditText() { - if (room.chatBoxEditId == messageId && room.chatBoxEditMessage.length > 0) { - root.text = room.chatBoxEditMessage - forceActiveFocus(); - root.cursorPosition = root.length; + QtObject { + id: _private + property ChatBarCache chatBarCache + onChatBarCacheChanged: documentHandler.chatBarCache = chatBarCache + + function updateEditText() { + if (chatBarCache.isEditing && chatBarCache.relationMessage.length > 0) { + root.text = chatBarCache.relationMessage + root.forceActiveFocus(); + root.cursorPosition = root.length; + } } } } diff --git a/src/qml/TextDelegate.qml b/src/qml/TextDelegate.qml index c89de9909..cb0d17d96 100644 --- a/src/qml/TextDelegate.qml +++ b/src/qml/TextDelegate.qml @@ -43,7 +43,7 @@ MessageDelegate { RichLabel { id: label Layout.fillWidth: true - visible: currentRoom.chatBoxEditId !== root.eventId + visible: currentRoom.editCache.editId !== root.eventId isReply: root.isReply @@ -59,7 +59,7 @@ MessageDelegate { Layout.fillWidth: true Layout.minimumHeight: item ? item.minimumHeight : -1 Layout.preferredWidth: item ? item.preferredWidth : -1 - visible: currentRoom.chatBoxEditId === root.eventId + visible: currentRoom.editCache.editId === root.eventId active: visible sourceComponent: MessageEditComponent { room: currentRoom diff --git a/src/qml/TimelineView.qml b/src/qml/TimelineView.qml index 285ba385c..b07a327cd 100644 --- a/src/qml/TimelineView.qml +++ b/src/qml/TimelineView.qml @@ -182,7 +182,7 @@ QQC2.ScrollView { DropArea { id: dropAreaFile anchors.fill: parent - onDropped: root.currentRoom.chatBoxAttachmentPath = drop.urls[0] + onDropped: root.currentRoom.mainCache.attachmentPath = drop.urls[0] ; enabled: !Controller.isFlatpak } @@ -250,12 +250,13 @@ QQC2.ScrollView { } } onEditClicked: { - root.currentRoom.chatBoxEditId = delegate.eventId; - root.currentRoom.chatBoxReplyId = ""; + root.currentRoom.editCache.editId = delegate.eventId; + root.currentRoom.mainCache.replyId = ""; + } onReplyClicked: { - root.currentRoom.chatBoxReplyId = delegate.eventId; - root.currentRoom.chatBoxEditId = ""; + root.currentRoom.mainCache.replyId = delegate.eventId; + root.currentRoom.editCache.editId = ""; root.focusChatBox(); } } diff --git a/src/roommanager.cpp b/src/roommanager.cpp index e532b2c5d..ccd859b76 100644 --- a/src/roommanager.cpp +++ b/src/roommanager.cpp @@ -4,6 +4,7 @@ #include "roommanager.h" +#include "chatbarcache.h" #include "controller.h" #include "enums/delegatetype.h" #include "models/messageeventmodel.h" @@ -190,15 +191,15 @@ void RoomManager::openRoomForActiveConnection() void RoomManager::enterRoom(NeoChatRoom *room) { - if (m_currentRoom && !m_currentRoom->chatBoxEditId().isEmpty()) { - m_currentRoom->setChatBoxEditId({}); + if (m_currentRoom && !m_currentRoom->editCache()->editId().isEmpty()) { + m_currentRoom->editCache()->setEditId({}); } if (m_currentRoom && m_chatDocumentHandler) { // We're doing these things here because it is critical that they are switched at the same time if (m_chatDocumentHandler->document()) { - m_currentRoom->setSavedText(m_chatDocumentHandler->document()->textDocument()->toPlainText()); + m_currentRoom->mainCache()->setSavedText(m_chatDocumentHandler->document()->textDocument()->toPlainText()); m_chatDocumentHandler->setRoom(room); - m_chatDocumentHandler->document()->textDocument()->setPlainText(room->savedText()); + m_chatDocumentHandler->document()->textDocument()->setPlainText(room->mainCache()->savedText()); } else { m_chatDocumentHandler->setRoom(room); } @@ -232,13 +233,13 @@ void RoomManager::enterSpaceHome(NeoChatRoom *spaceRoom) return; } // If replacing a normal room message timeline make sure any edit is cancelled. - if (m_currentRoom && !m_currentRoom->chatBoxEditId().isEmpty()) { - m_currentRoom->setChatBoxEditId({}); + if (m_currentRoom && !m_currentRoom->editCache()->editId().isEmpty()) { + m_currentRoom->editCache()->setEditId({}); } // Save the chatbar text for the current room if any before switching if (m_currentRoom && m_chatDocumentHandler) { if (m_chatDocumentHandler->document()) { - m_currentRoom->setSavedText(m_chatDocumentHandler->document()->textDocument()->toPlainText()); + m_currentRoom->mainCache()->setSavedText(m_chatDocumentHandler->document()->textDocument()->toPlainText()); } } m_lastCurrentRoom = std::exchange(m_currentRoom, spaceRoom);