Refactor LinkPreviewer
Refactor `LinkPreviewer` to take an event and put the functions for getting the link in the class itself. This means the functions in `EventHandler` are no longer required. This mr also sets up `LinkPreviewer` so that it is automatically updated when an event is edited. This includes changing the link if edited, and it can handle a message having a previous link removed or a one added when one didn't exist before. Also adds test suite.
This commit is contained in:
@@ -76,3 +76,9 @@ ecm_add_test(
|
|||||||
LINK_LIBRARIES neochat Qt::Test
|
LINK_LIBRARIES neochat Qt::Test
|
||||||
TEST_NAME reactionmodeltest
|
TEST_NAME reactionmodeltest
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ecm_add_test(
|
||||||
|
linkpreviewertest.cpp
|
||||||
|
LINK_LIBRARIES neochat Qt::Test
|
||||||
|
TEST_NAME linkpreviewertest
|
||||||
|
)
|
||||||
|
|||||||
14
autotests/data/test-invalidmatrixtolink-event.json
Normal file
14
autotests/data/test-invalidmatrixtolink-event.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"body": "https://matrix.to/#/@alice:example.org",
|
||||||
|
"msgtype": "m.text"
|
||||||
|
},
|
||||||
|
"event_id": "$validlink1:example.org",
|
||||||
|
"origin_server_ts": 1432735824654,
|
||||||
|
"room_id": "!test:example.org",
|
||||||
|
"sender": "@example:example.org",
|
||||||
|
"type": "m.room.message",
|
||||||
|
"unsigned": {
|
||||||
|
"age": 1234
|
||||||
|
}
|
||||||
|
}
|
||||||
14
autotests/data/test-invalidmxclink-event.json
Normal file
14
autotests/data/test-invalidmxclink-event.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"body": "mxc://example.org/SEsfnsuifSDFSSEF",
|
||||||
|
"msgtype": "m.text"
|
||||||
|
},
|
||||||
|
"event_id": "$validlink1:example.org",
|
||||||
|
"origin_server_ts": 1432735824654,
|
||||||
|
"room_id": "!test:example.org",
|
||||||
|
"sender": "@example:example.org",
|
||||||
|
"type": "m.room.message",
|
||||||
|
"unsigned": {
|
||||||
|
"age": 1234
|
||||||
|
}
|
||||||
|
}
|
||||||
14
autotests/data/test-invalidnospacelink-event.json
Normal file
14
autotests/data/test-invalidnospacelink-event.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"body": "testhttps://kde.org",
|
||||||
|
"msgtype": "m.text"
|
||||||
|
},
|
||||||
|
"event_id": "$validlink1:example.org",
|
||||||
|
"origin_server_ts": 1432735824654,
|
||||||
|
"room_id": "!test:example.org",
|
||||||
|
"sender": "@example:example.org",
|
||||||
|
"type": "m.room.message",
|
||||||
|
"unsigned": {
|
||||||
|
"age": 1234
|
||||||
|
}
|
||||||
|
}
|
||||||
24
autotests/data/test-linkpreviewerintial-sync.json
Normal file
24
autotests/data/test-linkpreviewerintial-sync.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"timeline": {
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"body": "https://kde.org",
|
||||||
|
"format": "org.matrix.custom.html",
|
||||||
|
"formatted_body": "https://kde.org",
|
||||||
|
"msgtype": "m.text"
|
||||||
|
},
|
||||||
|
"origin_server_ts": 1704648567967,
|
||||||
|
"sender": "@example:example.org",
|
||||||
|
"type": "m.room.message",
|
||||||
|
"unsigned": {
|
||||||
|
"age": 112
|
||||||
|
},
|
||||||
|
"event_id": "$validlink:example.org",
|
||||||
|
"room_id": "!test:example.org"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"limited": true,
|
||||||
|
"prev_batch": "t34-23535_0_0"
|
||||||
|
}
|
||||||
|
}
|
||||||
35
autotests/data/test-linkpreviewerreplace-sync.json
Normal file
35
autotests/data/test-linkpreviewerreplace-sync.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"timeline": {
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"body": "* ",
|
||||||
|
"format": "org.matrix.custom.html",
|
||||||
|
"formatted_body": "no link",
|
||||||
|
"m.new_content": {
|
||||||
|
"body": "",
|
||||||
|
"format": "org.matrix.custom.html",
|
||||||
|
"formatted_body": "no link",
|
||||||
|
"msgtype": "m.text"
|
||||||
|
},
|
||||||
|
"m.relates_to": {
|
||||||
|
"event_id": "$validlink:example.org",
|
||||||
|
"rel_type": "m.replace"
|
||||||
|
},
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"type": "m.room.message"
|
||||||
|
},
|
||||||
|
"origin_server_ts": 1704648614969,
|
||||||
|
"sender": "@example:example.org",
|
||||||
|
"type": "m.room.message",
|
||||||
|
"unsigned": {
|
||||||
|
"age": 65
|
||||||
|
},
|
||||||
|
"event_id": "$nolink:example.org",
|
||||||
|
"room_id": "!test:example.org"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"limited": true,
|
||||||
|
"prev_batch": "t34-23535_0_0"
|
||||||
|
}
|
||||||
|
}
|
||||||
14
autotests/data/test-multiplelink-event.json
Normal file
14
autotests/data/test-multiplelink-event.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"body": "www.example.org https://kde.org",
|
||||||
|
"msgtype": "m.text"
|
||||||
|
},
|
||||||
|
"event_id": "$validlink1:example.org",
|
||||||
|
"origin_server_ts": 1432735824654,
|
||||||
|
"room_id": "!test:example.org",
|
||||||
|
"sender": "@example:example.org",
|
||||||
|
"type": "m.room.message",
|
||||||
|
"unsigned": {
|
||||||
|
"age": 1234
|
||||||
|
}
|
||||||
|
}
|
||||||
14
autotests/data/test-validplainlink-event.json
Normal file
14
autotests/data/test-validplainlink-event.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"body": "https://kde.org",
|
||||||
|
"msgtype": "m.text"
|
||||||
|
},
|
||||||
|
"event_id": "$validlink1:example.org",
|
||||||
|
"origin_server_ts": 1432735824654,
|
||||||
|
"room_id": "!test:example.org",
|
||||||
|
"sender": "@example:example.org",
|
||||||
|
"type": "m.room.message",
|
||||||
|
"unsigned": {
|
||||||
|
"age": 1234
|
||||||
|
}
|
||||||
|
}
|
||||||
14
autotests/data/test-validplainwwwlink-event.json
Normal file
14
autotests/data/test-validplainwwwlink-event.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"body": "www.example.org",
|
||||||
|
"msgtype": "m.text"
|
||||||
|
},
|
||||||
|
"event_id": "$validlink1:example.org",
|
||||||
|
"origin_server_ts": 1432735824654,
|
||||||
|
"room_id": "!test:example.org",
|
||||||
|
"sender": "@example:example.org",
|
||||||
|
"type": "m.room.message",
|
||||||
|
"unsigned": {
|
||||||
|
"age": 1234
|
||||||
|
}
|
||||||
|
}
|
||||||
16
autotests/data/test-validrichlink-event.json
Normal file
16
autotests/data/test-validrichlink-event.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"body": "[Rich Link](https://kde.org)",
|
||||||
|
"format": "org.matrix.custom.html",
|
||||||
|
"formatted_body": "<a href=\"https://kde.org\">Rich Link</a>",
|
||||||
|
"msgtype": "m.text"
|
||||||
|
},
|
||||||
|
"event_id": "$validlink1:example.org",
|
||||||
|
"origin_server_ts": 1432735824654,
|
||||||
|
"room_id": "!test:example.org",
|
||||||
|
"sender": "@example:example.org",
|
||||||
|
"type": "m.room.message",
|
||||||
|
"unsigned": {
|
||||||
|
"age": 1234
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -65,8 +65,6 @@ private Q_SLOTS:
|
|||||||
void nullSubtitle();
|
void nullSubtitle();
|
||||||
void mediaInfo();
|
void mediaInfo();
|
||||||
void nullMediaInfo();
|
void nullMediaInfo();
|
||||||
void linkPreviewer();
|
|
||||||
void nullLinkPreviewer();
|
|
||||||
void hasReply();
|
void hasReply();
|
||||||
void nullHasReply();
|
void nullHasReply();
|
||||||
void replyId();
|
void replyId();
|
||||||
@@ -399,28 +397,6 @@ void EventHandlerTest::nullMediaInfo()
|
|||||||
QCOMPARE(noEventHandler.getMediaInfo(), QVariantMap());
|
QCOMPARE(noEventHandler.getMediaInfo(), QVariantMap());
|
||||||
}
|
}
|
||||||
|
|
||||||
void EventHandlerTest::linkPreviewer()
|
|
||||||
{
|
|
||||||
auto event = room->messageEvents().at(2).get();
|
|
||||||
eventHandler.setEvent(event);
|
|
||||||
|
|
||||||
QCOMPARE(eventHandler.getLinkPreviewer()->url(), QUrl("https://kde.org"_ls));
|
|
||||||
|
|
||||||
event = room->messageEvents().at(0).get();
|
|
||||||
eventHandler.setEvent(event);
|
|
||||||
|
|
||||||
QCOMPARE(eventHandler.getLinkPreviewer(), nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
void EventHandlerTest::nullLinkPreviewer()
|
|
||||||
{
|
|
||||||
QTest::ignoreMessage(QtWarningMsg, "getLinkPreviewer called with m_room set to nullptr.");
|
|
||||||
QCOMPARE(emptyHandler.getLinkPreviewer(), nullptr);
|
|
||||||
|
|
||||||
QTest::ignoreMessage(QtWarningMsg, "getLinkPreviewer called with m_event set to nullptr.");
|
|
||||||
QCOMPARE(noEventHandler.getLinkPreviewer(), nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
void EventHandlerTest::hasReply()
|
void EventHandlerTest::hasReply()
|
||||||
{
|
{
|
||||||
auto event = room->messageEvents().at(5).get();
|
auto event = room->messageEvents().at(5).get();
|
||||||
|
|||||||
104
autotests/linkpreviewertest.cpp
Normal file
104
autotests/linkpreviewertest.cpp
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QTest>
|
||||||
|
|
||||||
|
#include "linkpreviewer.h"
|
||||||
|
|
||||||
|
#include <Quotient/events/roommessageevent.h>
|
||||||
|
#include <Quotient/quotient_common.h>
|
||||||
|
#include <Quotient/syncdata.h>
|
||||||
|
|
||||||
|
#include "utils.h"
|
||||||
|
|
||||||
|
#include "testutils.h"
|
||||||
|
|
||||||
|
using namespace Quotient;
|
||||||
|
|
||||||
|
class LinkPreviewerTest : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private:
|
||||||
|
Connection *connection = nullptr;
|
||||||
|
TestUtils::TestRoom *room = nullptr;
|
||||||
|
|
||||||
|
private Q_SLOTS:
|
||||||
|
void initTestCase();
|
||||||
|
|
||||||
|
void linkPreviewsMatch_data();
|
||||||
|
void linkPreviewsMatch();
|
||||||
|
|
||||||
|
void linkPreviewsReject_data();
|
||||||
|
void linkPreviewsReject();
|
||||||
|
|
||||||
|
void editedLink();
|
||||||
|
};
|
||||||
|
|
||||||
|
void LinkPreviewerTest::initTestCase()
|
||||||
|
{
|
||||||
|
connection = Connection::makeMockConnection(QStringLiteral("@bob:example.org"));
|
||||||
|
room = new TestUtils::TestRoom(connection, QStringLiteral("!test:example.org"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinkPreviewerTest::linkPreviewsMatch_data()
|
||||||
|
{
|
||||||
|
QTest::addColumn<QString>("eventSource");
|
||||||
|
QTest::addColumn<QUrl>("testOutputLink");
|
||||||
|
|
||||||
|
QTest::newRow("plainHttps") << QStringLiteral("test-validplainlink-event.json") << QUrl("https://kde.org"_ls);
|
||||||
|
QTest::newRow("richHttps") << QStringLiteral("test-validrichlink-event.json") << QUrl("https://kde.org"_ls);
|
||||||
|
QTest::newRow("plainWww") << QStringLiteral("test-validplainwwwlink-event.json") << QUrl("www.example.org"_ls);
|
||||||
|
QTest::newRow("multipleHttps") << QStringLiteral("test-multiplelink-event.json") << QUrl("www.example.org"_ls);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinkPreviewerTest::linkPreviewsMatch()
|
||||||
|
{
|
||||||
|
QFETCH(QString, eventSource);
|
||||||
|
QFETCH(QUrl, testOutputLink);
|
||||||
|
|
||||||
|
auto event = TestUtils::loadEventFromFile<RoomMessageEvent>(eventSource);
|
||||||
|
auto linkPreviewer = LinkPreviewer(room, event.get());
|
||||||
|
|
||||||
|
QCOMPARE(linkPreviewer.empty(), false);
|
||||||
|
QCOMPARE(linkPreviewer.url(), testOutputLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinkPreviewerTest::linkPreviewsReject_data()
|
||||||
|
{
|
||||||
|
QTest::addColumn<QString>("eventSource");
|
||||||
|
|
||||||
|
QTest::newRow("mxc") << QStringLiteral("test-invalidmxclink-event.json");
|
||||||
|
QTest::newRow("matrixTo") << QStringLiteral("test-invalidmatrixtolink-event.json");
|
||||||
|
QTest::newRow("noSpace") << QStringLiteral("test-invalidnospacelink-event.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinkPreviewerTest::linkPreviewsReject()
|
||||||
|
{
|
||||||
|
QFETCH(QString, eventSource);
|
||||||
|
|
||||||
|
auto event = TestUtils::loadEventFromFile<RoomMessageEvent>(eventSource);
|
||||||
|
auto linkPreviewer = LinkPreviewer(room, event.get());
|
||||||
|
|
||||||
|
QCOMPARE(linkPreviewer.empty(), true);
|
||||||
|
QCOMPARE(linkPreviewer.url(), QUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinkPreviewerTest::editedLink()
|
||||||
|
{
|
||||||
|
room->syncNewEvents(QStringLiteral("test-linkpreviewerintial-sync.json"));
|
||||||
|
auto event = eventCast<const RoomMessageEvent>(room->messageEvents().at(0).get());
|
||||||
|
auto linkPreviewer = LinkPreviewer(room, event);
|
||||||
|
|
||||||
|
QCOMPARE(linkPreviewer.empty(), false);
|
||||||
|
QCOMPARE(linkPreviewer.url(), QUrl("https://kde.org"_ls));
|
||||||
|
|
||||||
|
room->syncNewEvents(QStringLiteral("test-linkpreviewerreplace-sync.json"));
|
||||||
|
|
||||||
|
QCOMPARE(linkPreviewer.empty(), true);
|
||||||
|
QCOMPARE(linkPreviewer.url(), QUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
QTEST_MAIN(LinkPreviewerTest)
|
||||||
|
#include "linkpreviewertest.moc"
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#include <Quotient/events/event.h>
|
||||||
#include <Quotient/syncdata.h>
|
#include <Quotient/syncdata.h>
|
||||||
|
|
||||||
#include "neochatroom.h"
|
#include "neochatroom.h"
|
||||||
@@ -38,4 +39,17 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
template<Quotient::EventClass EventT>
|
||||||
|
inline Quotient::event_ptr_tt<EventT> loadEventFromFile(const QString &eventFileName)
|
||||||
|
{
|
||||||
|
if (!eventFileName.isEmpty()) {
|
||||||
|
QFile testEventFile;
|
||||||
|
testEventFile.setFileName(QLatin1String(DATA_DIR) + u'/' + eventFileName);
|
||||||
|
testEventFile.open(QIODevice::ReadOnly);
|
||||||
|
auto testSyncJson = QJsonDocument::fromJson(testEventFile.readAll()).object();
|
||||||
|
return Quotient::loadEvent<EventT>(testSyncJson);
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,11 +64,6 @@ private Q_SLOTS:
|
|||||||
void receiveRichEdited();
|
void receiveRichEdited();
|
||||||
void receiveLineSeparator();
|
void receiveLineSeparator();
|
||||||
void receiveRichCodeUrl();
|
void receiveRichCodeUrl();
|
||||||
|
|
||||||
void linkPreviewsMatch_data();
|
|
||||||
void linkPreviewsMatch();
|
|
||||||
void linkPreviewsReject_data();
|
|
||||||
void linkPreviewsReject();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
void TextHandlerTest::initTestCase()
|
void TextHandlerTest::initTestCase()
|
||||||
@@ -523,53 +518,6 @@ void TextHandlerTest::receiveLineSeparator()
|
|||||||
QCOMPARE(textHandler.handleRecievePlainText(Qt::PlainText, true), QStringLiteral("foo bar"));
|
QCOMPARE(textHandler.handleRecievePlainText(Qt::PlainText, true), QStringLiteral("foo bar"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextHandlerTest::linkPreviewsMatch_data()
|
|
||||||
{
|
|
||||||
QTest::addColumn<QString>("testInputString");
|
|
||||||
QTest::addColumn<QList<QUrl>>("testOutputLinks");
|
|
||||||
|
|
||||||
QTest::newRow("plainHttps") << QStringLiteral("https://kde.org") << QList<QUrl>({QUrl("https://kde.org"_ls)});
|
|
||||||
QTest::newRow("richHttps") << QStringLiteral("<a href=\"https://kde.org\">Rich Link</a>") << QList<QUrl>({QUrl("https://kde.org"_ls)});
|
|
||||||
QTest::newRow("plainWww") << QStringLiteral("www.example.org") << QList<QUrl>({QUrl("www.example.org"_ls)});
|
|
||||||
QTest::newRow("multipleHttps") << QStringLiteral("https://kde.org www.example.org")
|
|
||||||
<< QList<QUrl>({
|
|
||||||
QUrl("https://kde.org"_ls),
|
|
||||||
QUrl("www.example.org"_ls),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void TextHandlerTest::linkPreviewsMatch()
|
|
||||||
{
|
|
||||||
QFETCH(QString, testInputString);
|
|
||||||
QFETCH(QList<QUrl>, testOutputLinks);
|
|
||||||
|
|
||||||
TextHandler testTextHandler;
|
|
||||||
testTextHandler.setData(testInputString);
|
|
||||||
|
|
||||||
QCOMPARE(testTextHandler.getLinkPreviews(), testOutputLinks);
|
|
||||||
}
|
|
||||||
|
|
||||||
void TextHandlerTest::linkPreviewsReject_data()
|
|
||||||
{
|
|
||||||
QTest::addColumn<QString>("testInputString");
|
|
||||||
QTest::addColumn<QList<QUrl>>("testOutputLinks");
|
|
||||||
|
|
||||||
QTest::newRow("mxc") << QStringLiteral("mxc://example.org/SEsfnsuifSDFSSEF") << QList<QUrl>();
|
|
||||||
QTest::newRow("matrixTo") << QStringLiteral("https://matrix.to/#/@alice:example.org") << QList<QUrl>();
|
|
||||||
QTest::newRow("noSpace") << QStringLiteral("testhttps://kde.org") << QList<QUrl>();
|
|
||||||
}
|
|
||||||
|
|
||||||
void TextHandlerTest::linkPreviewsReject()
|
|
||||||
{
|
|
||||||
QFETCH(QString, testInputString);
|
|
||||||
QFETCH(QList<QUrl>, testOutputLinks);
|
|
||||||
|
|
||||||
TextHandler testTextHandler;
|
|
||||||
testTextHandler.setData(testInputString);
|
|
||||||
|
|
||||||
QCOMPARE(testTextHandler.getLinkPreviews(), testOutputLinks);
|
|
||||||
}
|
|
||||||
|
|
||||||
void TextHandlerTest::receiveRichCodeUrl()
|
void TextHandlerTest::receiveRichCodeUrl()
|
||||||
{
|
{
|
||||||
auto input = QStringLiteral("<code>https://kde.org</code>");
|
auto input = QStringLiteral("<code>https://kde.org</code>");
|
||||||
|
|||||||
@@ -777,43 +777,6 @@ QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo
|
|||||||
return mediaInfo;
|
return mediaInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
QSharedPointer<LinkPreviewer> EventHandler::getLinkPreviewer() const
|
|
||||||
{
|
|
||||||
if (m_room == nullptr) {
|
|
||||||
qCWarning(EventHandling) << "getLinkPreviewer called with m_room set to nullptr.";
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
if (m_event == nullptr) {
|
|
||||||
qCWarning(EventHandling) << "getLinkPreviewer called with m_event set to nullptr.";
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
if (!m_event->is<RoomMessageEvent>()) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString text;
|
|
||||||
auto event = eventCast<const RoomMessageEvent>(m_event);
|
|
||||||
if (event->hasTextContent()) {
|
|
||||||
auto textContent = static_cast<const EventContent::TextContent *>(event->content());
|
|
||||||
if (textContent) {
|
|
||||||
text = textContent->body;
|
|
||||||
} else {
|
|
||||||
text = event->plainBody();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
text = event->plainBody();
|
|
||||||
}
|
|
||||||
TextHandler textHandler;
|
|
||||||
textHandler.setData(text);
|
|
||||||
|
|
||||||
QList<QUrl> links = textHandler.getLinkPreviews();
|
|
||||||
if (links.size() > 0) {
|
|
||||||
return QSharedPointer<LinkPreviewer>(new LinkPreviewer(nullptr, m_room, links.size() > 0 ? links[0] : QUrl()));
|
|
||||||
} else {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool EventHandler::hasReply() const
|
bool EventHandler::hasReply() const
|
||||||
{
|
{
|
||||||
if (m_event == nullptr) {
|
if (m_event == nullptr) {
|
||||||
|
|||||||
@@ -231,16 +231,6 @@ public:
|
|||||||
*/
|
*/
|
||||||
QVariantMap getMediaInfo() const;
|
QVariantMap getMediaInfo() const;
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Return a LinkPreviewer object for the event.
|
|
||||||
*
|
|
||||||
* A nullptr will be returned for any event that doesn't have any links so the
|
|
||||||
* return should be null checked and an empty LinkPreviewer provided if null.
|
|
||||||
*
|
|
||||||
* @sa LinkPreviewer
|
|
||||||
*/
|
|
||||||
QSharedPointer<LinkPreviewer> getLinkPreviewer() const;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Whether the event is a reply to another in the timeline.
|
* @brief Whether the event is a reply to another in the timeline.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,25 +3,37 @@
|
|||||||
|
|
||||||
#include "linkpreviewer.h"
|
#include "linkpreviewer.h"
|
||||||
|
|
||||||
#include "controller.h"
|
|
||||||
|
|
||||||
#include <Quotient/connection.h>
|
#include <Quotient/connection.h>
|
||||||
#include <Quotient/csapi/content-repo.h>
|
#include <Quotient/csapi/content-repo.h>
|
||||||
|
#include <Quotient/events/roommessageevent.h>
|
||||||
|
|
||||||
#include "neochatconfig.h"
|
#include "neochatconfig.h"
|
||||||
#include "neochatroom.h"
|
#include "neochatroom.h"
|
||||||
|
#include "utils.h"
|
||||||
|
|
||||||
using namespace Quotient;
|
using namespace Quotient;
|
||||||
|
|
||||||
LinkPreviewer::LinkPreviewer(QObject *parent, const NeoChatRoom *room, const QUrl &url)
|
LinkPreviewer::LinkPreviewer(const NeoChatRoom *room, const Quotient::RoomMessageEvent *event)
|
||||||
: QObject(parent)
|
: QObject(nullptr)
|
||||||
, m_currentRoom(room)
|
, m_currentRoom(room)
|
||||||
|
, m_event(event)
|
||||||
, m_loaded(false)
|
, m_loaded(false)
|
||||||
, m_url(url)
|
, m_url(linkPreview(event))
|
||||||
{
|
{
|
||||||
loadUrlPreview();
|
connect(this, &LinkPreviewer::urlChanged, this, &LinkPreviewer::emptyChanged);
|
||||||
if (m_currentRoom) {
|
|
||||||
|
if (m_event != nullptr && m_currentRoom != nullptr) {
|
||||||
|
loadUrlPreview();
|
||||||
connect(m_currentRoom, &NeoChatRoom::urlPreviewEnabledChanged, this, &LinkPreviewer::loadUrlPreview);
|
connect(m_currentRoom, &NeoChatRoom::urlPreviewEnabledChanged, this, &LinkPreviewer::loadUrlPreview);
|
||||||
|
// Make sure that we react to edits
|
||||||
|
connect(m_currentRoom, &NeoChatRoom::replacedEvent, this, [this](const Quotient::RoomEvent *newEvent) {
|
||||||
|
if (m_event->id() == newEvent->id()) {
|
||||||
|
m_event = eventCast<const Quotient::RoomMessageEvent>(newEvent);
|
||||||
|
m_url = linkPreview(m_event);
|
||||||
|
Q_EMIT urlChanged();
|
||||||
|
loadUrlPreview();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
connect(NeoChatConfig::self(), &NeoChatConfig::ShowLinkPreviewChanged, this, &LinkPreviewer::loadUrlPreview);
|
connect(NeoChatConfig::self(), &NeoChatConfig::ShowLinkPreviewChanged, this, &LinkPreviewer::loadUrlPreview);
|
||||||
}
|
}
|
||||||
@@ -51,15 +63,6 @@ QUrl LinkPreviewer::url() const
|
|||||||
return m_url;
|
return m_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
void LinkPreviewer::setUrl(QUrl url)
|
|
||||||
{
|
|
||||||
if (url != m_url) {
|
|
||||||
m_url = url;
|
|
||||||
urlChanged();
|
|
||||||
loadUrlPreview();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void LinkPreviewer::loadUrlPreview()
|
void LinkPreviewer::loadUrlPreview()
|
||||||
{
|
{
|
||||||
if (!m_currentRoom || !NeoChatConfig::showLinkPreview() || !m_currentRoom->urlPreviewEnabled()) {
|
if (!m_currentRoom || !NeoChatConfig::showLinkPreview() || !m_currentRoom->urlPreviewEnabled()) {
|
||||||
@@ -98,4 +101,38 @@ bool LinkPreviewer::empty() const
|
|||||||
return m_url.isEmpty();
|
return m_url.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QUrl LinkPreviewer::linkPreview(const Quotient::RoomMessageEvent *event)
|
||||||
|
{
|
||||||
|
if (event == nullptr) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QString text;
|
||||||
|
if (event->hasTextContent()) {
|
||||||
|
auto textContent = static_cast<const Quotient::EventContent::TextContent *>(event->content());
|
||||||
|
if (textContent) {
|
||||||
|
text = textContent->body;
|
||||||
|
} else {
|
||||||
|
text = event->plainBody();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
text = event->plainBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto data = text.remove(TextRegex::removeRichReply);
|
||||||
|
auto linksMatch = TextRegex::url.globalMatch(data);
|
||||||
|
while (linksMatch.hasNext()) {
|
||||||
|
auto link = linksMatch.next().captured();
|
||||||
|
if (!link.contains(QStringLiteral("matrix.to"))) {
|
||||||
|
return QUrl(link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LinkPreviewer::hasPreviewableLinks(const Quotient::RoomMessageEvent *event)
|
||||||
|
{
|
||||||
|
return !linkPreview(event).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
#include "moc_linkpreviewer.cpp"
|
#include "moc_linkpreviewer.cpp"
|
||||||
|
|||||||
@@ -7,6 +7,11 @@
|
|||||||
#include <QQmlEngine>
|
#include <QQmlEngine>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
|
||||||
|
namespace Quotient
|
||||||
|
{
|
||||||
|
class RoomMessageEvent;
|
||||||
|
}
|
||||||
|
|
||||||
class NeoChatRoom;
|
class NeoChatRoom;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,7 +30,7 @@ class LinkPreviewer : public QObject
|
|||||||
/**
|
/**
|
||||||
* @brief The URL to get the preview for.
|
* @brief The URL to get the preview for.
|
||||||
*/
|
*/
|
||||||
Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged)
|
Q_PROPERTY(QUrl url READ url NOTIFY urlChanged)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Whether the preview information has been loaded.
|
* @brief Whether the preview information has been loaded.
|
||||||
@@ -55,18 +60,25 @@ class LinkPreviewer : public QObject
|
|||||||
Q_PROPERTY(bool empty READ empty NOTIFY emptyChanged)
|
Q_PROPERTY(bool empty READ empty NOTIFY emptyChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit LinkPreviewer(QObject *parent = nullptr, const NeoChatRoom *room = nullptr, const QUrl &url = {});
|
explicit LinkPreviewer(const NeoChatRoom *room = nullptr, const Quotient::RoomMessageEvent *event = nullptr);
|
||||||
|
|
||||||
[[nodiscard]] QUrl url() const;
|
[[nodiscard]] QUrl url() const;
|
||||||
void setUrl(QUrl);
|
|
||||||
[[nodiscard]] bool loaded() const;
|
[[nodiscard]] bool loaded() const;
|
||||||
[[nodiscard]] QString title() const;
|
[[nodiscard]] QString title() const;
|
||||||
[[nodiscard]] QString description() const;
|
[[nodiscard]] QString description() const;
|
||||||
[[nodiscard]] QUrl imageSource() const;
|
[[nodiscard]] QUrl imageSource() const;
|
||||||
[[nodiscard]] bool empty() const;
|
[[nodiscard]] bool empty() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the given event has at least 1 pre-viewable link.
|
||||||
|
*
|
||||||
|
* A link is only pre-viewable if it is http, https or something starting with www.
|
||||||
|
*/
|
||||||
|
static bool hasPreviewableLinks(const Quotient::RoomMessageEvent *event);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const NeoChatRoom *m_currentRoom = nullptr;
|
const NeoChatRoom *m_currentRoom;
|
||||||
|
const Quotient::RoomMessageEvent *m_event;
|
||||||
|
|
||||||
bool m_loaded;
|
bool m_loaded;
|
||||||
QString m_title = QString();
|
QString m_title = QString();
|
||||||
@@ -76,6 +88,14 @@ private:
|
|||||||
|
|
||||||
void loadUrlPreview();
|
void loadUrlPreview();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return the link to be previewed from the given event.
|
||||||
|
*
|
||||||
|
* This function is designed to give only links that should be previewed so
|
||||||
|
* http, https or something starting with www. The first valid link is returned.
|
||||||
|
*/
|
||||||
|
static QUrl linkPreview(const Quotient::RoomMessageEvent *event);
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void loadedChanged();
|
void loadedChanged();
|
||||||
void titleChanged();
|
void titleChanged();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
#include "messageeventmodel.h"
|
#include "messageeventmodel.h"
|
||||||
|
#include "linkpreviewer.h"
|
||||||
#include "messageeventmodel_logging.h"
|
#include "messageeventmodel_logging.h"
|
||||||
|
|
||||||
#include "neochatconfig.h"
|
#include "neochatconfig.h"
|
||||||
@@ -22,6 +23,7 @@
|
|||||||
#include "eventhandler.h"
|
#include "eventhandler.h"
|
||||||
#include "events/pollevent.h"
|
#include "events/pollevent.h"
|
||||||
#include "models/reactionmodel.h"
|
#include "models/reactionmodel.h"
|
||||||
|
#include "texthandler.h"
|
||||||
|
|
||||||
using namespace Quotient;
|
using namespace Quotient;
|
||||||
|
|
||||||
@@ -237,6 +239,10 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
|||||||
});
|
});
|
||||||
connect(m_currentRoom, &Room::replacedEvent, this, [this](const RoomEvent *newEvent) {
|
connect(m_currentRoom, &Room::replacedEvent, this, [this](const RoomEvent *newEvent) {
|
||||||
refreshLastUserEvents(refreshEvent(newEvent->id()) - timelineBaseIndex());
|
refreshLastUserEvents(refreshEvent(newEvent->id()) - timelineBaseIndex());
|
||||||
|
const RoomMessageEvent *message = eventCast<const RoomMessageEvent>(newEvent);
|
||||||
|
if (message != nullptr) {
|
||||||
|
createEventObjects(message);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
connect(m_currentRoom, &Room::updatedEvent, this, [this](const QString &eventId) {
|
connect(m_currentRoom, &Room::updatedEvent, this, [this](const QString &eventId) {
|
||||||
if (eventId.isEmpty()) { // How did we get here?
|
if (eventId.isEmpty()) { // How did we get here?
|
||||||
@@ -720,14 +726,14 @@ void MessageEventModel::createEventObjects(const Quotient::RoomMessageEvent *eve
|
|||||||
{
|
{
|
||||||
auto eventId = event->id();
|
auto eventId = event->id();
|
||||||
|
|
||||||
EventHandler eventHandler;
|
if (m_linkPreviewers.contains(eventId)) {
|
||||||
eventHandler.setRoom(m_currentRoom);
|
if (!LinkPreviewer::hasPreviewableLinks(event)) {
|
||||||
eventHandler.setEvent(event);
|
m_linkPreviewers.remove(eventId);
|
||||||
|
}
|
||||||
if (auto linkPreviewer = eventHandler.getLinkPreviewer()) {
|
|
||||||
m_linkPreviewers[eventId] = linkPreviewer;
|
|
||||||
} else {
|
} else {
|
||||||
m_linkPreviewers.remove(eventId);
|
if (LinkPreviewer::hasPreviewableLinks(event)) {
|
||||||
|
m_linkPreviewers[eventId] = QSharedPointer<LinkPreviewer>(new LinkPreviewer(m_currentRoom, event));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReactionModel handles updates to add and remove reactions, we only need to
|
// ReactionModel handles updates to add and remove reactions, we only need to
|
||||||
|
|||||||
@@ -493,18 +493,4 @@ QString TextHandler::linkifyUrls(QString stringIn)
|
|||||||
return stringIn;
|
return stringIn;
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QUrl> TextHandler::getLinkPreviews()
|
|
||||||
{
|
|
||||||
auto data = m_data.remove(TextRegex::removeRichReply);
|
|
||||||
auto linksMatch = TextRegex::url.globalMatch(data);
|
|
||||||
QList<QUrl> links;
|
|
||||||
while (linksMatch.hasNext()) {
|
|
||||||
auto link = linksMatch.next().captured();
|
|
||||||
if (!link.contains(QStringLiteral("matrix.to"))) {
|
|
||||||
links += QUrl(link);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return links;
|
|
||||||
}
|
|
||||||
|
|
||||||
#include "moc_texthandler.cpp"
|
#include "moc_texthandler.cpp"
|
||||||
|
|||||||
@@ -11,29 +11,9 @@
|
|||||||
|
|
||||||
#include "neochatroom.h"
|
#include "neochatroom.h"
|
||||||
|
|
||||||
namespace TextRegex
|
namespace Quotient
|
||||||
{
|
{
|
||||||
static const QRegularExpression endTagType{QStringLiteral("(>| )")};
|
class RoomMessageEvent;
|
||||||
static const QRegularExpression attributeData{QStringLiteral("['\"](.*?)['\"]")};
|
|
||||||
static const QRegularExpression removeReply{QStringLiteral("> <.*?>.*?\\n\\n"), QRegularExpression::DotMatchesEverythingOption};
|
|
||||||
static const QRegularExpression removeRichReply{QStringLiteral("<mx-reply>.*?</mx-reply>"), QRegularExpression::DotMatchesEverythingOption};
|
|
||||||
static const QRegularExpression codePill{QStringLiteral("<pre><code[^>]*>(.*?)</code></pre>"), QRegularExpression::DotMatchesEverythingOption};
|
|
||||||
static const QRegularExpression userPill{QStringLiteral("(<a href=\"https://matrix.to/#/@.*?:.*?\">.*?</a>)"), QRegularExpression::DotMatchesEverythingOption};
|
|
||||||
static const QRegularExpression blockQuote{QStringLiteral("<blockquote>\n?(?:<p>)?(.*?)(?:</p>)?\n?</blockquote>"),
|
|
||||||
QRegularExpression::DotMatchesEverythingOption};
|
|
||||||
static const QRegularExpression strikethrough{QStringLiteral("<del>(.*?)</del>"), QRegularExpression::DotMatchesEverythingOption};
|
|
||||||
static const QRegularExpression mxcImage{QStringLiteral(R"AAA(<img(.*?)src="mxc:\/\/(.*?)\/(.*?)"(.*?)>)AAA")};
|
|
||||||
static const QRegularExpression plainUrl(
|
|
||||||
QStringLiteral(
|
|
||||||
R"(<a.*?<\/a>(*SKIP)(*F)|\b((www\.(?!\.)(?!(\w|\.|-)+@)|(https?|ftp):(//)?\w|(magnet|matrix):)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))"),
|
|
||||||
QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption);
|
|
||||||
static const QRegularExpression
|
|
||||||
url(QStringLiteral(R"(\b((www\.(?!\.)(?!(\w|\.|-)+@)|https?:(//)?\w)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))"),
|
|
||||||
QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption);
|
|
||||||
static const QRegularExpression emailAddress(QStringLiteral(R"(<a.*?<\/a>(*SKIP)(*F)|\b(mailto:)?((\w|\.|-)+@(\w|\.|-)+\.\w+\b))"),
|
|
||||||
QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption);
|
|
||||||
static const QRegularExpression mxId(QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"),
|
|
||||||
QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -114,14 +94,6 @@ public:
|
|||||||
*/
|
*/
|
||||||
QString handleRecievePlainText(Qt::TextFormat inputFormat = Qt::PlainText, const bool &stripNewlines = false);
|
QString handleRecievePlainText(Qt::TextFormat inputFormat = Qt::PlainText, const bool &stripNewlines = false);
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Return a list of links that can be previewed.
|
|
||||||
*
|
|
||||||
* This function is designed to give only links that should be previewed so
|
|
||||||
* http, https or something starting with www.
|
|
||||||
*/
|
|
||||||
QList<QUrl> getLinkPreviews();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString m_data;
|
QString m_data;
|
||||||
|
|
||||||
|
|||||||
26
src/utils.h
26
src/utils.h
@@ -4,6 +4,7 @@
|
|||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
#include <QPalette>
|
#include <QPalette>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
namespace Utils
|
namespace Utils
|
||||||
{
|
{
|
||||||
@@ -21,3 +22,28 @@ inline QColor getUserColor(qreal hueF)
|
|||||||
return QColor::fromHslF(hueF, 1, -0.7 * lightness + 0.9, 1);
|
return QColor::fromHslF(hueF, 1, -0.7 * lightness + 0.9, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace TextRegex
|
||||||
|
{
|
||||||
|
static const QRegularExpression endTagType{QStringLiteral("(>| )")};
|
||||||
|
static const QRegularExpression attributeData{QStringLiteral("['\"](.*?)['\"]")};
|
||||||
|
static const QRegularExpression removeReply{QStringLiteral("> <.*?>.*?\\n\\n"), QRegularExpression::DotMatchesEverythingOption};
|
||||||
|
static const QRegularExpression removeRichReply{QStringLiteral("<mx-reply>.*?</mx-reply>"), QRegularExpression::DotMatchesEverythingOption};
|
||||||
|
static const QRegularExpression codePill{QStringLiteral("<pre><code[^>]*>(.*?)</code></pre>"), QRegularExpression::DotMatchesEverythingOption};
|
||||||
|
static const QRegularExpression userPill{QStringLiteral("(<a href=\"https://matrix.to/#/@.*?:.*?\">.*?</a>)"), QRegularExpression::DotMatchesEverythingOption};
|
||||||
|
static const QRegularExpression blockQuote{QStringLiteral("<blockquote>\n?(?:<p>)?(.*?)(?:</p>)?\n?</blockquote>"),
|
||||||
|
QRegularExpression::DotMatchesEverythingOption};
|
||||||
|
static const QRegularExpression strikethrough{QStringLiteral("<del>(.*?)</del>"), QRegularExpression::DotMatchesEverythingOption};
|
||||||
|
static const QRegularExpression mxcImage{QStringLiteral(R"AAA(<img(.*?)src="mxc:\/\/(.*?)\/(.*?)"(.*?)>)AAA")};
|
||||||
|
static const QRegularExpression plainUrl(
|
||||||
|
QStringLiteral(
|
||||||
|
R"(<a.*?<\/a>(*SKIP)(*F)|\b((www\.(?!\.)(?!(\w|\.|-)+@)|(https?|ftp):(//)?\w|(magnet|matrix):)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))"),
|
||||||
|
QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption);
|
||||||
|
static const QRegularExpression
|
||||||
|
url(QStringLiteral(R"(\b((www\.(?!\.)(?!(\w|\.|-)+@)|https?:(//)?\w)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))"),
|
||||||
|
QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption);
|
||||||
|
static const QRegularExpression emailAddress(QStringLiteral(R"(<a.*?<\/a>(*SKIP)(*F)|\b(mailto:)?((\w|\.|-)+@(\w|\.|-)+\.\w+\b))"),
|
||||||
|
QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption);
|
||||||
|
static const QRegularExpression mxId(QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"),
|
||||||
|
QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user