diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 6f7ecd0fe..c8625b0b4 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -8,3 +8,9 @@ ecm_add_test( LINK_LIBRARIES neochat Qt::Test Quotient TEST_NAME neochatroomtest ) + +ecm_add_test( + texthandlertest.cpp + LINK_LIBRARIES neochat Qt::Test + TEST_NAME texthandlertest +) diff --git a/autotests/neochatroomtest.cpp b/autotests/neochatroomtest.cpp index 66e281604..e9edad2ca 100644 --- a/autotests/neochatroomtest.cpp +++ b/autotests/neochatroomtest.cpp @@ -136,7 +136,7 @@ void NeoChatRoomTest::initTestCase() void NeoChatRoomTest::subtitleTextTest() { QCOMPARE(room->timelineSize(), 1); - QCOMPARE(room->subtitleText(), QStringLiteral("@example:example.org: This is an example text message")); + QCOMPARE(room->lastEventToString(), QStringLiteral("@example:example.org: This is an example text message")); } void NeoChatRoomTest::eventTest() diff --git a/autotests/texthandlertest.cpp b/autotests/texthandlertest.cpp new file mode 100644 index 000000000..aa3c46a0d --- /dev/null +++ b/autotests/texthandlertest.cpp @@ -0,0 +1,482 @@ +// 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 "texthandler.h" + +#include +#include +#include + +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 TextHandlerTest : public QObject +{ + Q_OBJECT + +private: + Connection *connection = nullptr; + TestRoom *room = nullptr; + +private Q_SLOTS: + void initTestCase(); + + void allowedAttributes(); + void stripDisallowedTags(); + void stripDisallowedAttributes(); + void emptyCodeTags(); + + void sendSimpleStringCase(); + void sendSingleParaMarkup(); + void sendMultipleSectionMarkup(); + void sendBadLinks(); + void sendEscapeCode(); + void sendCodeClass(); + + void receiveStripReply(); + void receivePlainTextIn(); + + void recieveRichInPlainOut(); + void receivePlainStripHtml(); + void receivePlainStripMarkup(); + void receiveStripNewlines(); + + void receiveRichUserPill(); + void receiveRichStrikethrough(); + void receiveRichtextIn(); + void receiveRichMxcUrl(); + void receiveRichPlainUrl(); +}; + +#ifdef QUOTIENT_07 +void TextHandlerTest::initTestCase() +{ + connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org")); + room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join); + + const auto json = QJsonDocument::fromJson(R"EVENT({ + "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** text message", + "format": "org.matrix.custom.html", + "formatted_body": "This is an example text message", + "msgtype": "m.text" + }, + "event_id": "$143273582443PhrSn:example.org", + "origin_server_ts": 1432735824654, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "sender": "@example:example.org", + "type": "m.room.message", + "unsigned": { + "age": 1235 + } + } + ], + "limited": true, + "prev_batch": "t34-23535_0_0" + } +})EVENT"); + SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, json.object()); + room->update(std::move(roomData)); +} +#endif + +void TextHandlerTest::allowedAttributes() +{ + const QString testInputString = QStringLiteral("

Test

"); + const QString testOutputString = QStringLiteral("

Test

"); + + TextHandler testTextHandler; + testTextHandler.setData(testInputString); + + QCOMPARE(testTextHandler.handleSendText(), testOutputString); + QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString); +} + +void TextHandlerTest::stripDisallowedTags() +{ + const QString testInputString = QStringLiteral("

Allowed

Allowed Disallowed"); + const QString testOutputString = QStringLiteral("

Allowed

Allowed Disallowed"); + + TextHandler testTextHandler; + testTextHandler.setData(testInputString); + + QCOMPARE(testTextHandler.handleSendText(), testOutputString); + QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString); +} + +void TextHandlerTest::stripDisallowedAttributes() +{ + const QString testInputString = QStringLiteral("

Test

"); + const QString testOutputString = QStringLiteral("

Test

"); + + TextHandler testTextHandler; + testTextHandler.setData(testInputString); + + QCOMPARE(testTextHandler.handleSendText(), testOutputString); + QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString); +} + +/** + * Make sure that empty code tags are handled. + * (this was a bug during development hence the test) + */ +void TextHandlerTest::emptyCodeTags() +{ + const QString testInputString = QStringLiteral("
"); + const QString testOutputString = QStringLiteral("
"); + + TextHandler testTextHandler; + testTextHandler.setData(testInputString); + + QCOMPARE(testTextHandler.handleSendText(), testOutputString); + QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString); +} + +void TextHandlerTest::sendSimpleStringCase() +{ + const QString testInputString = QStringLiteral("This data should just be put in a paragraph."); + const QString testOutputString = QStringLiteral("

This data should just be put in a paragraph.

"); + + TextHandler testTextHandler; + testTextHandler.setData(testInputString); + + QCOMPARE(testTextHandler.handleSendText(), testOutputString); +} + +void TextHandlerTest::sendSingleParaMarkup() +{ + const QString testInputString = QStringLiteral( + "Text para with **bold**, *italic*, [link](https://kde.org), ![image](mxc://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e), `inline code`."); + const QString testOutputString = QStringLiteral( + "

Text para with bold, italic, link, \"image\", inline code.

"); + + TextHandler testTextHandler; + testTextHandler.setData(testInputString); + + QCOMPARE(testTextHandler.handleSendText(), testOutputString); +} + +void TextHandlerTest::sendMultipleSectionMarkup() +{ + const QString testInputString = + QStringLiteral("Text para\n> blockquote\n* List 1\n* List 2\n1. one\n2. two\n# Heading 1\n## Heading 2\nhorizontal rule\n\n---\n```\ncodeblock\n```"); + const QString testOutputString = QStringLiteral( + "

Text para

\n
\n

blockquote

\n
\n
    \n
  • List 1
  • \n
  • List " + "2
  • \n
\n
    \n
  1. one
  2. \n
  3. two
  4. \n
\n

Heading 1

\n

Heading 2

\n

horizontal " + "rule

\n
\n
codeblock\n
"); + + TextHandler testTextHandler; + testTextHandler.setData(testInputString); + + QCOMPARE(testTextHandler.handleSendText(), testOutputString); +} + +void TextHandlerTest::sendBadLinks() +{ + const QString testInputString = QStringLiteral("[link](kde.org), ![image](https://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e)"); + const QString testOutputString = QStringLiteral("

link, \"image\"

"); + + TextHandler testTextHandler; + testTextHandler.setData(testInputString); + + QCOMPARE(testTextHandler.handleSendText(), testOutputString); +} + +/** + * All text between code tags is treated as plain so it should get escaped. + */ +void TextHandlerTest::sendEscapeCode() +{ + const QString testInputString = QStringLiteral("```\n

Test some code

\n```"); + const QString testOutputString = + QStringLiteral("
<p>Test <span style="font-size:50px;">some</span> code</p>\n
"); + + TextHandler testTextHandler; + testTextHandler.setData(testInputString); + + QCOMPARE(testTextHandler.handleSendText(), testOutputString); +} + +void TextHandlerTest::sendCodeClass() +{ + const QString testInputString = QStringLiteral("```html\nsome code\n```\n
some more code
"); + const QString testOutputString = QStringLiteral("
some code\n
\n
some more code
"); + + TextHandler testTextHandler; + testTextHandler.setData(testInputString); + + QCOMPARE(testTextHandler.handleSendText(), testOutputString); +} + +void TextHandlerTest::receiveStripReply() +{ + const QString testInputString = QStringLiteral( + "
In reply to@alice:example.org
Message replied to.
Reply message."); + const QString testOutputString = QStringLiteral("Reply message."); + + TextHandler testTextHandler; + testTextHandler.setData(testInputString); + + QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString); + QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputString); +} + +void TextHandlerTest::recieveRichInPlainOut() +{ + const QString testInputString = QStringLiteral("a & b"); + const QString testOutputString = QStringLiteral("a & b"); + + TextHandler testTextHandler; + testTextHandler.setData(testInputString); + + QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputString); +} + +void TextHandlerTest::receivePlainTextIn() +{ + const QString testInputString = QStringLiteral("\nTest link https://kde.org."); + const QString testOutputStringRich = QStringLiteral("<plain text in tag bracket>
Test link https://kde.org."); + QString testOutputStringPlain = QStringLiteral("\nTest link https://kde.org."); + + TextHandler testTextHandler; + testTextHandler.setData(testInputString); + + QCOMPARE(testTextHandler.handleRecieveRichText(Qt::PlainText), testOutputStringRich); + QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputStringPlain); +} + +void TextHandlerTest::receiveStripNewlines() +{ + const QString testInputStringPlain = QStringLiteral("Test\nmany\nnew\nlines."); + const QString testInputStringRich = QStringLiteral("Test
many
new
lines."); + const QString testOutputString = QStringLiteral("Test many new lines."); + + TextHandler testTextHandler; + testTextHandler.setData(testInputStringPlain); + + QCOMPARE(testTextHandler.handleRecievePlainText(Qt::PlainText, true), testOutputString); + QCOMPARE(testTextHandler.handleRecieveRichText(Qt::PlainText, nullptr, nullptr, true), testOutputString); + + testTextHandler.setData(testInputStringRich); + + QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText, true), testOutputString); + QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, nullptr, nullptr, true), testOutputString); +} + +/** + * For a plain text output of a received string all html is stripped except for + * code which is unescaped if it's html. + */ +void TextHandlerTest::receivePlainStripHtml() +{ + const QString testInputString = QStringLiteral("

Test

Some code with tags
"); + const QString testOutputString = QStringLiteral("Test Some code with tags"); + + TextHandler testTextHandler; + testTextHandler.setData(testInputString); + + QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText), testOutputString); +} + +void TextHandlerTest::receivePlainStripMarkup() +{ + const QString testInputString = QStringLiteral("**bold** `

inline code

` *italic*"); + const QString testOutputString = QStringLiteral("bold

inline code

italic"); + + TextHandler testTextHandler; + testTextHandler.setData(testInputString); + + QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputString); +} + +void TextHandlerTest::receiveRichUserPill() +{ + const QString testInputString = QStringLiteral("

@alice:example.org

"); + const QString testOutputString = QStringLiteral("

@alice:example.org

"); + + TextHandler testTextHandler; + testTextHandler.setData(testInputString); + + QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString); +} + +void TextHandlerTest::receiveRichStrikethrough() +{ + const QString testInputString = QStringLiteral("

Test

"); + const QString testOutputString = QStringLiteral("

Test

"); + + TextHandler testTextHandler; + testTextHandler.setData(testInputString); + + QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString); +} + +void TextHandlerTest::receiveRichtextIn() +{ + const QString testInputString = QStringLiteral("

Test

Some code with tags
"); + const QString testOutputString = QStringLiteral("

Test

Some code <strong>with tags</strong>
"); + + TextHandler testTextHandler; + testTextHandler.setData(testInputString); + + QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString); +} + +#ifdef QUOTIENT_07 +void TextHandlerTest::receiveRichMxcUrl() +{ + const QString testInputString = QStringLiteral( + "\"image\""); + const QString testOutputString = QStringLiteral( + ""); + + TextHandler testTextHandler; + testTextHandler.setData(testInputString); + + QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, room->messageEvents().back().get()), testOutputString); +} +#endif + +/** + * For when your rich input string has a plain text url left in. + * + * This test is to show that a url that is already rich will be left alone but a + * plain one will be linkified. + */ +void TextHandlerTest::receiveRichPlainUrl() +{ + // This is an actual link that caused trouble which is why it's so long. Keeping + // so we can confirm consistent behaviour for complex urls. + const QString testInputStringLink1 = QStringLiteral( + "https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im " + "Link already rich"); + const QString testOutputStringLink1 = QStringLiteral( + "https://matrix.to/#/" + "!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im Link already rich"); + + // Another real case. The linkification wasn't handling it when a single link + // contains what looks like and email. It was been broken into 3 but needs to + // be just single link. + const QString testInputStringLink2 = QStringLiteral("https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/"); + const QString testOutputStringLink2 = QStringLiteral( + "https://lore.kernel.org/lkml/" + "CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/"); + + QString testInputStringEmail = QStringLiteral(R"(email@example.com Link already rich)"); + QString testOutputStringEmail = + QStringLiteral(R"(email@example.com Link already rich)"); + + QString testInputStringMxId = QStringLiteral("@user:kde.org Link already rich"); + QString testOutputStringMxId = QStringLiteral( + "@user:kde.org Link already rich"); + + TextHandler testTextHandler; + testTextHandler.setData(testInputStringLink1); + + QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringLink1); + + testTextHandler.setData(testInputStringLink2); + QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringLink2); + + testTextHandler.setData(testInputStringEmail); + QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringEmail); + + testTextHandler.setData(testInputStringMxId); + QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringMxId); +} + +QTEST_MAIN(TextHandlerTest) +#include "texthandlertest.moc" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c6481abec..719505e98 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -24,7 +24,6 @@ add_library(neochat STATIC models/publicroomlistmodel.cpp models/userdirectorylistmodel.cpp models/keywordnotificationrulemodel.cpp - utils.cpp notificationsmanager.cpp models/sortfilterroomlistmodel.cpp chatdocumenthandler.cpp @@ -47,6 +46,7 @@ add_library(neochat STATIC models/statemodel.cpp filetransferpseudojob.cpp models/searchmodel.cpp + texthandler.cpp ) add_executable(neochat-app diff --git a/src/actionshandler.cpp b/src/actionshandler.cpp index f735f5cc0..9433e2d29 100644 --- a/src/actionshandler.cpp +++ b/src/actionshandler.cpp @@ -20,25 +20,10 @@ #include "neochatroom.h" #include "neochatuser.h" #include "roommanager.h" +#include "texthandler.h" using namespace Quotient; -QString markdownToHTML(const QString &markdown) -{ - const auto str = markdown.toUtf8(); - char *tmp_buf = cmark_markdown_to_html(str.constData(), str.size(), CMARK_OPT_HARDBREAKS | CMARK_OPT_UNSAFE); - - const std::string html(tmp_buf); - - free(tmp_buf); - - auto result = QString::fromStdString(html).trimmed(); - - result.replace("", ""); - - return result; -} - ActionsHandler::ActionsHandler(QObject *parent) : QObject(parent) { @@ -169,7 +154,10 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, con } handledText = CustomEmojiModel::instance().preprocessText(handledText); - handledText = markdownToHTML(handledText); + TextHandler textHandler; + textHandler.setData(handledText); + handledText = textHandler.handleSendText(); + if (handledText.count("

") == 1 && handledText.count("

") == 1) { handledText.remove("

"); handledText.remove("

"); diff --git a/src/actionshandler.h b/src/actionshandler.h index f03071b1b..85e30e4d6 100644 --- a/src/actionshandler.h +++ b/src/actionshandler.h @@ -50,5 +50,3 @@ private: QString handleMentions(QString handledText, const bool &isEdit = false); void handleMessage(const QString &text, QString handledText, const bool &isEdit = false); }; - -QString markdownToHTML(const QString &markdown); diff --git a/src/models/messageeventmodel.cpp b/src/models/messageeventmodel.cpp index d0f6a459b..b9ccb37a2 100644 --- a/src/models/messageeventmodel.cpp +++ b/src/models/messageeventmodel.cpp @@ -27,7 +27,6 @@ #include #include "neochatuser.h" -#include "utils.h" using namespace Quotient; diff --git a/src/models/roomlistmodel.cpp b/src/models/roomlistmodel.cpp index 161eb6ed6..094e75a09 100644 --- a/src/models/roomlistmodel.cpp +++ b/src/models/roomlistmodel.cpp @@ -417,7 +417,7 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const return m_categoryVisibility.value(data(index, CategoryRole).toInt(), true); } if (role == SubtitleTextRole) { - return room->subtitleText(); + return room->lastEventToString(Qt::PlainText, true); } if (role == AvatarImageRole) { return room->avatar(128); diff --git a/src/neochatroom.cpp b/src/neochatroom.cpp index c6b1bfb9a..8a79a42fa 100644 --- a/src/neochatroom.cpp +++ b/src/neochatroom.cpp @@ -47,7 +47,7 @@ #endif #include "filetransferpseudojob.h" #include "stickerevent.h" -#include "utils.h" +#include "texthandler.h" #ifndef Q_OS_ANDROID #include @@ -257,10 +257,11 @@ bool NeoChatRoom::lastEventIsSpoiler() const return false; } -QString NeoChatRoom::lastEventToString() const +QString NeoChatRoom::lastEventToString(Qt::TextFormat format, bool stripNewlines) const { if (auto event = lastEvent()) { - return roomMembername(event->senderId()) + (event->isStateEvent() ? " " : ": ") + eventToString(*event); + return roomMembername(event->senderId()) + (event->isStateEvent() ? QLatin1String(" ") : QLatin1String(": ")) + + eventToString(*event, format, stripNewlines); } return QLatin1String(""); } @@ -329,45 +330,6 @@ QDateTime NeoChatRoom::lastActiveTime() return messageEvents().rbegin()->get()->originTimestamp(); } -QString NeoChatRoom::subtitleText() -{ - static const QRegularExpression blockquote("(\r\n\t|\n|\r\t|)> "); - static const QRegularExpression heading("(\r\n\t|\n|\r\t|)\\#{1,6} "); - static const QRegularExpression newlines("(\r\n\t|\n|\r\t|\r\n)"); - static const QRegularExpression bold1("(\\*\\*|__)(?=\\S)([^\\r]*\\S)\\1"); - static const QRegularExpression bold2("(\\*|_)(?=\\S)([^\\r]*\\S)\\1"); - static const QRegularExpression strike1("~~(.*)~~"); - static const QRegularExpression strike2("~(.*)~"); - static const QRegularExpression del("(.*)"); - static const QRegularExpression multileLineCode("```([^```]+)```"); - static const QRegularExpression singleLinecode("`([^`]+)`"); - QString subtitle = lastEventToString().size() == 0 ? topic() : lastEventToString(); - - subtitle - // replace blockquote, i.e. '> text' - .replace(blockquote, " ") - // replace headings, i.e. "# text" - .replace(heading, " ") - // replace newlines - .replace(newlines, " ") - // replace '**text**' and '__text__' - .replace(bold1, "\\2") - // replace '*text*' and '_text_' - .replace(bold2, "\\2") - // replace '~~text~~' - .replace(strike1, "\\1") - // replace '~text~' - .replace(strike2, "\\1") - // replace 'text' - .replace(del, "\\1") - // replace '```code```' - .replace(multileLineCode, "\\1") - // replace '`code`' - .replace(singleLinecode, "\\1"); - - return subtitle.size() > 0 ? subtitle : QStringLiteral(" "); -} - int NeoChatRoom::savedTopVisibleIndex() const { return firstDisplayedMarker() == historyEdge() ? 0 : int(firstDisplayedMarker() - messageEvents().rbegin()); @@ -451,7 +413,7 @@ QString NeoChatRoom::avatarMediaId() const return {}; } -QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format, bool removeReply) const +QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format, bool stripNewlines) const { const bool prettyPrint = (format == Qt::RichText); @@ -462,53 +424,43 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format, return visit( #endif evt, - [this, prettyPrint, removeReply](const RoomMessageEvent &e) { + [this, format, stripNewlines](const RoomMessageEvent &e) { using namespace MessageEventContent; - // 1. prettyPrint/HTML - if (prettyPrint && e.hasTextContent() && e.mimeType().name() != "text/plain") { - auto htmlBody = static_cast(e.content())->body; - if (removeReply) { - htmlBody.remove(utils::removeRichReplyRegex); - } - htmlBody.replace(utils::userPillRegExp, R"(\1)"); - htmlBody.replace(utils::strikethroughRegExp, "\\1"); - - auto url = connection()->homeserver(); - auto base = url.scheme() + QStringLiteral("://") + url.host() + (url.port() != -1 ? ':' + QString::number(url.port()) : QString()); - htmlBody.replace(utils::mxcImageRegExp, QStringLiteral(R"( )").arg(base)); - - return htmlBody; - } + TextHandler textHandler; if (e.hasFileContent()) { - auto fileCaption = e.content()->fileInfo()->originalName.toHtmlEscaped(); + auto fileCaption = e.content()->fileInfo()->originalName; if (fileCaption.isEmpty()) { - fileCaption = prettyPrint ? Quotient::prettyPrint(e.plainBody()) : e.plainBody(); + fileCaption = e.plainBody(); } else if (e.content()->fileInfo()->originalName != e.plainBody()) { fileCaption = e.plainBody() + " | " + fileCaption; } - return !fileCaption.isEmpty() ? fileCaption : i18n("a file"); + textHandler.setData(fileCaption); + return !fileCaption.isEmpty() ? textHandler.handleRecievePlainText() : i18n("a file"); } - // 2. prettyPrint/text 3. plainText/HTML 4. plainText/text - QString plainBody; - if (e.hasTextContent() && e.content() && e.mimeType().name() == "text/plain") { // 2/4 - plainBody = static_cast(e.content())->body; - } else { // 3 - plainBody = e.plainBody(); + QString body; + if (e.hasTextContent() && e.content()) { + body = static_cast(e.content())->body; + } else { + body = e.plainBody(); } - if (prettyPrint) { - if (removeReply) { - plainBody.remove(utils::removeReplyRegex); - } - return Quotient::prettyPrint(plainBody); + textHandler.setData(body); + + Qt::TextFormat inputFormat; + if (e.mimeType().name() == "text/plain") { + inputFormat = Qt::PlainText; + } else { + inputFormat = Qt::RichText; } - if (removeReply) { - return plainBody.remove(utils::removeReplyRegex); + + if (format == Qt::RichText) { + return textHandler.handleRecieveRichText(inputFormat, this, &e, stripNewlines); + } else { + return textHandler.handleRecievePlainText(inputFormat, stripNewlines); } - return plainBody; }, [](const StickerEvent &e) { return e.body(); diff --git a/src/neochatroom.h b/src/neochatroom.h index 3320d96fd..abc84389d 100644 --- a/src/neochatroom.h +++ b/src/neochatroom.h @@ -124,7 +124,7 @@ public: /// /// \see lastEvent /// \see lastEventIsSpoiler - [[nodiscard]] QString lastEventToString() const; + [[nodiscard]] QString lastEventToString(Qt::TextFormat format = Qt::PlainText, bool stripNewlines = false) const; /// Convenient way to check if the last event looks like it has spoilers. /// @@ -137,12 +137,6 @@ public: /// \see lastEvent [[nodiscard]] QDateTime lastActiveTime(); - /// Get subtitle text for room - /// - /// Fetches last event and removes markdown formatting - /// \see lastEventToString - [[nodiscard]] QString subtitleText(); - [[nodiscard]] bool isSpace(); bool isEventHighlighted(const Quotient::RoomEvent *e) const; @@ -262,7 +256,7 @@ public: [[nodiscard]] QString avatarMediaId() const; - [[nodiscard]] QString eventToString(const Quotient::RoomEvent &evt, Qt::TextFormat format = Qt::PlainText, bool removeReply = true) const; + [[nodiscard]] QString eventToString(const Quotient::RoomEvent &evt, Qt::TextFormat format = Qt::PlainText, bool stripNewlines = false) const; [[nodiscard]] QString eventToGenericString(const Quotient::RoomEvent &evt) const; Q_INVOKABLE [[nodiscard]] bool containsUser(const QString &userID) const; diff --git a/src/notificationsmanager.cpp b/src/notificationsmanager.cpp index c0373b58a..cffed6a1a 100644 --- a/src/notificationsmanager.cpp +++ b/src/notificationsmanager.cpp @@ -22,11 +22,11 @@ #include #include -#include "actionshandler.h" #include "controller.h" #include "neochatconfig.h" #include "neochatroom.h" #include "roommanager.h" +#include "texthandler.h" #include "windowcontroller.h" using namespace Quotient; @@ -85,7 +85,9 @@ void NotificationsManager::postNotification(NeoChatRoom *room, std::unique_ptr replyAction(new KNotificationReplyAction(i18n("Reply"))); replyAction->setPlaceholderText(i18n("Reply...")); connect(replyAction.get(), &KNotificationReplyAction::replied, this, [room, replyEventId](const QString &text) { - room->postMessage(text, markdownToHTML(text), RoomMessageEvent::MsgType::Text, replyEventId, QString()); + TextHandler textHandler; + textHandler.setData(text); + room->postMessage(text, textHandler.handleSendText(), RoomMessageEvent::MsgType::Text, replyEventId, QString()); }); notification->setReplyAction(std::move(replyAction)); } diff --git a/src/qml/Component/Timeline/RichLabel.qml b/src/qml/Component/Timeline/RichLabel.qml index 6a623c40a..3f2537bfb 100644 --- a/src/qml/Component/Timeline/RichLabel.qml +++ b/src/qml/Component/Timeline/RichLabel.qml @@ -16,25 +16,7 @@ TextEdit { property bool isEmote: false property bool isReplyLabel: false - - readonly property var linkRegex: /(href=["'])?(\b(https?):\/\/[^\s\<\>\"\'\\\?\:\)\(]+(\(.*?\))*(\?(?=[a-z])[^\s\\\)]+|$)?)/g - property string textMessage: model.display.includes("http") - ? model.display.replace(linkRegex, function() { - if (arguments[0].includes("/_matrix/media/r0/download/")) { - return arguments[0]; - } - if (arguments[1]) { - return arguments[0]; - } - const l = arguments[2]; - if ([".", ","].includes(l[l.length-1])) { - const link = l.substring(0, l.length-1); - const leftover = l[l.length-1]; - return `${link}${leftover}`; - } - return `${l}`; - }) - : model.display + property string textMessage: model.display property bool spoilerRevealed: !hasSpoiler.test(textMessage) ListView.onReused: Qt.binding(() => !hasSpoiler.test(textMessage)) @@ -46,6 +28,7 @@ TextEdit { Controller.forceRefreshTextDocument(contentLabel.textDocument, contentLabel) } + onTextChanged: console.log(text) text: "