diff --git a/autotests/eventhandlertest.cpp b/autotests/eventhandlertest.cpp index 61ff738ed..bc3489bbc 100644 --- a/autotests/eventhandlertest.cpp +++ b/autotests/eventhandlertest.cpp @@ -61,6 +61,7 @@ private Q_SLOTS: void genericBody_data(); void genericBody(); void nullGenericBody(); + void markdownBody(); void subtitle(); void nullSubtitle(); void mediaInfo(); @@ -346,6 +347,13 @@ void EventHandlerTest::nullGenericBody() QCOMPARE(noEventHandler.getGenericBody(), QString()); } +void EventHandlerTest::markdownBody() +{ + eventHandler.setEvent(room->messageEvents().at(0).get()); + + QCOMPARE(eventHandler.getMarkdownBody(), QStringLiteral("This is an example\ntext message")); +} + void EventHandlerTest::subtitle() { auto event = room->messageEvents().at(0).get(); diff --git a/src/chatbarcache.cpp b/src/chatbarcache.cpp index ec13041e4..d3783db53 100644 --- a/src/chatbarcache.cpp +++ b/src/chatbarcache.cpp @@ -3,6 +3,7 @@ #include "chatbarcache.h" +#include "chatdocumenthandler.h" #include "eventhandler.h" #include "neochatroom.h" @@ -118,7 +119,7 @@ QString ChatBarCache::relationMessage() const eventhandler.setRoom(room); if (auto event = room->findInTimeline(m_relationId); event != room->historyEdge()) { eventhandler.setEvent(&**event); - return eventhandler.getPlainBody(); + return eventhandler.getMarkdownBody(); } return {}; } @@ -164,6 +165,54 @@ QList *ChatBarCache::mentions() return &m_mentions; } +void ChatBarCache::updateMentions(QQuickTextDocument *document, ChatDocumentHandler *documentHandler) +{ + documentHandler->setDocument(document); + + 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; + } + + if (auto event = room->findInTimeline(m_relationId); event != room->historyEdge()) { + if (const auto &roomMessageEvent = &*event->viewAs()) { + // Replaces the mentions that are baked into the HTML but plaintext in the original markdown + const QRegularExpression re(QStringLiteral(R"lit(([\S]*)<\/a>)lit")); + + m_mentions.clear(); + + int linkSize = 0; + auto matches = re.globalMatch(EventHandler::rawMessageBody(*roomMessageEvent)); + while (matches.hasNext()) { + const QRegularExpressionMatch match = matches.next(); + if (match.hasMatch()) { + const QString id = match.captured(1); + const QString name = match.captured(2); + + const int position = match.capturedStart(0) - linkSize; + const int end = position + name.length(); + linkSize += match.capturedLength(0) - name.length(); + + QTextCursor cursor(documentHandler->document()->textDocument()); + cursor.setPosition(position); + cursor.setPosition(end, QTextCursor::KeepAnchor); + cursor.setKeepPositionOnInsert(true); + + m_mentions.push_back(Mention{.cursor = cursor, .text = name, .start = position, .position = end, .id = id}); + } + } + } + } +} + QString ChatBarCache::savedText() const { return m_savedText; diff --git a/src/chatbarcache.h b/src/chatbarcache.h index 433295e16..f9031e877 100644 --- a/src/chatbarcache.h +++ b/src/chatbarcache.h @@ -5,8 +5,11 @@ #include #include +#include #include +class ChatDocumentHandler; + /** * @brief Defines a user mention in the current chat or edit text. */ @@ -174,6 +177,11 @@ public: */ QList *mentions(); + /** + * @brief Update the mentions in @p document when editing a message. + */ + Q_INVOKABLE void updateMentions(QQuickTextDocument *document, ChatDocumentHandler *documentHandler); + /** * @brief Get the saved chat bar text. */ diff --git a/src/eventhandler.cpp b/src/eventhandler.cpp index 6b337366e..498303df7 100644 --- a/src/eventhandler.cpp +++ b/src/eventhandler.cpp @@ -300,6 +300,27 @@ bool EventHandler::isHidden() return false; } +QString EventHandler::rawMessageBody(const Quotient::RoomMessageEvent &event) +{ + if (event.hasFileContent()) { + auto fileCaption = event.content()->fileInfo()->originalName; + if (fileCaption.isEmpty()) { + fileCaption = event.plainBody(); + } else if (event.content()->fileInfo()->originalName != event.plainBody()) { + fileCaption = event.plainBody() + " | "_ls + fileCaption; + } + return fileCaption; + } + + QString body; + if (event.hasTextContent() && event.content()) { + body = static_cast(event.content())->body; + } else { + body = event.plainBody(); + } + return body; +} + QString EventHandler::getRichBody(bool stripNewlines) const { if (m_event == nullptr) { @@ -318,6 +339,22 @@ QString EventHandler::getPlainBody(bool stripNewlines) const return getBody(m_event, Qt::PlainText, stripNewlines); } +QString EventHandler::getMarkdownBody() const +{ + if (m_event == nullptr) { + qCWarning(EventHandling) << "getMarkdownBody called with m_event set to nullptr."; + return {}; + } + + if (!m_event->is()) { + qCWarning(EventHandling) << "getMarkdownBody called when m_event isn't a RoomMessageEvent."; + return {}; + } + + const auto roomMessageEvent = eventCast(m_event); + return roomMessageEvent->plainBody(); +} + QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat format, bool stripNewlines) const { if (event->isRedacted()) { diff --git a/src/eventhandler.h b/src/eventhandler.h index 54378696c..88d4c6639 100644 --- a/src/eventhandler.h +++ b/src/eventhandler.h @@ -159,6 +159,14 @@ public: */ bool isHidden(); + /** + * @brief Output a string for the room message content without any formatting. + * + * This is the content of the formatted_body key if present or the body key if + * not. + */ + static QString rawMessageBody(const Quotient::RoomMessageEvent &event); + /** * @brief Output a string for the message content ready for display in a rich text field. * @@ -191,6 +199,13 @@ public: */ QString getPlainBody(bool stripNewlines = false) const; + /** + * @brief Output the original body for the message content, useful for editing the original message. + * + * The event type must be a room message event. + */ + QString getMarkdownBody() const; + /** * @brief Output a generic string for the message content ready for display. * diff --git a/src/qml/MessageEditComponent.qml b/src/qml/MessageEditComponent.qml index 5388223f9..922305481 100644 --- a/src/qml/MessageEditComponent.qml +++ b/src/qml/MessageEditComponent.qml @@ -155,8 +155,11 @@ QQC2.TextArea { onChatBarCacheChanged: documentHandler.chatBarCache = chatBarCache function updateEditText() { + // This could possibly be undefined due to some esoteric QtQuick issue. Referencing it somewhere in JS is enough. + documentHandler.document; if (chatBarCache?.isEditing && chatBarCache.relationMessage.length > 0) { - root.text = chatBarCache.relationMessage + root.text = chatBarCache.relationMessage; + chatBarCache.updateMentions(root.textDocument, documentHandler); root.forceActiveFocus(); root.cursorPosition = root.length; }