Compare commits
38 Commits
v23.04.3
...
work/locat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73869e7b49 | ||
|
|
072f7c6626 | ||
|
|
f11abdeebd | ||
|
|
89141dddf2 | ||
|
|
69f08de0ff | ||
|
|
925e20ebb8 | ||
|
|
ee254a286d | ||
|
|
3cc8d32dd3 | ||
|
|
50cf6d9750 | ||
|
|
cedbb64932 | ||
|
|
b45898a5b6 | ||
|
|
ccb1748ab3 | ||
|
|
11e9af15f7 | ||
|
|
8ad41fec92 | ||
|
|
ac5212ebb2 | ||
|
|
4e16b91f54 | ||
|
|
343f4965ed | ||
|
|
ac775c5aaf | ||
|
|
db17888d42 | ||
|
|
f12d10baa6 | ||
|
|
4289c1345f | ||
|
|
0d6a83b063 | ||
|
|
76b04dcba9 | ||
|
|
64dee7eb12 | ||
|
|
bc84ad8d56 | ||
|
|
afe8a2a5e4 | ||
|
|
87213dc9dd | ||
|
|
300d2428eb | ||
|
|
abe7d70822 | ||
|
|
d8bf26158a | ||
|
|
741cb57105 | ||
|
|
81c73037ca | ||
|
|
f6ba4f2ecd | ||
|
|
23303c0483 | ||
|
|
5a02448326 | ||
|
|
411ae25e80 | ||
|
|
6e1b3fe860 | ||
|
|
4a38d83a68 |
@@ -28,6 +28,17 @@
|
|||||||
"buildsystem": "cmake-ninja",
|
"buildsystem": "cmake-ninja",
|
||||||
"sources": [ { "type": "git", "url": "https://invent.kde.org/libraries/kirigami-addons.git" } ]
|
"sources": [ { "type": "git", "url": "https://invent.kde.org/libraries/kirigami-addons.git" } ]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "kquickcharts",
|
||||||
|
"buildsystem": "cmake-ninja",
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://invent.kde.org/frameworks/kquickcharts.git",
|
||||||
|
"branch": "kf5"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "kquickimageeditor",
|
"name": "kquickimageeditor",
|
||||||
"buildsystem": "cmake-ninja",
|
"buildsystem": "cmake-ninja",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ Dependencies:
|
|||||||
'frameworks/kconfig': '@stable'
|
'frameworks/kconfig': '@stable'
|
||||||
'frameworks/syntax-highlighting': '@stable'
|
'frameworks/syntax-highlighting': '@stable'
|
||||||
'frameworks/kitemmodels': '@stable'
|
'frameworks/kitemmodels': '@stable'
|
||||||
|
'frameworks/kquickcharts': '@stable'
|
||||||
'frameworks/knotifications': '@stable'
|
'frameworks/knotifications': '@stable'
|
||||||
'libraries/kquickimageeditor': '@stable'
|
'libraries/kquickimageeditor': '@stable'
|
||||||
'frameworks/sonnet': '@stable'
|
'frameworks/sonnet': '@stable'
|
||||||
@@ -38,6 +39,7 @@ Dependencies:
|
|||||||
'frameworks/kconfig': '@latest-kf6'
|
'frameworks/kconfig': '@latest-kf6'
|
||||||
'frameworks/syntax-highlighting': '@latest-kf6'
|
'frameworks/syntax-highlighting': '@latest-kf6'
|
||||||
'frameworks/kitemmodels': '@latest-kf6'
|
'frameworks/kitemmodels': '@latest-kf6'
|
||||||
|
'frameworks/kquickcharts': '@latest-kf6'
|
||||||
'frameworks/knotifications': '@latest-kf6'
|
'frameworks/knotifications': '@latest-kf6'
|
||||||
'libraries/kquickimageeditor': '@latest-kf6'
|
'libraries/kquickimageeditor': '@latest-kf6'
|
||||||
'frameworks/sonnet': '@latest-kf6'
|
'frameworks/sonnet': '@latest-kf6'
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ cmake_minimum_required(VERSION 3.16)
|
|||||||
|
|
||||||
# KDE Applications version, managed by release script.
|
# KDE Applications version, managed by release script.
|
||||||
set(RELEASE_SERVICE_VERSION_MAJOR "23")
|
set(RELEASE_SERVICE_VERSION_MAJOR "23")
|
||||||
set(RELEASE_SERVICE_VERSION_MINOR "03")
|
set(RELEASE_SERVICE_VERSION_MINOR "07")
|
||||||
set(RELEASE_SERVICE_VERSION_MICRO "70")
|
set(RELEASE_SERVICE_VERSION_MICRO "70")
|
||||||
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
|
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
|
||||||
|
|
||||||
@@ -111,6 +111,7 @@ set_package_properties(cmark PROPERTIES
|
|||||||
|
|
||||||
ecm_find_qmlmodule(org.kde.kquickimageeditor 1.0)
|
ecm_find_qmlmodule(org.kde.kquickimageeditor 1.0)
|
||||||
ecm_find_qmlmodule(org.kde.kitemmodels 1.0)
|
ecm_find_qmlmodule(org.kde.kitemmodels 1.0)
|
||||||
|
ecm_find_qmlmodule(org.kde.quickcharts 1.0)
|
||||||
|
|
||||||
find_package(KQuickImageEditor COMPONENTS)
|
find_package(KQuickImageEditor COMPONENTS)
|
||||||
set_package_properties(KQuickImageEditor PROPERTIES
|
set_package_properties(KQuickImageEditor PROPERTIES
|
||||||
@@ -130,13 +131,12 @@ set_package_properties(KF${QT_MAJOR_VERSION}DocTools PROPERTIES DESCRIPTION
|
|||||||
TYPE OPTIONAL
|
TYPE OPTIONAL
|
||||||
)
|
)
|
||||||
|
|
||||||
find_package(Sqlite3)
|
|
||||||
|
|
||||||
if(NOT Quotient_VERSION_MINOR GREATER 6)
|
if(NOT Quotient_VERSION_MINOR GREATER 6)
|
||||||
cmake_policy(SET CMP0063 OLD)
|
cmake_policy(SET CMP0063 OLD)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(ANDROID)
|
if(ANDROID)
|
||||||
|
find_package(Sqlite3)
|
||||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/android/version.gradle.in ${CMAKE_BINARY_DIR}/version.gradle)
|
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/android/version.gradle.in ${CMAKE_BINARY_DIR}/version.gradle)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|||||||
@@ -8,3 +8,9 @@ ecm_add_test(
|
|||||||
LINK_LIBRARIES neochat Qt::Test Quotient
|
LINK_LIBRARIES neochat Qt::Test Quotient
|
||||||
TEST_NAME neochatroomtest
|
TEST_NAME neochatroomtest
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ecm_add_test(
|
||||||
|
texthandlertest.cpp
|
||||||
|
LINK_LIBRARIES neochat Qt::Test
|
||||||
|
TEST_NAME texthandlertest
|
||||||
|
)
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ void NeoChatRoomTest::initTestCase()
|
|||||||
void NeoChatRoomTest::subtitleTextTest()
|
void NeoChatRoomTest::subtitleTextTest()
|
||||||
{
|
{
|
||||||
QCOMPARE(room->timelineSize(), 1);
|
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()
|
void NeoChatRoomTest::eventTest()
|
||||||
|
|||||||
497
autotests/texthandlertest.cpp
Normal file
497
autotests/texthandlertest.cpp
Normal file
@@ -0,0 +1,497 @@
|
|||||||
|
// 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 "texthandler.h"
|
||||||
|
|
||||||
|
#include <connection.h>
|
||||||
|
#include <quotient_common.h>
|
||||||
|
#include <syncdata.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 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": "<b>This is an example text message</b>",
|
||||||
|
"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 testInputString1 = QStringLiteral("<p><span data-mx-spoiler><font color=#FFFFFF>Test</font><span></p>");
|
||||||
|
const QString testOutputString1 = QStringLiteral("<p><span data-mx-spoiler><font color=#FFFFFF>Test</font><span></p>");
|
||||||
|
// Handle urls where the href has either single (') or double (") quotes.
|
||||||
|
const QString testInputString2 = QStringLiteral("<p><a href=\"https://kde.org\">link</a><a href='https://kde.org'>link</a></p>");
|
||||||
|
const QString testOutputString2 = QStringLiteral("<p><a href=\"https://kde.org\">link</a><a href='https://kde.org'>link</a></p>");
|
||||||
|
|
||||||
|
TextHandler testTextHandler;
|
||||||
|
testTextHandler.setData(testInputString1);
|
||||||
|
|
||||||
|
QCOMPARE(testTextHandler.handleSendText(), testOutputString1);
|
||||||
|
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString1);
|
||||||
|
|
||||||
|
testTextHandler.setData(testInputString2);
|
||||||
|
QCOMPARE(testTextHandler.handleSendText(), testOutputString2);
|
||||||
|
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString2);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextHandlerTest::stripDisallowedTags()
|
||||||
|
{
|
||||||
|
const QString testInputString = QStringLiteral("<p>Allowed</p> <span>Allowed</span> <body>Disallowed</body>");
|
||||||
|
const QString testOutputString = QStringLiteral("<p>Allowed</p> <span>Allowed</span> Disallowed");
|
||||||
|
|
||||||
|
TextHandler testTextHandler;
|
||||||
|
testTextHandler.setData(testInputString);
|
||||||
|
|
||||||
|
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
|
||||||
|
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextHandlerTest::stripDisallowedAttributes()
|
||||||
|
{
|
||||||
|
const QString testInputString = QStringLiteral("<p style=\"font-size:50px;\" color=#FFFFFF>Test</p>");
|
||||||
|
const QString testOutputString = QStringLiteral("<p>Test</p>");
|
||||||
|
|
||||||
|
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("<pre><code></code></pre>");
|
||||||
|
const QString testOutputString = QStringLiteral("<pre><code></code></pre>");
|
||||||
|
|
||||||
|
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("<p>This data should just be put in a paragraph.</p>");
|
||||||
|
|
||||||
|
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), , `inline code`.");
|
||||||
|
const QString testOutputString = QStringLiteral(
|
||||||
|
"<p>Text para with <strong>bold</strong>, <em>italic</em>, <a href=\"https://kde.org\">link</a>, <img "
|
||||||
|
"src=\"mxc://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e\" alt=\"image\">, <code>inline code</code>.</p>");
|
||||||
|
|
||||||
|
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(
|
||||||
|
"<p>Text para</p>\n<blockquote>\n<p>blockquote</p>\n</blockquote>\n<ul>\n<li>List 1</li>\n<li>List "
|
||||||
|
"2</li>\n</ul>\n<ol>\n<li>one</li>\n<li>two</li>\n</ol>\n<h1>Heading 1</h1>\n<h2>Heading 2</h2>\n<p>horizontal "
|
||||||
|
"rule</p>\n<hr>\n<pre><code>codeblock\n</code></pre>");
|
||||||
|
|
||||||
|
TextHandler testTextHandler;
|
||||||
|
testTextHandler.setData(testInputString);
|
||||||
|
|
||||||
|
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextHandlerTest::sendBadLinks()
|
||||||
|
{
|
||||||
|
const QString testInputString = QStringLiteral("[link](kde.org), ");
|
||||||
|
const QString testOutputString = QStringLiteral("<p><a>link</a>, <img alt=\"image\"></p>");
|
||||||
|
|
||||||
|
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<p>Test <span style=\"font-size:50px;\">some</span> code</p>\n```");
|
||||||
|
const QString testOutputString =
|
||||||
|
QStringLiteral("<pre><code><p>Test <span style="font-size:50px;">some</span> code</p>\n</code></pre>");
|
||||||
|
|
||||||
|
TextHandler testTextHandler;
|
||||||
|
testTextHandler.setData(testInputString);
|
||||||
|
|
||||||
|
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextHandlerTest::sendCodeClass()
|
||||||
|
{
|
||||||
|
const QString testInputString = QStringLiteral("```html\nsome code\n```\n<pre><code class=\"code-underline\">some more code</code></pre>");
|
||||||
|
const QString testOutputString = QStringLiteral("<pre><code class=\"language-html\">some code\n</code></pre>\n<pre><code>some more code</code></pre>");
|
||||||
|
|
||||||
|
TextHandler testTextHandler;
|
||||||
|
testTextHandler.setData(testInputString);
|
||||||
|
|
||||||
|
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextHandlerTest::receiveStripReply()
|
||||||
|
{
|
||||||
|
const QString testInputString = QStringLiteral(
|
||||||
|
"<mx-reply><blockquote><a href=\"https://matrix.to/#/!somewhere:example.org/$event:example.org\">In reply to</a><a "
|
||||||
|
"href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a><br />Message replied to.</blockquote></mx-reply>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("<plain text in tag bracket>\nTest link https://kde.org.");
|
||||||
|
const QString testOutputStringRich = QStringLiteral("<plain text in tag bracket><br>Test link <a href=\"https://kde.org\">https://kde.org</a>.");
|
||||||
|
QString testOutputStringPlain = QStringLiteral("<plain text in tag bracket>\nTest link https://kde.org.");
|
||||||
|
|
||||||
|
// Make sure quotes are maintained in a plain string.
|
||||||
|
const QString testInputString2 = QStringLiteral("last line is \"Time to switch to a new topic.\"");
|
||||||
|
const QString testOutputString2 = QStringLiteral("last line is \"Time to switch to a new topic.\"");
|
||||||
|
|
||||||
|
TextHandler testTextHandler;
|
||||||
|
testTextHandler.setData(testInputString);
|
||||||
|
|
||||||
|
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::PlainText), testOutputStringRich);
|
||||||
|
QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputStringPlain);
|
||||||
|
|
||||||
|
testTextHandler.setData(testInputString2);
|
||||||
|
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::PlainText), testOutputString2);
|
||||||
|
QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputString2);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextHandlerTest::receiveStripNewlines()
|
||||||
|
{
|
||||||
|
const QString testInputStringPlain = QStringLiteral("Test\nmany\nnew\nlines.");
|
||||||
|
const QString testInputStringRich = QStringLiteral("Test<br>many<br />new<br>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("<p>Test</p> <pre><code>Some code <strong>with tags</strong></code></pre>");
|
||||||
|
const QString testOutputString = QStringLiteral("Test Some code <strong>with tags</strong>");
|
||||||
|
|
||||||
|
TextHandler testTextHandler;
|
||||||
|
testTextHandler.setData(testInputString);
|
||||||
|
|
||||||
|
QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText), testOutputString);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextHandlerTest::receivePlainStripMarkup()
|
||||||
|
{
|
||||||
|
const QString testInputString = QStringLiteral("**bold** `<p>inline code</p>` *italic*");
|
||||||
|
const QString testOutputString = QStringLiteral("bold <p>inline code</p> italic");
|
||||||
|
|
||||||
|
TextHandler testTextHandler;
|
||||||
|
testTextHandler.setData(testInputString);
|
||||||
|
|
||||||
|
QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputString);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextHandlerTest::receiveRichUserPill()
|
||||||
|
{
|
||||||
|
const QString testInputString = QStringLiteral("<p><a href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a></p>");
|
||||||
|
const QString testOutputString = QStringLiteral("<p><b><a href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a></b></p>");
|
||||||
|
|
||||||
|
TextHandler testTextHandler;
|
||||||
|
testTextHandler.setData(testInputString);
|
||||||
|
|
||||||
|
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextHandlerTest::receiveRichStrikethrough()
|
||||||
|
{
|
||||||
|
const QString testInputString = QStringLiteral("<p><del>Test</del></p>");
|
||||||
|
const QString testOutputString = QStringLiteral("<p><s>Test</s></p>");
|
||||||
|
|
||||||
|
TextHandler testTextHandler;
|
||||||
|
testTextHandler.setData(testInputString);
|
||||||
|
|
||||||
|
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextHandlerTest::receiveRichtextIn()
|
||||||
|
{
|
||||||
|
const QString testInputString = QStringLiteral("<p>Test</p> <pre><code>Some code <strong>with tags</strong></code></pre>");
|
||||||
|
const QString testOutputString = QStringLiteral("<p>Test</p> <pre><code>Some code <strong>with tags</strong></code></pre>");
|
||||||
|
|
||||||
|
TextHandler testTextHandler;
|
||||||
|
testTextHandler.setData(testInputString);
|
||||||
|
|
||||||
|
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
void TextHandlerTest::receiveRichMxcUrl()
|
||||||
|
{
|
||||||
|
const QString testInputString = QStringLiteral(
|
||||||
|
"<img src=\"mxc://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e\" alt=\"image\"><img src=\"mxc://kde.org/34c3464b3a1bd7f55af2d559e07d2c773c430e73\" "
|
||||||
|
"alt=\"image\">");
|
||||||
|
const QString testOutputString = QStringLiteral(
|
||||||
|
"<img "
|
||||||
|
"src=\"mxc://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e?user_id=@bob:kde.org&room_id=%23myroom:kde.org&event_id=$143273582443PhrSn:example.org\" "
|
||||||
|
"alt=\"image\"><img "
|
||||||
|
"src=\"mxc://kde.org/34c3464b3a1bd7f55af2d559e07d2c773c430e73?user_id=@bob:kde.org&room_id=%23myroom:kde.org&event_id=$143273582443PhrSn:example.org\" "
|
||||||
|
"alt=\"image\">");
|
||||||
|
|
||||||
|
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 "
|
||||||
|
"<a "
|
||||||
|
"href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/"
|
||||||
|
"$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im\">Link already rich</a>");
|
||||||
|
const QString testOutputStringLink1 = QStringLiteral(
|
||||||
|
"<a "
|
||||||
|
"href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/"
|
||||||
|
"$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im\">https://matrix.to/#/"
|
||||||
|
"!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im</a> <a "
|
||||||
|
"href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/"
|
||||||
|
"$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im\">Link already rich</a>");
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
"<a "
|
||||||
|
"href=\"https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/\">https://lore.kernel.org/lkml/"
|
||||||
|
"CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/</a>");
|
||||||
|
|
||||||
|
QString testInputStringEmail = QStringLiteral(R"(email@example.com <a href="mailto:email@example.com">Link already rich</a>)");
|
||||||
|
QString testOutputStringEmail =
|
||||||
|
QStringLiteral(R"(<a href="mailto:email@example.com">email@example.com</a> <a href="mailto:email@example.com">Link already rich</a>)");
|
||||||
|
|
||||||
|
QString testInputStringMxId = QStringLiteral("@user:kde.org <a href=\"https://matrix.to/#/@user:kde.org\">Link already rich</a>");
|
||||||
|
QString testOutputStringMxId = QStringLiteral(
|
||||||
|
"<b><a href=\"https://matrix.to/#/@user:kde.org\">@user:kde.org</a></b> <b><a href=\"https://matrix.to/#/@user:kde.org\">Link already rich</a></b>");
|
||||||
|
|
||||||
|
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"
|
||||||
433
po/ar/neochat.po
433
po/ar/neochat.po
File diff suppressed because it is too large
Load Diff
428
po/az/neochat.po
428
po/az/neochat.po
File diff suppressed because it is too large
Load Diff
411
po/ca/neochat.po
411
po/ca/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
406
po/cs/neochat.po
406
po/cs/neochat.po
File diff suppressed because it is too large
Load Diff
423
po/da/neochat.po
423
po/da/neochat.po
File diff suppressed because it is too large
Load Diff
414
po/de/neochat.po
414
po/de/neochat.po
File diff suppressed because it is too large
Load Diff
414
po/el/neochat.po
414
po/el/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
414
po/es/neochat.po
414
po/es/neochat.po
File diff suppressed because it is too large
Load Diff
419
po/eu/neochat.po
419
po/eu/neochat.po
File diff suppressed because it is too large
Load Diff
1711
po/fi/neochat.po
1711
po/fi/neochat.po
File diff suppressed because it is too large
Load Diff
418
po/fr/neochat.po
418
po/fr/neochat.po
File diff suppressed because it is too large
Load Diff
430
po/hu/neochat.po
430
po/hu/neochat.po
File diff suppressed because it is too large
Load Diff
696
po/ia/neochat.po
696
po/ia/neochat.po
File diff suppressed because it is too large
Load Diff
419
po/id/neochat.po
419
po/id/neochat.po
File diff suppressed because it is too large
Load Diff
428
po/ie/neochat.po
428
po/ie/neochat.po
File diff suppressed because it is too large
Load Diff
450
po/it/neochat.po
450
po/it/neochat.po
File diff suppressed because it is too large
Load Diff
403
po/ja/neochat.po
403
po/ja/neochat.po
File diff suppressed because it is too large
Load Diff
412
po/ka/neochat.po
412
po/ka/neochat.po
File diff suppressed because it is too large
Load Diff
427
po/ko/neochat.po
427
po/ko/neochat.po
File diff suppressed because it is too large
Load Diff
405
po/lt/neochat.po
405
po/lt/neochat.po
File diff suppressed because it is too large
Load Diff
408
po/nl/neochat.po
408
po/nl/neochat.po
File diff suppressed because it is too large
Load Diff
403
po/nn/neochat.po
403
po/nn/neochat.po
File diff suppressed because it is too large
Load Diff
430
po/pa/neochat.po
430
po/pa/neochat.po
File diff suppressed because it is too large
Load Diff
514
po/pl/neochat.po
514
po/pl/neochat.po
File diff suppressed because it is too large
Load Diff
405
po/pt/neochat.po
405
po/pt/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
416
po/ru/neochat.po
416
po/ru/neochat.po
File diff suppressed because it is too large
Load Diff
433
po/sk/neochat.po
433
po/sk/neochat.po
File diff suppressed because it is too large
Load Diff
416
po/sl/neochat.po
416
po/sl/neochat.po
File diff suppressed because it is too large
Load Diff
428
po/sv/neochat.po
428
po/sv/neochat.po
File diff suppressed because it is too large
Load Diff
484
po/ta/neochat.po
484
po/ta/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
408
po/tr/neochat.po
408
po/tr/neochat.po
File diff suppressed because it is too large
Load Diff
411
po/uk/neochat.po
411
po/uk/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -24,7 +24,6 @@ add_library(neochat STATIC
|
|||||||
models/publicroomlistmodel.cpp
|
models/publicroomlistmodel.cpp
|
||||||
models/userdirectorylistmodel.cpp
|
models/userdirectorylistmodel.cpp
|
||||||
models/keywordnotificationrulemodel.cpp
|
models/keywordnotificationrulemodel.cpp
|
||||||
utils.cpp
|
|
||||||
notificationsmanager.cpp
|
notificationsmanager.cpp
|
||||||
models/sortfilterroomlistmodel.cpp
|
models/sortfilterroomlistmodel.cpp
|
||||||
chatdocumenthandler.cpp
|
chatdocumenthandler.cpp
|
||||||
@@ -47,6 +46,8 @@ add_library(neochat STATIC
|
|||||||
models/statemodel.cpp
|
models/statemodel.cpp
|
||||||
filetransferpseudojob.cpp
|
filetransferpseudojob.cpp
|
||||||
models/searchmodel.cpp
|
models/searchmodel.cpp
|
||||||
|
texthandler.cpp
|
||||||
|
models/locationsmodel.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(neochat-app
|
add_executable(neochat-app
|
||||||
@@ -175,6 +176,9 @@ if(ANDROID)
|
|||||||
"window-new"
|
"window-new"
|
||||||
"globe"
|
"globe"
|
||||||
"visibility"
|
"visibility"
|
||||||
|
"home"
|
||||||
|
"preferences-desktop-notification"
|
||||||
|
"computer-symbolic"
|
||||||
)
|
)
|
||||||
else()
|
else()
|
||||||
target_link_libraries(neochat PUBLIC Qt::Widgets KF${QT_MAJOR_VERSION}::KIOWidgets)
|
target_link_libraries(neochat PUBLIC Qt::Widgets KF${QT_MAJOR_VERSION}::KIOWidgets)
|
||||||
|
|||||||
@@ -20,25 +20,10 @@
|
|||||||
#include "neochatroom.h"
|
#include "neochatroom.h"
|
||||||
#include "neochatuser.h"
|
#include "neochatuser.h"
|
||||||
#include "roommanager.h"
|
#include "roommanager.h"
|
||||||
|
#include "texthandler.h"
|
||||||
|
|
||||||
using namespace Quotient;
|
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("<!-- raw HTML omitted -->", "");
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
ActionsHandler::ActionsHandler(QObject *parent)
|
ActionsHandler::ActionsHandler(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
{
|
{
|
||||||
@@ -169,7 +154,10 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, con
|
|||||||
}
|
}
|
||||||
|
|
||||||
handledText = CustomEmojiModel::instance().preprocessText(handledText);
|
handledText = CustomEmojiModel::instance().preprocessText(handledText);
|
||||||
handledText = markdownToHTML(handledText);
|
TextHandler textHandler;
|
||||||
|
textHandler.setData(handledText);
|
||||||
|
handledText = textHandler.handleSendText();
|
||||||
|
|
||||||
if (handledText.count("<p>") == 1 && handledText.count("</p>") == 1) {
|
if (handledText.count("<p>") == 1 && handledText.count("</p>") == 1) {
|
||||||
handledText.remove("<p>");
|
handledText.remove("<p>");
|
||||||
handledText.remove("</p>");
|
handledText.remove("</p>");
|
||||||
|
|||||||
@@ -50,5 +50,3 @@ private:
|
|||||||
QString handleMentions(QString handledText, const bool &isEdit = false);
|
QString handleMentions(QString handledText, const bool &isEdit = false);
|
||||||
void handleMessage(const QString &text, QString handledText, const bool &isEdit = false);
|
void handleMessage(const QString &text, QString handledText, const bool &isEdit = false);
|
||||||
};
|
};
|
||||||
|
|
||||||
QString markdownToHTML(const QString &markdown);
|
|
||||||
|
|||||||
@@ -44,7 +44,6 @@
|
|||||||
#include "clipboard.h"
|
#include "clipboard.h"
|
||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
#include "filetypesingleton.h"
|
#include "filetypesingleton.h"
|
||||||
#include "joinrulesevent.h"
|
|
||||||
#include "linkpreviewer.h"
|
#include "linkpreviewer.h"
|
||||||
#include "login.h"
|
#include "login.h"
|
||||||
#include "matriximageprovider.h"
|
#include "matriximageprovider.h"
|
||||||
@@ -53,6 +52,7 @@
|
|||||||
#include "models/devicesmodel.h"
|
#include "models/devicesmodel.h"
|
||||||
#include "models/emojimodel.h"
|
#include "models/emojimodel.h"
|
||||||
#include "models/keywordnotificationrulemodel.h"
|
#include "models/keywordnotificationrulemodel.h"
|
||||||
|
#include "models/locationsmodel.h"
|
||||||
#include "models/messageeventmodel.h"
|
#include "models/messageeventmodel.h"
|
||||||
#include "models/messagefiltermodel.h"
|
#include "models/messagefiltermodel.h"
|
||||||
#include "models/publicroomlistmodel.h"
|
#include "models/publicroomlistmodel.h"
|
||||||
@@ -236,6 +236,7 @@ int main(int argc, char *argv[])
|
|||||||
qmlRegisterType<CompletionModel>("org.kde.neochat", 1, 0, "CompletionModel");
|
qmlRegisterType<CompletionModel>("org.kde.neochat", 1, 0, "CompletionModel");
|
||||||
qmlRegisterType<StateModel>("org.kde.neochat", 1, 0, "StateModel");
|
qmlRegisterType<StateModel>("org.kde.neochat", 1, 0, "StateModel");
|
||||||
qmlRegisterType<SearchModel>("org.kde.neochat", 1, 0, "SearchModel");
|
qmlRegisterType<SearchModel>("org.kde.neochat", 1, 0, "SearchModel");
|
||||||
|
qmlRegisterType<LocationsModel>("org.kde.neochat", 1, 0, "LocationsModel");
|
||||||
#ifdef QUOTIENT_07
|
#ifdef QUOTIENT_07
|
||||||
qmlRegisterType<PollHandler>("org.kde.neochat", 1, 0, "PollHandler");
|
qmlRegisterType<PollHandler>("org.kde.neochat", 1, 0, "PollHandler");
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
102
src/models/locationsmodel.cpp
Normal file
102
src/models/locationsmodel.cpp
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "locationsmodel.h"
|
||||||
|
|
||||||
|
using namespace Quotient;
|
||||||
|
|
||||||
|
LocationsModel::LocationsModel(QObject *parent)
|
||||||
|
: QAbstractListModel(parent)
|
||||||
|
{
|
||||||
|
connect(this, &LocationsModel::roomChanged, this, [=]() {
|
||||||
|
for (const auto &event : m_room->messageEvents()) {
|
||||||
|
if (!is<RoomMessageEvent>(*event)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (event->contentJson()["msgtype"] == "m.location") {
|
||||||
|
const auto &e = *event;
|
||||||
|
addLocation(eventCast<const RoomMessageEvent>(&e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
connect(m_room, &NeoChatRoom::aboutToAddHistoricalMessages, this, [=](const auto &events) {
|
||||||
|
for (const auto &event : events) {
|
||||||
|
if (!is<RoomMessageEvent>(*event)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (event->contentJson()["msgtype"] == "m.location") {
|
||||||
|
const auto &e = *event;
|
||||||
|
addLocation(eventCast<const RoomMessageEvent>(&e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(m_room, &NeoChatRoom::aboutToAddNewMessages, this, [=](const auto &events) {
|
||||||
|
for (const auto &event : events) {
|
||||||
|
if (!is<RoomMessageEvent>(*event)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (event->contentJson()["msgtype"] == "m.location") {
|
||||||
|
const auto &e = *event;
|
||||||
|
addLocation(eventCast<const RoomMessageEvent>(&e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocationsModel::addLocation(const RoomMessageEvent *event)
|
||||||
|
{
|
||||||
|
const auto uri = event->contentJson()["org.matrix.msc3488.location"]["uri"].toString();
|
||||||
|
const auto parts = uri.mid(4).split(QLatin1Char(','));
|
||||||
|
const auto latitude = parts[0].toFloat();
|
||||||
|
const auto longitude = parts[1].toFloat();
|
||||||
|
beginInsertRows(QModelIndex(), m_locations.size(), m_locations.size() + 1);
|
||||||
|
m_locations += LocationData{
|
||||||
|
.eventId = event->id(),
|
||||||
|
.latitude = latitude,
|
||||||
|
.longitude = longitude,
|
||||||
|
.text = event->contentJson()["body"].toString(),
|
||||||
|
.author = dynamic_cast<NeoChatUser *>(m_room->user(event->senderId())),
|
||||||
|
};
|
||||||
|
endInsertRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
NeoChatRoom *LocationsModel::room() const
|
||||||
|
{
|
||||||
|
return m_room;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocationsModel::setRoom(NeoChatRoom *room)
|
||||||
|
{
|
||||||
|
if (m_room) {
|
||||||
|
disconnect(this, nullptr, m_room, nullptr);
|
||||||
|
}
|
||||||
|
m_room = room;
|
||||||
|
Q_EMIT roomChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
QHash<int, QByteArray> LocationsModel::roleNames() const
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
{LongitudeRole, "longitude"},
|
||||||
|
{LatitudeRole, "latitude"},
|
||||||
|
{TextRole, "text"},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant LocationsModel::data(const QModelIndex &index, int roleName) const
|
||||||
|
{
|
||||||
|
auto row = index.row();
|
||||||
|
if (roleName == LongitudeRole) {
|
||||||
|
return m_locations[row].longitude;
|
||||||
|
} else if (roleName == LatitudeRole) {
|
||||||
|
return m_locations[row].latitude;
|
||||||
|
} else if (roleName == TextRole) {
|
||||||
|
return m_locations[row].text;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
int LocationsModel::rowCount(const QModelIndex &parent) const
|
||||||
|
{
|
||||||
|
return m_locations.size();
|
||||||
|
}
|
||||||
49
src/models/locationsmodel.h
Normal file
49
src/models/locationsmodel.h
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QAbstractListModel>
|
||||||
|
|
||||||
|
#include "neochatroom.h"
|
||||||
|
|
||||||
|
#include <events/roommessageevent.h>
|
||||||
|
|
||||||
|
class LocationsModel : public QAbstractListModel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum Roles {
|
||||||
|
TextRole = Qt::DisplayRole,
|
||||||
|
LongitudeRole,
|
||||||
|
LatitudeRole,
|
||||||
|
};
|
||||||
|
Q_ENUM(Roles)
|
||||||
|
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
||||||
|
|
||||||
|
explicit LocationsModel(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
[[nodiscard]] NeoChatRoom *room() const;
|
||||||
|
void setRoom(NeoChatRoom *room);
|
||||||
|
|
||||||
|
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||||
|
[[nodiscard]] QVariant data(const QModelIndex &index, int roleName) const override;
|
||||||
|
[[nodiscard]] int rowCount(const QModelIndex &parent) const override;
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void roomChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QPointer<NeoChatRoom> m_room;
|
||||||
|
|
||||||
|
struct LocationData {
|
||||||
|
QString eventId;
|
||||||
|
float latitude;
|
||||||
|
float longitude;
|
||||||
|
QString text;
|
||||||
|
NeoChatUser *author;
|
||||||
|
};
|
||||||
|
QList<LocationData> m_locations;
|
||||||
|
void addLocation(const Quotient::RoomMessageEvent *event);
|
||||||
|
};
|
||||||
@@ -27,7 +27,6 @@
|
|||||||
#include <KLocalizedString>
|
#include <KLocalizedString>
|
||||||
|
|
||||||
#include "neochatuser.h"
|
#include "neochatuser.h"
|
||||||
#include "utils.h"
|
|
||||||
|
|
||||||
using namespace Quotient;
|
using namespace Quotient;
|
||||||
|
|
||||||
@@ -54,6 +53,9 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
|||||||
roles[UserMarkerRole] = "userMarker";
|
roles[UserMarkerRole] = "userMarker";
|
||||||
roles[ShowAuthorRole] = "showAuthor";
|
roles[ShowAuthorRole] = "showAuthor";
|
||||||
roles[ShowSectionRole] = "showSection";
|
roles[ShowSectionRole] = "showSection";
|
||||||
|
roles[ReadMarkersRole] = "readMarkers";
|
||||||
|
roles[ReadMarkersStringRole] = "readMarkersString";
|
||||||
|
roles[ShowReadMarkersRole] = "showReadMarkers";
|
||||||
roles[ReactionRole] = "reaction";
|
roles[ReactionRole] = "reaction";
|
||||||
roles[IsEditedRole] = "isEdited";
|
roles[IsEditedRole] = "isEdited";
|
||||||
roles[SourceRole] = "source";
|
roles[SourceRole] = "source";
|
||||||
@@ -64,8 +66,6 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
|||||||
roles[VerifiedRole] = "verified";
|
roles[VerifiedRole] = "verified";
|
||||||
roles[DisplayNameForInitialsRole] = "displayNameForInitials";
|
roles[DisplayNameForInitialsRole] = "displayNameForInitials";
|
||||||
roles[AuthorDisplayNameRole] = "authorDisplayName";
|
roles[AuthorDisplayNameRole] = "authorDisplayName";
|
||||||
roles[IsNameChangeRole] = "isNameChange";
|
|
||||||
roles[IsAvatarChangeRole] = "isAvatarChange";
|
|
||||||
roles[IsRedactedRole] = "isRedacted";
|
roles[IsRedactedRole] = "isRedacted";
|
||||||
roles[GenericDisplayRole] = "genericDisplay";
|
roles[GenericDisplayRole] = "genericDisplay";
|
||||||
roles[IsPendingRole] = "isPending";
|
roles[IsPendingRole] = "isPending";
|
||||||
@@ -216,6 +216,12 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
|||||||
}
|
}
|
||||||
refreshEventRoles(eventId, {ReactionRole, Qt::DisplayRole});
|
refreshEventRoles(eventId, {ReactionRole, Qt::DisplayRole});
|
||||||
});
|
});
|
||||||
|
connect(m_currentRoom, &Room::changed, this, [this]() {
|
||||||
|
for (auto it = m_currentRoom->messageEvents().rbegin(); it != m_currentRoom->messageEvents().rend(); ++it) {
|
||||||
|
auto event = it->event();
|
||||||
|
refreshEventRoles(event->id(), {ReadMarkersRole, ReadMarkersStringRole});
|
||||||
|
}
|
||||||
|
});
|
||||||
connect(m_currentRoom, &Room::newFileTransfer, this, &MessageEventModel::refreshEvent);
|
connect(m_currentRoom, &Room::newFileTransfer, this, &MessageEventModel::refreshEvent);
|
||||||
connect(m_currentRoom, &Room::fileTransferProgress, this, &MessageEventModel::refreshEvent);
|
connect(m_currentRoom, &Room::fileTransferProgress, this, &MessageEventModel::refreshEvent);
|
||||||
connect(m_currentRoom, &Room::fileTransferCompleted, this, &MessageEventModel::refreshEvent);
|
connect(m_currentRoom, &Room::fileTransferCompleted, this, &MessageEventModel::refreshEvent);
|
||||||
@@ -504,6 +510,8 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
|||||||
return DelegateType::Audio;
|
return DelegateType::Audio;
|
||||||
case MessageEventType::Video:
|
case MessageEventType::Video:
|
||||||
return DelegateType::Video;
|
return DelegateType::Video;
|
||||||
|
case MessageEventType::Location:
|
||||||
|
return DelegateType::Location;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -558,6 +566,9 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
|
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
|
||||||
|
if(e->msgtype() == Quotient::MessageEventType::Location) {
|
||||||
|
return e->contentJson();
|
||||||
|
}
|
||||||
// Cannot use e.contentJson() here because some
|
// Cannot use e.contentJson() here because some
|
||||||
// EventContent classes inject values into the copy of the
|
// EventContent classes inject values into the copy of the
|
||||||
// content JSON stored in EventContent::Base
|
// content JSON stored in EventContent::Base
|
||||||
@@ -599,12 +610,25 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
|||||||
|
|
||||||
if (role == SpecialMarksRole) {
|
if (role == SpecialMarksRole) {
|
||||||
if (isPending) {
|
if (isPending) {
|
||||||
|
// A pending event with an m.new_content key will be merged into the
|
||||||
|
// original event so don't show.
|
||||||
|
if (evt.contentJson().contains("m.new_content")) {
|
||||||
|
return EventStatus::Hidden;
|
||||||
|
}
|
||||||
return pendingIt->deliveryStatus();
|
return pendingIt->deliveryStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto *memberEvent = timelineIt->viewAs<RoomMemberEvent>();
|
if (evt.isStateEvent() && !NeoChatConfig::self()->showStateEvent()) {
|
||||||
if (memberEvent) {
|
return EventStatus::Hidden;
|
||||||
if ((memberEvent->isJoin() || memberEvent->isLeave()) && !NeoChatConfig::self()->showLeaveJoinEvent()) {
|
}
|
||||||
|
|
||||||
|
if (auto roomMemberEvent = eventCast<const RoomMemberEvent>(&evt)) {
|
||||||
|
if ((roomMemberEvent->isJoin() || roomMemberEvent->isLeave()) && !NeoChatConfig::self()->showLeaveJoinEvent()) {
|
||||||
|
return EventStatus::Hidden;
|
||||||
|
} else if (roomMemberEvent->isRename() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::self()->showRename()) {
|
||||||
|
return EventStatus::Hidden;
|
||||||
|
} else if (roomMemberEvent->isAvatarUpdate() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave()
|
||||||
|
&& !NeoChatConfig::self()->showAvatarUpdate()) {
|
||||||
return EventStatus::Hidden;
|
return EventStatus::Hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -791,6 +815,65 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (role == ReadMarkersRole) {
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
auto userIds = room()->userIdsAtEvent(evt.id());
|
||||||
|
userIds.remove(m_currentRoom->localUser()->id());
|
||||||
|
#else
|
||||||
|
auto userIds = room()->usersAtEventId(evt.id());
|
||||||
|
userIds.removeAll(m_currentRoom->localUser());
|
||||||
|
#endif
|
||||||
|
|
||||||
|
QVariantList users;
|
||||||
|
users.reserve(userIds.size());
|
||||||
|
for (const auto &userId : userIds) {
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
auto user = static_cast<NeoChatUser *>(m_currentRoom->user(userId));
|
||||||
|
#else
|
||||||
|
auto user = static_cast<NeoChatUser *>(userId);
|
||||||
|
#endif
|
||||||
|
users += userAtEvent(user, m_currentRoom, evt);
|
||||||
|
}
|
||||||
|
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role == ReadMarkersStringRole) {
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
auto userIds = room()->userIdsAtEvent(evt.id());
|
||||||
|
userIds.remove(m_currentRoom->localUser()->id());
|
||||||
|
#else
|
||||||
|
auto userIds = room()->usersAtEventId(evt.id());
|
||||||
|
userIds.removeAll(m_currentRoom->localUser());
|
||||||
|
#endif
|
||||||
|
/**
|
||||||
|
* The string ends up in the form
|
||||||
|
* "x users: user1DisplayName, user2DisplayName, etc."
|
||||||
|
*/
|
||||||
|
QString readMarkersString = i18np("1 user: ", "%1 users: ", userIds.size());
|
||||||
|
for (const auto &userId : userIds) {
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
auto user = static_cast<NeoChatUser *>(m_currentRoom->user(userId));
|
||||||
|
#else
|
||||||
|
auto user = static_cast<NeoChatUser *>(userId);
|
||||||
|
#endif
|
||||||
|
readMarkersString += user->displayname(m_currentRoom) + i18nc("list separator", ", ");
|
||||||
|
}
|
||||||
|
readMarkersString.chop(2);
|
||||||
|
return readMarkersString;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role == ShowReadMarkersRole) {
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
auto userIds = room()->userIdsAtEvent(evt.id());
|
||||||
|
userIds.remove(m_currentRoom->localUser()->id());
|
||||||
|
#else
|
||||||
|
auto userIds = room()->usersAtEventId(evt.id());
|
||||||
|
userIds.removeAll(m_currentRoom->localUser());
|
||||||
|
#endif
|
||||||
|
return userIds.size() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (role == ReactionRole) {
|
if (role == ReactionRole) {
|
||||||
const auto &annotations = m_currentRoom->relatedEvents(evt, EventRelation::Annotation());
|
const auto &annotations = m_currentRoom->relatedEvents(evt, EventRelation::Annotation());
|
||||||
if (annotations.isEmpty()) {
|
if (annotations.isEmpty()) {
|
||||||
@@ -891,21 +974,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == IsNameChangeRole) {
|
|
||||||
auto roomMemberEvent = eventCast<const RoomMemberEvent>(&evt);
|
|
||||||
if (roomMemberEvent) {
|
|
||||||
return roomMemberEvent->isRename();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (role == IsAvatarChangeRole) {
|
|
||||||
auto roomMemberEvent = eventCast<const RoomMemberEvent>(&evt);
|
|
||||||
if (roomMemberEvent) {
|
|
||||||
return roomMemberEvent->isAvatarUpdate();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (role == IsRedactedRole) {
|
if (role == IsRedactedRole) {
|
||||||
return evt.isRedacted();
|
return evt.isRedacted();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ public:
|
|||||||
Encrypted,
|
Encrypted,
|
||||||
ReadMarker,
|
ReadMarker,
|
||||||
Poll,
|
Poll,
|
||||||
|
Location,
|
||||||
Other,
|
Other,
|
||||||
};
|
};
|
||||||
Q_ENUM(DelegateType);
|
Q_ENUM(DelegateType);
|
||||||
@@ -57,6 +58,9 @@ public:
|
|||||||
ShowAuthorRole,
|
ShowAuthorRole,
|
||||||
ShowSectionRole,
|
ShowSectionRole,
|
||||||
|
|
||||||
|
ReadMarkersRole, /**< QVariantList of users at the event for read marker tracking. */
|
||||||
|
ReadMarkersStringRole, /**< QString with the display name and mxID of the users at the event. */
|
||||||
|
ShowReadMarkersRole, /**< bool with whether there are any other user read markers to be shown. */
|
||||||
ReactionRole,
|
ReactionRole,
|
||||||
|
|
||||||
IsEditedRole,
|
IsEditedRole,
|
||||||
@@ -70,8 +74,6 @@ public:
|
|||||||
DisplayNameForInitialsRole,
|
DisplayNameForInitialsRole,
|
||||||
// The displayname for the event's sender; for name change events, the old displayname
|
// The displayname for the event's sender; for name change events, the old displayname
|
||||||
AuthorDisplayNameRole,
|
AuthorDisplayNameRole,
|
||||||
IsNameChangeRole,
|
|
||||||
IsAvatarChangeRole,
|
|
||||||
IsRedactedRole,
|
IsRedactedRole,
|
||||||
IsPendingRole,
|
IsPendingRole,
|
||||||
LastRole, // Keep this last
|
LastRole, // Keep this last
|
||||||
|
|||||||
@@ -11,23 +11,20 @@ using namespace Quotient;
|
|||||||
MessageFilterModel::MessageFilterModel(QObject *parent)
|
MessageFilterModel::MessageFilterModel(QObject *parent)
|
||||||
: QSortFilterProxyModel(parent)
|
: QSortFilterProxyModel(parent)
|
||||||
{
|
{
|
||||||
|
connect(NeoChatConfig::self(), &NeoChatConfig::ShowStateEventChanged, this, [this] {
|
||||||
|
invalidateFilter();
|
||||||
|
});
|
||||||
connect(NeoChatConfig::self(), &NeoChatConfig::ShowLeaveJoinEventChanged, this, [this] {
|
connect(NeoChatConfig::self(), &NeoChatConfig::ShowLeaveJoinEventChanged, this, [this] {
|
||||||
beginResetModel();
|
invalidateFilter();
|
||||||
endResetModel();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
connect(NeoChatConfig::self(), &NeoChatConfig::ShowRenameChanged, this, [this] {
|
connect(NeoChatConfig::self(), &NeoChatConfig::ShowRenameChanged, this, [this] {
|
||||||
beginResetModel();
|
invalidateFilter();
|
||||||
endResetModel();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
connect(NeoChatConfig::self(), &NeoChatConfig::ShowAvatarUpdateChanged, this, [this] {
|
connect(NeoChatConfig::self(), &NeoChatConfig::ShowAvatarUpdateChanged, this, [this] {
|
||||||
beginResetModel();
|
invalidateFilter();
|
||||||
endResetModel();
|
|
||||||
});
|
});
|
||||||
connect(NeoChatConfig::self(), &NeoChatConfig::ShowDeletedMessagesChanged, this, [this] {
|
connect(NeoChatConfig::self(), &NeoChatConfig::ShowDeletedMessagesChanged, this, [this] {
|
||||||
beginResetModel();
|
invalidateFilter();
|
||||||
endResetModel();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,18 +32,11 @@ bool MessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sour
|
|||||||
{
|
{
|
||||||
const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
|
const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
|
||||||
|
|
||||||
const int specialMarks = index.data(MessageEventModel::SpecialMarksRole).toInt();
|
|
||||||
if (index.data(MessageEventModel::IsNameChangeRole).toBool() && !NeoChatConfig::self()->showRename()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index.data(MessageEventModel::IsAvatarChangeRole).toBool() && !NeoChatConfig::self()->showAvatarUpdate()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (index.data(MessageEventModel::IsRedactedRole).toBool() && !NeoChatConfig::self()->showDeletedMessages()) {
|
if (index.data(MessageEventModel::IsRedactedRole).toBool() && !NeoChatConfig::self()->showDeletedMessages()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const int specialMarks = index.data(MessageEventModel::SpecialMarksRole).toInt();
|
||||||
if (specialMarks == EventStatus::Hidden || specialMarks == EventStatus::Replaced) {
|
if (specialMarks == EventStatus::Hidden || specialMarks == EventStatus::Replaced) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -57,9 +47,5 @@ bool MessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sour
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!NeoChatConfig::self()->showLeaveJoinEvent() && eventType == MessageEventModel::State) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,28 +180,7 @@ void RoomListModel::connectRoomSignals(NeoChatRoom *room)
|
|||||||
#ifndef QUOTIENT_07
|
#ifndef QUOTIENT_07
|
||||||
connect(room, &Room::notificationCountChanged, this, &RoomListModel::handleNotifications);
|
connect(room, &Room::notificationCountChanged, this, &RoomListModel::handleNotifications);
|
||||||
#endif
|
#endif
|
||||||
connect(room, &Room::highlightCountChanged, this, [this, room] {
|
|
||||||
if (room->highlightCount() == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (room->timelineSize() == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto *lastEvent = room->lastEvent();
|
|
||||||
|
|
||||||
if (!lastEvent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!lastEvent->isStateEvent()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
User *sender = room->user(lastEvent->senderId());
|
|
||||||
if (sender == room->localUser()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Q_EMIT newHighlight(room->id(), lastEvent->id(), room->displayName(), sender->displayname(), room->eventToString(*lastEvent), room->avatar(128));
|
|
||||||
});
|
|
||||||
#ifndef QUOTIENT_07
|
#ifndef QUOTIENT_07
|
||||||
connect(room, &Room::notificationCountChanged, this, &RoomListModel::refreshNotificationCount);
|
connect(room, &Room::notificationCountChanged, this, &RoomListModel::refreshNotificationCount);
|
||||||
#else
|
#else
|
||||||
@@ -417,7 +396,7 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
|
|||||||
return m_categoryVisibility.value(data(index, CategoryRole).toInt(), true);
|
return m_categoryVisibility.value(data(index, CategoryRole).toInt(), true);
|
||||||
}
|
}
|
||||||
if (role == SubtitleTextRole) {
|
if (role == SubtitleTextRole) {
|
||||||
return room->subtitleText();
|
return room->lastEventToString(Qt::PlainText, true);
|
||||||
}
|
}
|
||||||
if (role == AvatarImageRole) {
|
if (role == AvatarImageRole) {
|
||||||
return room->avatar(128);
|
return room->avatar(128);
|
||||||
|
|||||||
@@ -116,5 +116,4 @@ Q_SIGNALS:
|
|||||||
void notificationCountChanged();
|
void notificationCountChanged();
|
||||||
|
|
||||||
void roomAdded(NeoChatRoom *_t1);
|
void roomAdded(NeoChatRoom *_t1);
|
||||||
void newHighlight(const QString &_t1, const QString &_t2, const QString &_t3, const QString &_t4, const QString &_t5, const QImage &_t6);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,10 +29,6 @@
|
|||||||
<label>Merge Room Lists</label>
|
<label>Merge Room Lists</label>
|
||||||
<default>false</default>
|
<default>false</default>
|
||||||
</entry>
|
</entry>
|
||||||
<entry name="ShowLeaveJoinEvent" type="bool">
|
|
||||||
<label>Show leave and join events in the timeline</label>
|
|
||||||
<default>true</default>
|
|
||||||
</entry>
|
|
||||||
<entry name="AllowQuickEdit" type="bool">
|
<entry name="AllowQuickEdit" type="bool">
|
||||||
<label>Use s/text/replacement syntax to edit your last message.</label>
|
<label>Use s/text/replacement syntax to edit your last message.</label>
|
||||||
<default>false</default>
|
<default>false</default>
|
||||||
@@ -72,6 +68,14 @@
|
|||||||
<label>Use a compact room list layout</label>
|
<label>Use a compact room list layout</label>
|
||||||
<default>false</default>
|
<default>false</default>
|
||||||
</entry>
|
</entry>
|
||||||
|
<entry name="ShowStateEvent" type="bool">
|
||||||
|
<label>Show state events in the timeline</label>
|
||||||
|
<default>true</default>
|
||||||
|
</entry>
|
||||||
|
<entry name="ShowLeaveJoinEvent" type="bool">
|
||||||
|
<label>Show leave and join events in the timeline</label>
|
||||||
|
<default>true</default>
|
||||||
|
</entry>
|
||||||
<entry name="ShowRename" type="bool">
|
<entry name="ShowRename" type="bool">
|
||||||
<label>Show rename events in the timeline</label>
|
<label>Show rename events in the timeline</label>
|
||||||
<default>true</default>
|
<default>true</default>
|
||||||
|
|||||||
@@ -13,9 +13,11 @@
|
|||||||
#include <QMediaMetaData>
|
#include <QMediaMetaData>
|
||||||
#include <QMediaPlayer>
|
#include <QMediaPlayer>
|
||||||
|
|
||||||
|
#include <jobs/basejob.h>
|
||||||
#include <qcoro/qcorosignal.h>
|
#include <qcoro/qcorosignal.h>
|
||||||
|
|
||||||
#include <connection.h>
|
#include <connection.h>
|
||||||
|
#include <csapi/account-data.h>
|
||||||
#include <csapi/directory.h>
|
#include <csapi/directory.h>
|
||||||
#include <csapi/pushrules.h>
|
#include <csapi/pushrules.h>
|
||||||
#include <csapi/redaction.h>
|
#include <csapi/redaction.h>
|
||||||
@@ -47,7 +49,7 @@
|
|||||||
#endif
|
#endif
|
||||||
#include "filetransferpseudojob.h"
|
#include "filetransferpseudojob.h"
|
||||||
#include "stickerevent.h"
|
#include "stickerevent.h"
|
||||||
#include "utils.h"
|
#include "texthandler.h"
|
||||||
|
|
||||||
#ifndef Q_OS_ANDROID
|
#ifndef Q_OS_ANDROID
|
||||||
#include <KIO/Job>
|
#include <KIO/Job>
|
||||||
@@ -98,6 +100,14 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
|
|||||||
Q_EMIT canEncryptRoomChanged();
|
Q_EMIT canEncryptRoomChanged();
|
||||||
});
|
});
|
||||||
connect(connection, &Connection::capabilitiesLoaded, this, &NeoChatRoom::maxRoomVersionChanged);
|
connect(connection, &Connection::capabilitiesLoaded, this, &NeoChatRoom::maxRoomVersionChanged);
|
||||||
|
connect(this, &Room::changed, this, [this]() {
|
||||||
|
Q_EMIT defaultUrlPreviewStateChanged();
|
||||||
|
});
|
||||||
|
connect(this, &Room::accountDataChanged, this, [this](QString type) {
|
||||||
|
if (type == "org.matrix.room.preview_urls") {
|
||||||
|
Q_EMIT urlPreviewEnabledChanged();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void NeoChatRoom::uploadFile(const QUrl &url, const QString &body)
|
void NeoChatRoom::uploadFile(const QUrl &url, const QString &body)
|
||||||
@@ -205,7 +215,7 @@ void NeoChatRoom::sendTypingNotification(bool isTyping)
|
|||||||
connection()->callApi<SetTypingJob>(BackgroundRequest, localUser()->id(), id(), isTyping, 10000);
|
connection()->callApi<SetTypingJob>(BackgroundRequest, localUser()->id(), id(), isTyping, 10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
const RoomEvent *NeoChatRoom::lastEvent(bool ignoreStateEvent) const
|
const RoomEvent *NeoChatRoom::lastEvent() const
|
||||||
{
|
{
|
||||||
for (auto timelineItem = messageEvents().rbegin(); timelineItem < messageEvents().rend(); timelineItem++) {
|
for (auto timelineItem = messageEvents().rbegin(); timelineItem < messageEvents().rend(); timelineItem++) {
|
||||||
const RoomEvent *event = timelineItem->get();
|
const RoomEvent *event = timelineItem->get();
|
||||||
@@ -217,8 +227,21 @@ const RoomEvent *NeoChatRoom::lastEvent(bool ignoreStateEvent) const
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event->isStateEvent()
|
if (event->isStateEvent() && !NeoChatConfig::self()->showStateEvent()) {
|
||||||
&& (ignoreStateEvent || !NeoChatConfig::self()->showLeaveJoinEvent() || static_cast<const StateEventBase &>(*event).repeatsState())) {
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto roomMemberEvent = eventCast<const RoomMemberEvent>(event)) {
|
||||||
|
if ((roomMemberEvent->isJoin() || roomMemberEvent->isLeave()) && !NeoChatConfig::self()->showLeaveJoinEvent()) {
|
||||||
|
continue;
|
||||||
|
} else if (roomMemberEvent->isRename() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::self()->showRename()) {
|
||||||
|
continue;
|
||||||
|
} else if (roomMemberEvent->isAvatarUpdate() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave()
|
||||||
|
&& !NeoChatConfig::self()->showAvatarUpdate()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event->isStateEvent() && static_cast<const StateEventBase &>(*event).repeatsState()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,6 +255,14 @@ const RoomEvent *NeoChatRoom::lastEvent(bool ignoreStateEvent) const
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
if (auto lastEvent = eventCast<const StateEvent>(event)) {
|
||||||
|
#else
|
||||||
|
if (auto lastEvent = eventCast<const StateEventBase>(event)) {
|
||||||
|
#endif
|
||||||
|
return lastEvent;
|
||||||
|
}
|
||||||
|
|
||||||
if (auto lastEvent = eventCast<const RoomMessageEvent>(event)) {
|
if (auto lastEvent = eventCast<const RoomMessageEvent>(event)) {
|
||||||
return lastEvent;
|
return lastEvent;
|
||||||
}
|
}
|
||||||
@@ -257,10 +288,11 @@ bool NeoChatRoom::lastEventIsSpoiler() const
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString NeoChatRoom::lastEventToString() const
|
QString NeoChatRoom::lastEventToString(Qt::TextFormat format, bool stripNewlines) const
|
||||||
{
|
{
|
||||||
if (auto event = lastEvent()) {
|
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("");
|
return QLatin1String("");
|
||||||
}
|
}
|
||||||
@@ -321,7 +353,7 @@ QDateTime NeoChatRoom::lastActiveTime()
|
|||||||
return QDateTime();
|
return QDateTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auto event = lastEvent(true)) {
|
if (auto event = lastEvent()) {
|
||||||
return event->originTimestamp();
|
return event->originTimestamp();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,45 +361,6 @@ QDateTime NeoChatRoom::lastActiveTime()
|
|||||||
return messageEvents().rbegin()->get()->originTimestamp();
|
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("<del>(.*)</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 '<del>text</del>'
|
|
||||||
.replace(del, "\\1")
|
|
||||||
// replace '```code```'
|
|
||||||
.replace(multileLineCode, "\\1")
|
|
||||||
// replace '`code`'
|
|
||||||
.replace(singleLinecode, "\\1");
|
|
||||||
|
|
||||||
return subtitle.size() > 0 ? subtitle : QStringLiteral(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
int NeoChatRoom::savedTopVisibleIndex() const
|
int NeoChatRoom::savedTopVisibleIndex() const
|
||||||
{
|
{
|
||||||
return firstDisplayedMarker() == historyEdge() ? 0 : int(firstDisplayedMarker() - messageEvents().rbegin());
|
return firstDisplayedMarker() == historyEdge() ? 0 : int(firstDisplayedMarker() - messageEvents().rbegin());
|
||||||
@@ -451,7 +444,7 @@ QString NeoChatRoom::avatarMediaId() const
|
|||||||
return {};
|
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);
|
const bool prettyPrint = (format == Qt::RichText);
|
||||||
|
|
||||||
@@ -462,58 +455,48 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
|
|||||||
return visit(
|
return visit(
|
||||||
#endif
|
#endif
|
||||||
evt,
|
evt,
|
||||||
[this, prettyPrint, removeReply](const RoomMessageEvent &e) {
|
[this, format, stripNewlines](const RoomMessageEvent &e) {
|
||||||
using namespace MessageEventContent;
|
using namespace MessageEventContent;
|
||||||
|
|
||||||
// 1. prettyPrint/HTML
|
TextHandler textHandler;
|
||||||
if (prettyPrint && e.hasTextContent() && e.mimeType().name() != "text/plain") {
|
|
||||||
auto htmlBody = static_cast<const TextContent *>(e.content())->body;
|
|
||||||
if (removeReply) {
|
|
||||||
htmlBody.remove(utils::removeRichReplyRegex);
|
|
||||||
}
|
|
||||||
htmlBody.replace(utils::userPillRegExp, R"(<b class="user-pill">\1</b>)");
|
|
||||||
htmlBody.replace(utils::strikethroughRegExp, "<s>\\1</s>");
|
|
||||||
|
|
||||||
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"(<img \1 src="%1/_matrix/media/r0/download/\2/\3" \4 > )").arg(base));
|
|
||||||
|
|
||||||
return htmlBody;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.hasFileContent()) {
|
if (e.hasFileContent()) {
|
||||||
auto fileCaption = e.content()->fileInfo()->originalName.toHtmlEscaped();
|
auto fileCaption = e.content()->fileInfo()->originalName;
|
||||||
if (fileCaption.isEmpty()) {
|
if (fileCaption.isEmpty()) {
|
||||||
fileCaption = prettyPrint ? Quotient::prettyPrint(e.plainBody()) : e.plainBody();
|
fileCaption = e.plainBody();
|
||||||
} else if (e.content()->fileInfo()->originalName != e.plainBody()) {
|
} else if (e.content()->fileInfo()->originalName != e.plainBody()) {
|
||||||
fileCaption = e.plainBody() + " | " + fileCaption;
|
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 body;
|
||||||
QString plainBody;
|
if (e.hasTextContent() && e.content()) {
|
||||||
if (e.hasTextContent() && e.content() && e.mimeType().name() == "text/plain") { // 2/4
|
body = static_cast<const TextContent *>(e.content())->body;
|
||||||
plainBody = static_cast<const TextContent *>(e.content())->body;
|
} else {
|
||||||
} else { // 3
|
body = e.plainBody();
|
||||||
plainBody = e.plainBody();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prettyPrint) {
|
textHandler.setData(body);
|
||||||
if (removeReply) {
|
|
||||||
plainBody.remove(utils::removeReplyRegex);
|
Qt::TextFormat inputFormat;
|
||||||
}
|
if (e.mimeType().name() == "text/plain") {
|
||||||
return Quotient::prettyPrint(plainBody);
|
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) {
|
[](const StickerEvent &e) {
|
||||||
return e.body();
|
return e.body();
|
||||||
},
|
},
|
||||||
[this](const RoomMemberEvent &e) {
|
[this, prettyPrint](const RoomMemberEvent &e) {
|
||||||
// FIXME: Rewind to the name that was at the time of this event
|
// FIXME: Rewind to the name that was at the time of this event
|
||||||
auto subjectName = this->htmlSafeMemberName(e.userId());
|
auto subjectName = this->htmlSafeMemberName(e.userId());
|
||||||
if (e.membership() == MembershipType::Leave) {
|
if (e.membership() == MembershipType::Leave) {
|
||||||
@@ -526,8 +509,11 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
subjectName = QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a>")
|
|
||||||
.arg(e.userId(), static_cast<NeoChatUser *>(user(e.userId()))->color().name(), subjectName);
|
if (prettyPrint) {
|
||||||
|
subjectName = QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a>")
|
||||||
|
.arg(e.userId(), static_cast<NeoChatUser *>(user(e.userId()))->color().name(), subjectName);
|
||||||
|
}
|
||||||
|
|
||||||
// The below code assumes senderName output in AuthorRole
|
// The below code assumes senderName output in AuthorRole
|
||||||
switch (e.membership()) {
|
switch (e.membership()) {
|
||||||
@@ -985,7 +971,7 @@ bool NeoChatRoom::canSendState(const QString &eventType) const
|
|||||||
auto currentPl = plEvent->powerLevelForUser(localUser()->id());
|
auto currentPl = plEvent->powerLevelForUser(localUser()->id());
|
||||||
|
|
||||||
#ifndef QUOTIENT_07
|
#ifndef QUOTIENT_07
|
||||||
if (eventType == "m.room.history_visibility") {
|
if (eventType == "m.room.history_visibility" || eventType == "org.matrix.room.preview_urls") {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
return currentPl >= pl;
|
return currentPl >= pl;
|
||||||
@@ -1075,10 +1061,89 @@ void NeoChatRoom::setHistoryVisibility(const QString &historyVisibilityRule)
|
|||||||
// Not emitting historyVisibilityChanged() here, since that would override the change in the UI with the *current* value, which is not the *new* value.
|
// Not emitting historyVisibilityChanged() here, since that would override the change in the UI with the *current* value, which is not the *new* value.
|
||||||
}
|
}
|
||||||
|
|
||||||
int NeoChatRoom::getUserPowerLevel(const QString &userId) const
|
bool NeoChatRoom::defaultUrlPreviewState() const
|
||||||
{
|
{
|
||||||
auto powerLevelEvent = getCurrentState<RoomPowerLevelsEvent>();
|
#ifdef QUOTIENT_07
|
||||||
return powerLevelEvent->powerLevelForUser(userId);
|
auto urlPreviewsDisabled = currentState().get("org.matrix.room.preview_urls");
|
||||||
|
#else
|
||||||
|
auto urlPreviewsDisabled = getCurrentState("org.matrix.room.preview_urls");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Some rooms will not have this state event set so check for a nullptr return.
|
||||||
|
if (urlPreviewsDisabled != nullptr) {
|
||||||
|
return !urlPreviewsDisabled->contentJson()["disable"].toBool();
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NeoChatRoom::setDefaultUrlPreviewState(const bool &defaultUrlPreviewState)
|
||||||
|
{
|
||||||
|
if (!canSendState("org.matrix.room.preview_urls")) {
|
||||||
|
qWarning() << "Power level too low to set the default URL preview state for the room";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note the org.matrix.room.preview_urls room state event is completely undocumented
|
||||||
|
* so here it is because I'm nice.
|
||||||
|
*
|
||||||
|
* Also note this is a different event to org.matrix.room.preview_urls for room
|
||||||
|
* account data, because even though it has the same name and content it's totally different.
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* "content": {
|
||||||
|
* "disable": false
|
||||||
|
* },
|
||||||
|
* "origin_server_ts": 1673115224071,
|
||||||
|
* "sender": "@bob:kde.org",
|
||||||
|
* "state_key": "",
|
||||||
|
* "type": "org.matrix.room.preview_urls",
|
||||||
|
* "unsigned": {
|
||||||
|
* "replaces_state": "replaced_event_id",
|
||||||
|
* "prev_content": {
|
||||||
|
* "disable": true
|
||||||
|
* },
|
||||||
|
* "prev_sender": "@jeff:kde.org",
|
||||||
|
* "age": 99
|
||||||
|
* },
|
||||||
|
* "event_id": "$event_id",
|
||||||
|
* "room_id": "!room_id:kde.org"
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* You just have to set disable to true to disable URL previews by default.
|
||||||
|
*/
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
setState("org.matrix.room.preview_urls", "", QJsonObject{{"disable", !defaultUrlPreviewState}});
|
||||||
|
#else
|
||||||
|
qWarning() << "Quotient 0.7 required to set room default url preview setting";
|
||||||
|
return;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NeoChatRoom::urlPreviewEnabled() const
|
||||||
|
{
|
||||||
|
if (hasAccountData("org.matrix.room.preview_urls")) {
|
||||||
|
return !accountData("org.matrix.room.preview_urls")->contentJson()["disable"].toBool();
|
||||||
|
} else {
|
||||||
|
return defaultUrlPreviewState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NeoChatRoom::setUrlPreviewEnabled(const bool &urlPreviewEnabled)
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Once again this is undocumented and even though the name and content are the
|
||||||
|
* same this is a different event to the org.matrix.room.preview_urls room state event.
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* "content": {
|
||||||
|
* "disable": true
|
||||||
|
* }
|
||||||
|
* "type": "org.matrix.room.preview_urls",
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
connection()->callApi<SetAccountDataPerRoomJob>(localUser()->id(), id(), "org.matrix.room.preview_urls", QJsonObject{{"disable", !urlPreviewEnabled}});
|
||||||
}
|
}
|
||||||
|
|
||||||
void NeoChatRoom::setUserPowerLevel(const QString &userID, const int &powerLevel)
|
void NeoChatRoom::setUserPowerLevel(const QString &userID, const int &powerLevel)
|
||||||
@@ -1120,6 +1185,12 @@ void NeoChatRoom::setUserPowerLevel(const QString &userID, const int &powerLevel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int NeoChatRoom::getUserPowerLevel(const QString &userId) const
|
||||||
|
{
|
||||||
|
auto powerLevelEvent = getCurrentState<RoomPowerLevelsEvent>();
|
||||||
|
return powerLevelEvent->powerLevelForUser(userId);
|
||||||
|
}
|
||||||
|
|
||||||
int NeoChatRoom::powerLevel(const QString &eventName, const bool &isStateEvent) const
|
int NeoChatRoom::powerLevel(const QString &eventName, const bool &isStateEvent) const
|
||||||
{
|
{
|
||||||
#ifdef QUOTIENT_07
|
#ifdef QUOTIENT_07
|
||||||
|
|||||||
@@ -52,6 +52,19 @@ class NeoChatRoom : public Quotient::Room
|
|||||||
Q_PROPERTY(QString joinRule READ joinRule WRITE setJoinRule NOTIFY joinRuleChanged)
|
Q_PROPERTY(QString joinRule READ joinRule WRITE setJoinRule NOTIFY joinRuleChanged)
|
||||||
Q_PROPERTY(QString historyVisibility READ historyVisibility WRITE setHistoryVisibility NOTIFY historyVisibilityChanged)
|
Q_PROPERTY(QString historyVisibility READ historyVisibility WRITE setHistoryVisibility NOTIFY historyVisibilityChanged)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set the default URL preview state for room members.
|
||||||
|
*
|
||||||
|
* Assumed false if the org.matrix.room.preview_urls state message has never been
|
||||||
|
* set. Can only be set if the calling user has a high enough power level.
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(bool defaultUrlPreviewState READ defaultUrlPreviewState WRITE setDefaultUrlPreviewState NOTIFY defaultUrlPreviewStateChanged)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Enable URL previews for the local user.
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(bool urlPreviewEnabled READ urlPreviewEnabled WRITE setUrlPreviewEnabled NOTIFY urlPreviewEnabledChanged)
|
||||||
|
|
||||||
// Properties for the various permission levels for the room
|
// Properties for the various permission levels for the room
|
||||||
Q_PROPERTY(int defaultUserPowerLevel READ defaultUserPowerLevel WRITE setDefaultUserPowerLevel NOTIFY defaultUserPowerLevelChanged)
|
Q_PROPERTY(int defaultUserPowerLevel READ defaultUserPowerLevel WRITE setDefaultUserPowerLevel NOTIFY defaultUserPowerLevelChanged)
|
||||||
Q_PROPERTY(int invitePowerLevel READ invitePowerLevel WRITE setInvitePowerLevel NOTIFY invitePowerLevelChanged)
|
Q_PROPERTY(int invitePowerLevel READ invitePowerLevel WRITE setInvitePowerLevel NOTIFY invitePowerLevelChanged)
|
||||||
@@ -118,13 +131,13 @@ public:
|
|||||||
/// This function respect the showLeaveJoinEvent setting and discard
|
/// This function respect the showLeaveJoinEvent setting and discard
|
||||||
/// other not interesting events. This function can return an empty pointer
|
/// other not interesting events. This function can return an empty pointer
|
||||||
/// when the room is empty of RoomMessageEvent.
|
/// when the room is empty of RoomMessageEvent.
|
||||||
[[nodiscard]] const Quotient::RoomEvent *lastEvent(bool ignoreStateEvent = false) const;
|
[[nodiscard]] const Quotient::RoomEvent *lastEvent() const;
|
||||||
|
|
||||||
/// Convenient way to get the last event but in a string format.
|
/// Convenient way to get the last event but in a string format.
|
||||||
///
|
///
|
||||||
/// \see lastEvent
|
/// \see lastEvent
|
||||||
/// \see lastEventIsSpoiler
|
/// \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.
|
/// Convenient way to check if the last event looks like it has spoilers.
|
||||||
///
|
///
|
||||||
@@ -137,12 +150,6 @@ public:
|
|||||||
/// \see lastEvent
|
/// \see lastEvent
|
||||||
[[nodiscard]] QDateTime lastActiveTime();
|
[[nodiscard]] QDateTime lastActiveTime();
|
||||||
|
|
||||||
/// Get subtitle text for room
|
|
||||||
///
|
|
||||||
/// Fetches last event and removes markdown formatting
|
|
||||||
/// \see lastEventToString
|
|
||||||
[[nodiscard]] QString subtitleText();
|
|
||||||
|
|
||||||
[[nodiscard]] bool isSpace();
|
[[nodiscard]] bool isSpace();
|
||||||
|
|
||||||
bool isEventHighlighted(const Quotient::RoomEvent *e) const;
|
bool isEventHighlighted(const Quotient::RoomEvent *e) const;
|
||||||
@@ -153,6 +160,12 @@ public:
|
|||||||
[[nodiscard]] QString historyVisibility() const;
|
[[nodiscard]] QString historyVisibility() const;
|
||||||
void setHistoryVisibility(const QString &historyVisibilityRule);
|
void setHistoryVisibility(const QString &historyVisibilityRule);
|
||||||
|
|
||||||
|
[[nodiscard]] bool defaultUrlPreviewState() const;
|
||||||
|
void setDefaultUrlPreviewState(const bool &defaultUrlPreviewState);
|
||||||
|
|
||||||
|
[[nodiscard]] bool urlPreviewEnabled() const;
|
||||||
|
void setUrlPreviewEnabled(const bool &urlPreviewEnabled);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get the power level for the given user ID in the room.
|
* @brief Get the power level for the given user ID in the room.
|
||||||
*
|
*
|
||||||
@@ -262,7 +275,7 @@ public:
|
|||||||
|
|
||||||
[[nodiscard]] QString avatarMediaId() const;
|
[[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;
|
[[nodiscard]] QString eventToGenericString(const Quotient::RoomEvent &evt) const;
|
||||||
|
|
||||||
Q_INVOKABLE [[nodiscard]] bool containsUser(const QString &userID) const;
|
Q_INVOKABLE [[nodiscard]] bool containsUser(const QString &userID) const;
|
||||||
@@ -395,6 +408,8 @@ Q_SIGNALS:
|
|||||||
void canEncryptRoomChanged();
|
void canEncryptRoomChanged();
|
||||||
void joinRuleChanged();
|
void joinRuleChanged();
|
||||||
void historyVisibilityChanged();
|
void historyVisibilityChanged();
|
||||||
|
void defaultUrlPreviewStateChanged();
|
||||||
|
void urlPreviewEnabledChanged();
|
||||||
void maxRoomVersionChanged();
|
void maxRoomVersionChanged();
|
||||||
void defaultUserPowerLevelChanged();
|
void defaultUserPowerLevelChanged();
|
||||||
void invitePowerLevelChanged();
|
void invitePowerLevelChanged();
|
||||||
|
|||||||
@@ -22,11 +22,11 @@
|
|||||||
#include <jobs/basejob.h>
|
#include <jobs/basejob.h>
|
||||||
#include <user.h>
|
#include <user.h>
|
||||||
|
|
||||||
#include "actionshandler.h"
|
|
||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
#include "neochatconfig.h"
|
#include "neochatconfig.h"
|
||||||
#include "neochatroom.h"
|
#include "neochatroom.h"
|
||||||
#include "roommanager.h"
|
#include "roommanager.h"
|
||||||
|
#include "texthandler.h"
|
||||||
#include "windowcontroller.h"
|
#include "windowcontroller.h"
|
||||||
|
|
||||||
using namespace Quotient;
|
using namespace Quotient;
|
||||||
@@ -85,7 +85,9 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
|
|||||||
std::unique_ptr<KNotificationReplyAction> replyAction(new KNotificationReplyAction(i18n("Reply")));
|
std::unique_ptr<KNotificationReplyAction> replyAction(new KNotificationReplyAction(i18n("Reply")));
|
||||||
replyAction->setPlaceholderText(i18n("Reply..."));
|
replyAction->setPlaceholderText(i18n("Reply..."));
|
||||||
connect(replyAction.get(), &KNotificationReplyAction::replied, this, [room, replyEventId](const QString &text) {
|
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));
|
notification->setReplyAction(std::move(replyAction));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,11 +139,41 @@ QQC2.Control {
|
|||||||
currentRoom.chatBoxText = text
|
currentRoom.chatBoxText = text
|
||||||
}
|
}
|
||||||
onCursorRectangleChanged: chatBarScrollView.ensureVisible(cursorRectangle)
|
onCursorRectangleChanged: chatBarScrollView.ensureVisible(cursorRectangle)
|
||||||
|
onSelectedTextChanged: {
|
||||||
|
if (selectedText.length > 0) {
|
||||||
|
quickFormatBar.selectionStart = selectionStart
|
||||||
|
quickFormatBar.selectionEnd = selectionEnd
|
||||||
|
quickFormatBar.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QuickFormatBar {
|
||||||
|
id: quickFormatBar
|
||||||
|
|
||||||
|
x: textField.cursorRectangle.x
|
||||||
|
y: textField.cursorRectangle.y - height
|
||||||
|
|
||||||
|
onFormattingSelected: chatBar.formatText(format, selectionStart, selectionEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onDeletePressed: {
|
||||||
|
if (selectedText.length > 0) {
|
||||||
|
remove(selectionStart, selectionEnd)
|
||||||
|
} else {
|
||||||
|
remove(cursorPosition, cursorPosition + 1)
|
||||||
|
}
|
||||||
|
if (textField.text == selectedText || textField.text.length <= 1) {
|
||||||
|
currentRoom.sendTypingNotification(false)
|
||||||
|
repeatTimer.stop()
|
||||||
|
}
|
||||||
|
if (quickFormatBar.visible) {
|
||||||
|
quickFormatBar.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
Keys.onEnterPressed: {
|
Keys.onEnterPressed: {
|
||||||
if (completionMenu.visible) {
|
if (completionMenu.visible) {
|
||||||
completionMenu.complete()
|
completionMenu.complete()
|
||||||
} else if (event.modifiers & Qt.ShiftModifier) {
|
} else if (event.modifiers & Qt.ShiftModifier || Kirigami.Settings.isMobile) {
|
||||||
textField.insert(cursorPosition, "\n")
|
textField.insert(cursorPosition, "\n")
|
||||||
} else {
|
} else {
|
||||||
chatBar.postMessage();
|
chatBar.postMessage();
|
||||||
@@ -152,7 +182,7 @@ QQC2.Control {
|
|||||||
Keys.onReturnPressed: {
|
Keys.onReturnPressed: {
|
||||||
if (completionMenu.visible) {
|
if (completionMenu.visible) {
|
||||||
completionMenu.complete()
|
completionMenu.complete()
|
||||||
} else if (event.modifiers & Qt.ShiftModifier) {
|
} else if (event.modifiers & Qt.ShiftModifier || Kirigami.Settings.isMobile) {
|
||||||
textField.insert(cursorPosition, "\n")
|
textField.insert(cursorPosition, "\n")
|
||||||
} else {
|
} else {
|
||||||
chatBar.postMessage();
|
chatBar.postMessage();
|
||||||
@@ -180,9 +210,14 @@ QQC2.Control {
|
|||||||
completionMenu.decrementIndex()
|
completionMenu.decrementIndex()
|
||||||
} else if (event.key === Qt.Key_Down && completionMenu.visible) {
|
} else if (event.key === Qt.Key_Down && completionMenu.visible) {
|
||||||
completionMenu.incrementIndex()
|
completionMenu.incrementIndex()
|
||||||
} else if (event.key === Qt.Key_Backspace && textField.text.length <= 1) {
|
} else if (event.key === Qt.Key_Backspace) {
|
||||||
currentRoom.sendTypingNotification(false)
|
if (textField.text == selectedText || textField.text.length <= 1) {
|
||||||
repeatTimer.stop()
|
currentRoom.sendTypingNotification(false)
|
||||||
|
repeatTimer.stop()
|
||||||
|
}
|
||||||
|
if (quickFormatBar.visible && selectedText.length > 0) {
|
||||||
|
quickFormatBar.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Keys.onShortcutOverride: {
|
Keys.onShortcutOverride: {
|
||||||
@@ -313,14 +348,9 @@ QQC2.Control {
|
|||||||
QQC2.ToolTip.text: modelData.tooltip
|
QQC2.ToolTip.text: modelData.tooltip
|
||||||
HoverHandler { id: hoverHandler }
|
HoverHandler { id: hoverHandler }
|
||||||
|
|
||||||
QQC2.BusyIndicator {
|
PieProgressBar {
|
||||||
anchors.fill: parent
|
visible: modelData.isBusy
|
||||||
leftPadding: 0
|
progress: currentRoom.fileUploadingProgress
|
||||||
rightPadding: 0
|
|
||||||
topPadding: 0
|
|
||||||
bottomPadding: 0
|
|
||||||
visible: running
|
|
||||||
running: modelData.isBusy
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -398,4 +428,47 @@ QQC2.Control {
|
|||||||
currentRoom.chatBoxReplyId = "";
|
currentRoom.chatBoxReplyId = "";
|
||||||
messageSent()
|
messageSent()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatText(format, selectionStart, selectionEnd) {
|
||||||
|
let index = textField.cursorPosition;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* There cannot be white space at the beginning or end of the string for the
|
||||||
|
* formatting to work so move the sectionStart and sectionEnd markers past any whitespace.
|
||||||
|
*/
|
||||||
|
let innerText = textField.text.substr(selectionStart, selectionEnd - selectionStart);
|
||||||
|
if (innerText.charAt(innerText.length - 1) === " ") {
|
||||||
|
let trimmedRightString = innerText.replace(/\s*$/,"");
|
||||||
|
let trimDifference = innerText.length - trimmedRightString.length;
|
||||||
|
selectionEnd -= trimDifference;
|
||||||
|
}
|
||||||
|
if (innerText.charAt(0) === " ") {
|
||||||
|
let trimmedLeftString = innerText.replace(/^\s*/,"");
|
||||||
|
let trimDifference = innerText.length - trimmedLeftString.length;
|
||||||
|
selectionStart = selectionStart + trimDifference;
|
||||||
|
}
|
||||||
|
|
||||||
|
let startText = textField.text.substr(0, selectionStart);
|
||||||
|
// Needs updating with the new selectionStart and selectionEnd with white space trimmed.
|
||||||
|
innerText = textField.text.substr(selectionStart, selectionEnd - selectionStart);
|
||||||
|
let endText = textField.text.substr(selectionEnd);
|
||||||
|
|
||||||
|
textField.text = "";
|
||||||
|
textField.text = startText + format.start + innerText + format.end + format.extra + endText;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Put the cursor where it was when the popup was opened accounting for the
|
||||||
|
* new markup.
|
||||||
|
*
|
||||||
|
* The exception is for a hyperlink where it is placed ready to start typing
|
||||||
|
* the url.
|
||||||
|
*/
|
||||||
|
if (format.extra !== "") {
|
||||||
|
textField.cursorPosition = selectionEnd + format.start.length + format.end.length;
|
||||||
|
} else if (index == selectionStart) {
|
||||||
|
textField.cursorPosition = index;
|
||||||
|
} else {
|
||||||
|
textField.cursorPosition = index + format.start.length + format.end.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
65
src/qml/Component/ChatBox/PieProgressBar.qml
Normal file
65
src/qml/Component/ChatBox/PieProgressBar.qml
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
|
import QtQuick 2.15
|
||||||
|
|
||||||
|
import org.kde.kirigami 2.18 as Kirigami
|
||||||
|
import org.kde.quickcharts 1.0 as Charts
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A circular progress bar that fills an arc as progress goes up.
|
||||||
|
*/
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Progress of the circle as a percentage.
|
||||||
|
*
|
||||||
|
* Range - 0% to 100%.
|
||||||
|
*/
|
||||||
|
property int progress: 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Offset angle for the start of the pie fill arc.
|
||||||
|
*
|
||||||
|
* This defaults to 0, i.e. an upward vertical line from the center. This rotates
|
||||||
|
* that start point by the desired number of degrees.
|
||||||
|
*
|
||||||
|
* Range - 0 degrees to 360 degrees
|
||||||
|
*/
|
||||||
|
property int startOffset: 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Fill color of the pie.
|
||||||
|
*/
|
||||||
|
property color pieColor: Kirigami.Theme.highlightColor
|
||||||
|
|
||||||
|
width: Kirigami.Units.iconSizes.smallMedium
|
||||||
|
height: Kirigami.Units.iconSizes.smallMedium
|
||||||
|
radius: width / 2
|
||||||
|
color: Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.15)
|
||||||
|
|
||||||
|
Charts.PieChart {
|
||||||
|
id: chart
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 1
|
||||||
|
|
||||||
|
filled: true
|
||||||
|
// Set chart background color so the parent filled rectangle looks like
|
||||||
|
// an outline.
|
||||||
|
backgroundColor: Kirigami.Theme.backgroundColor
|
||||||
|
fromAngle: root.startOffset
|
||||||
|
toAngle: 360 + root.startOffset
|
||||||
|
range {
|
||||||
|
from: 0
|
||||||
|
to: 100
|
||||||
|
automatic: false
|
||||||
|
}
|
||||||
|
valueSources: Charts.SingleValueSource {
|
||||||
|
value: root.progress
|
||||||
|
}
|
||||||
|
colorSource: Charts.SingleValueSource {
|
||||||
|
value: Kirigami.Theme.highlightColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
136
src/qml/Component/ChatBox/QuickFormatBar.qml
Normal file
136
src/qml/Component/ChatBox/QuickFormatBar.qml
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
import QtQuick 2.15
|
||||||
|
import QtQuick.Controls 2.15 as QQC2
|
||||||
|
import QtQuick.Layouts 1.15
|
||||||
|
|
||||||
|
import org.kde.kirigami 2.15 as Kirigami
|
||||||
|
|
||||||
|
QQC2.Popup {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var selectionStart
|
||||||
|
property var selectionEnd
|
||||||
|
|
||||||
|
signal formattingSelected(var format, int selectionStart, int selectionEnd)
|
||||||
|
|
||||||
|
padding: 1
|
||||||
|
|
||||||
|
contentItem: Flow {
|
||||||
|
QQC2.ToolButton {
|
||||||
|
icon.name: "format-text-bold"
|
||||||
|
text: i18n("Bold")
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
const format = {
|
||||||
|
start: "**",
|
||||||
|
end: "**",
|
||||||
|
extra: "",
|
||||||
|
}
|
||||||
|
formattingSelected(format, selectionStart, selectionEnd)
|
||||||
|
root.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
QQC2.ToolButton {
|
||||||
|
icon.name: "format-text-italic"
|
||||||
|
text: i18n("Italic")
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
const format = {
|
||||||
|
start: "*",
|
||||||
|
end: "*",
|
||||||
|
extra: "",
|
||||||
|
}
|
||||||
|
formattingSelected(format, selectionStart, selectionEnd)
|
||||||
|
root.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
QQC2.ToolButton {
|
||||||
|
icon.name: "format-text-strikethrough"
|
||||||
|
text: i18n("Strikethrough")
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
const format = {
|
||||||
|
start: "<del>",
|
||||||
|
end: "</del>",
|
||||||
|
extra: "",
|
||||||
|
}
|
||||||
|
formattingSelected(format, selectionStart, selectionEnd)
|
||||||
|
root.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
QQC2.ToolButton {
|
||||||
|
icon.name: "format-text-code"
|
||||||
|
text: i18n("Code block")
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
const format = {
|
||||||
|
start: "`",
|
||||||
|
end: "`",
|
||||||
|
extra: "",
|
||||||
|
}
|
||||||
|
formattingSelected(format, selectionStart, selectionEnd)
|
||||||
|
root.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
QQC2.ToolButton {
|
||||||
|
icon.name: "format-text-blockquote"
|
||||||
|
text: i18n("Quote")
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
const format = {
|
||||||
|
start: selectionStart == 0 ? ">" : "\n>",
|
||||||
|
end: "\n\n",
|
||||||
|
extra: "",
|
||||||
|
}
|
||||||
|
formattingSelected(format, selectionStart, selectionEnd)
|
||||||
|
root.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
QQC2.ToolButton {
|
||||||
|
icon.name: "link"
|
||||||
|
text: i18n("Insert link")
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
const format = {
|
||||||
|
start: "[",
|
||||||
|
end: "](",
|
||||||
|
extra: ")",
|
||||||
|
}
|
||||||
|
formattingSelected(format, selectionStart, selectionEnd)
|
||||||
|
root.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
78
src/qml/Component/FullScreenMap.qml
Normal file
78
src/qml/Component/FullScreenMap.qml
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import QtQuick 2.15
|
||||||
|
import QtQuick.Controls 2.15
|
||||||
|
import QtLocation 5.15
|
||||||
|
import QtPositioning 5.15
|
||||||
|
|
||||||
|
import org.kde.kirigami 2.15 as Kirigami
|
||||||
|
|
||||||
|
ApplicationWindow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var content
|
||||||
|
|
||||||
|
flags: Qt.FramelessWindowHint | Qt.WA_TranslucentBackground
|
||||||
|
visibility: Qt.WindowFullScreen
|
||||||
|
|
||||||
|
title: i18n("View Location")
|
||||||
|
|
||||||
|
Shortcut {
|
||||||
|
sequence: "Escape"
|
||||||
|
onActivated: root.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
color: Kirigami.Theme.backgroundColor
|
||||||
|
|
||||||
|
background: AbstractButton {
|
||||||
|
onClicked: root.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
Map {
|
||||||
|
id: map
|
||||||
|
anchors.fill: parent
|
||||||
|
property string latlong: root.content.geo_uri.split(':')[1]
|
||||||
|
property string latitude: latlong.split(',')[0]
|
||||||
|
property string longitude: latlong.split(',')[1]
|
||||||
|
center: QtPositioning.coordinate(latitude, longitude)
|
||||||
|
zoomLevel: 15
|
||||||
|
plugin: Plugin {
|
||||||
|
name: "osm"
|
||||||
|
PluginParameter {
|
||||||
|
name: "osm.useragent"
|
||||||
|
value: Application.name + "/" + Application.version + " (kde-devel@kde.org)"
|
||||||
|
}
|
||||||
|
PluginParameter {
|
||||||
|
name: "osm.mapping.providersrepository.address"
|
||||||
|
value: "https://autoconfig.kde.org/qtlocation/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MapCircle {
|
||||||
|
radius: 1500 / map.zoomLevel
|
||||||
|
color: Kirigami.Theme.highlightColor
|
||||||
|
border.color: Kirigami.Theme.linkColor
|
||||||
|
border.width: Kirigami.Units.devicePixelRatio * 2
|
||||||
|
smooth: true
|
||||||
|
opacity: 0.25
|
||||||
|
center: QtPositioning.coordinate(map.latitude, map.longitude)
|
||||||
|
}
|
||||||
|
onCopyrightLinkActivated: {
|
||||||
|
Qt.openUrlExternally(link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.right: parent.right
|
||||||
|
|
||||||
|
text: i18n("Close")
|
||||||
|
icon.name: "dialog-close"
|
||||||
|
display: AbstractButton.IconOnly
|
||||||
|
|
||||||
|
width: Kirigami.Units.gridUnit * 2
|
||||||
|
height: Kirigami.Units.gridUnit * 2
|
||||||
|
|
||||||
|
onClicked: root.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/qml/Component/LocationPage.qml
Normal file
59
src/qml/Component/LocationPage.qml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import QtQuick 2.15
|
||||||
|
import QtQuick.Controls 2.15 as QQC2
|
||||||
|
import QtLocation 5.15
|
||||||
|
import QtPositioning 5.15
|
||||||
|
|
||||||
|
import org.kde.kirigami 2.20 as Kirigami
|
||||||
|
import org.kde.neochat 1.0
|
||||||
|
|
||||||
|
Kirigami.Page {
|
||||||
|
id: locationsPage
|
||||||
|
|
||||||
|
required property var room
|
||||||
|
|
||||||
|
title: i18nc("Locations on a map", "Locations")
|
||||||
|
|
||||||
|
padding: 0
|
||||||
|
|
||||||
|
Map {
|
||||||
|
id: map
|
||||||
|
anchors.fill: parent
|
||||||
|
plugin: Plugin {
|
||||||
|
name: "osm"
|
||||||
|
PluginParameter {
|
||||||
|
name: "osm.useragent"
|
||||||
|
value: Application.name + "/" + Application.version + " (kde-devel@kde.org)"
|
||||||
|
}
|
||||||
|
PluginParameter {
|
||||||
|
name: "osm.mapping.providersrepository.address"
|
||||||
|
value: "https://autoconfig.kde.org/qtlocation/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MapItemView {
|
||||||
|
model: LocationsModel {
|
||||||
|
room: locationsPage.room
|
||||||
|
}
|
||||||
|
delegate: MapQuickItem {
|
||||||
|
id: point
|
||||||
|
|
||||||
|
required property var longitude
|
||||||
|
required property var latitude
|
||||||
|
required property string text
|
||||||
|
anchorPoint.x: icon.width / 2
|
||||||
|
anchorPoint.y: icon.height / 2
|
||||||
|
coordinate: QtPositioning.coordinate(point.latitude, point.longitude)
|
||||||
|
autoFadeIn: false
|
||||||
|
sourceItem: Kirigami.Icon {
|
||||||
|
id: icon
|
||||||
|
width: height
|
||||||
|
height: Kirigami.Units.iconSizes.medium
|
||||||
|
source: "flag-blue"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/qml/Component/Timeline/AvatarFlow.qml
Normal file
37
src/qml/Component/Timeline/AvatarFlow.qml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
import QtQuick 2.15
|
||||||
|
import QtQuick.Controls 2.15 as QQC2
|
||||||
|
|
||||||
|
import org.kde.kirigami 2.15 as Kirigami
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var avatarSize: Kirigami.Units.iconSizes.small
|
||||||
|
property alias model: avatarFlowRepeater.model
|
||||||
|
property string toolTipText
|
||||||
|
|
||||||
|
spacing: -avatarSize / 2
|
||||||
|
Repeater {
|
||||||
|
id: avatarFlowRepeater
|
||||||
|
delegate: Kirigami.Avatar {
|
||||||
|
implicitWidth: avatarSize
|
||||||
|
implicitHeight: avatarSize
|
||||||
|
|
||||||
|
name: modelData.displayName
|
||||||
|
source: modelData.avatarMediaId ? ("image://mxc/" + modelData.avatarMediaId) : ""
|
||||||
|
color: modelData.color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.ToolTip.text: toolTipText
|
||||||
|
QQC2.ToolTip.visible: hoverHandler.hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
id: hoverHandler
|
||||||
|
margin: Kirigami.Units.smallSpacing
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -75,6 +75,11 @@ DelegateChooser {
|
|||||||
delegate: PollDelegate {}
|
delegate: PollDelegate {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MessageEventModel.Location
|
||||||
|
delegate: LocationDelegate {}
|
||||||
|
}
|
||||||
|
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: MessageEventModel.Other
|
roleValue: MessageEventModel.Other
|
||||||
delegate: Item {}
|
delegate: Item {}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ Loader {
|
|||||||
*/
|
*/
|
||||||
property bool indicatorEnabled: false
|
property bool indicatorEnabled: false
|
||||||
|
|
||||||
active: !currentRoom.usesEncryption && model.display && links && links.length > 0
|
active: !currentRoom.usesEncryption && model.display && links && links.length > 0 && currentRoom.urlPreviewEnabled
|
||||||
visible: Config.showLinkPreview && active
|
visible: Config.showLinkPreview && active
|
||||||
sourceComponent: linkPreviewer.loaded ? linkPreviewComponent : loadingComponent
|
sourceComponent: linkPreviewer.loaded ? linkPreviewComponent : loadingComponent
|
||||||
|
|
||||||
|
|||||||
74
src/qml/Component/Timeline/LocationDelegate.qml
Normal file
74
src/qml/Component/Timeline/LocationDelegate.qml
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import QtQuick 2.15
|
||||||
|
import QtQuick.Controls 2.15
|
||||||
|
import QtQuick.Layouts 1.15
|
||||||
|
import QtLocation 5.15
|
||||||
|
import QtPositioning 5.15
|
||||||
|
|
||||||
|
import org.kde.kirigami 2.15 as Kirigami
|
||||||
|
|
||||||
|
import org.kde.neochat 1.0
|
||||||
|
|
||||||
|
TimelineContainer {
|
||||||
|
id: locationDelegate
|
||||||
|
|
||||||
|
property string latlong: model.content.geo_uri.split(':')[1]
|
||||||
|
property string latitude: latlong.split(',')[0]
|
||||||
|
property string longitude: latlong.split(',')[1]
|
||||||
|
|
||||||
|
property string formattedBody: model.content.formatted_body
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.maximumWidth: locationDelegate.contentMaxWidth
|
||||||
|
Layout.preferredWidth: locationDelegate.contentMaxWidth
|
||||||
|
Map {
|
||||||
|
id: map
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: locationDelegate.contentMaxWidth / 16 * 9
|
||||||
|
|
||||||
|
center: QtPositioning.coordinate(locationDelegate.latitude, locationDelegate.longitude)
|
||||||
|
zoomLevel: 15
|
||||||
|
plugin: Plugin {
|
||||||
|
name: "osm"
|
||||||
|
PluginParameter {
|
||||||
|
name: "osm.useragent"
|
||||||
|
value: Application.name + "/" + Application.version + " (kde-devel@kde.org)"
|
||||||
|
}
|
||||||
|
PluginParameter {
|
||||||
|
name: "osm.mapping.providersrepository.address"
|
||||||
|
value: "https://autoconfig.kde.org/qtlocation/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MapCircle {
|
||||||
|
radius: 1500 / map.zoomLevel
|
||||||
|
color: Kirigami.Theme.highlightColor
|
||||||
|
border.color: Kirigami.Theme.linkColor
|
||||||
|
border.width: Kirigami.Units.devicePixelRatio * 2
|
||||||
|
smooth: true
|
||||||
|
opacity: 0.25
|
||||||
|
center: QtPositioning.coordinate(latitude, longitude)
|
||||||
|
}
|
||||||
|
onCopyrightLinkActivated: {
|
||||||
|
Qt.openUrlExternally(link)
|
||||||
|
}
|
||||||
|
TapHandler {
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
onTapped: {
|
||||||
|
let map = fullScreenMap.createObject(parent, {content: model.content});
|
||||||
|
map.open()
|
||||||
|
}
|
||||||
|
onLongPressed: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody ?? model.body, parent.selectedText)
|
||||||
|
}
|
||||||
|
TapHandler {
|
||||||
|
acceptedButtons: Qt.RightButton
|
||||||
|
onTapped: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody ?? model.body, parent.selectedText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Component {
|
||||||
|
id: fullScreenMap
|
||||||
|
FullScreenMap { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,25 +16,7 @@ TextEdit {
|
|||||||
|
|
||||||
property bool isEmote: false
|
property bool isEmote: false
|
||||||
property bool isReplyLabel: false
|
property bool isReplyLabel: false
|
||||||
|
property string textMessage: model.display
|
||||||
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 `<a href="${link}">${link}</a>${leftover}`;
|
|
||||||
}
|
|
||||||
return `<a href="${l}">${l}</a>`;
|
|
||||||
})
|
|
||||||
: model.display
|
|
||||||
property bool spoilerRevealed: !hasSpoiler.test(textMessage)
|
property bool spoilerRevealed: !hasSpoiler.test(textMessage)
|
||||||
|
|
||||||
ListView.onReused: Qt.binding(() => !hasSpoiler.test(textMessage))
|
ListView.onReused: Qt.binding(() => !hasSpoiler.test(textMessage))
|
||||||
|
|||||||
@@ -134,5 +134,12 @@ QQC2.Control {
|
|||||||
folded = !folded
|
folded = !folded
|
||||||
foldedChanged()
|
foldedChanged()
|
||||||
}
|
}
|
||||||
|
AvatarFlow {
|
||||||
|
Layout.alignment: Qt.AlignRight
|
||||||
|
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||||
|
visible: showReadMarkers
|
||||||
|
model: readMarkers
|
||||||
|
toolTipText: readMarkersString
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -338,6 +338,13 @@ ColumnLayout {
|
|||||||
|
|
||||||
visible: eventType !== MessageEventModel.State && eventType !== MessageEventModel.Notice && reaction != undefined && reaction.length > 0
|
visible: eventType !== MessageEventModel.State && eventType !== MessageEventModel.Notice && reaction != undefined && reaction.length > 0
|
||||||
}
|
}
|
||||||
|
AvatarFlow {
|
||||||
|
Layout.alignment: Qt.AlignRight
|
||||||
|
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||||
|
visible: showReadMarkers
|
||||||
|
model: readMarkers
|
||||||
|
toolTipText: readMarkersString
|
||||||
|
}
|
||||||
|
|
||||||
function isVisibleInTimeline() {
|
function isVisibleInTimeline() {
|
||||||
let yoff = Math.round(y - ListView.view.contentY);
|
let yoff = Math.round(y - ListView.view.contentY);
|
||||||
|
|||||||
@@ -144,12 +144,17 @@ QQC2.ToolBar {
|
|||||||
actions.main: Kirigami.Action {
|
actions.main: Kirigami.Action {
|
||||||
text: i18n("Edit this account")
|
text: i18n("Edit this account")
|
||||||
icon.name: "document-edit"
|
icon.name: "document-edit"
|
||||||
onTriggered: pageStack.pushDialogLayer(Qt.resolvedUrl('./AccountEditorPage.qml'), {
|
onTriggered: pageStack.pushDialogLayer(Qt.resolvedUrl('qrc:/AccountEditorPage.qml'), {
|
||||||
connection: Controller.activeConnection
|
connection: Controller.activeConnection
|
||||||
}, {
|
}, {
|
||||||
title: i18n("Account editor")
|
title: i18n("Account editor")
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
TapHandler {
|
||||||
|
acceptedButtons: Qt.RightButton
|
||||||
|
acceptedDevices: PointerDevice.Mouse
|
||||||
|
onTapped: accountMenu.open()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
@@ -214,6 +219,11 @@ QQC2.ToolBar {
|
|||||||
Item {
|
Item {
|
||||||
width: 1
|
width: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AccountMenu {
|
||||||
|
id: accountMenu
|
||||||
|
y: -height
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
44
src/qml/Menu/AccountMenu.qml
Normal file
44
src/qml/Menu/AccountMenu.qml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
import QtQuick 2.15
|
||||||
|
import QtQuick.Controls 2.15 as QQC2
|
||||||
|
import QtQuick.Layouts 1.15
|
||||||
|
|
||||||
|
import org.kde.kirigami 2.19 as Kirigami
|
||||||
|
|
||||||
|
import org.kde.neochat 1.0
|
||||||
|
|
||||||
|
QQC2.Menu {
|
||||||
|
id: root
|
||||||
|
margins: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
QQC2.MenuItem {
|
||||||
|
text: i18n("Edit this account")
|
||||||
|
icon.name: "document-edit"
|
||||||
|
onTriggered: pageStack.pushDialogLayer("qrc:/AccountEditorPage.qml", {
|
||||||
|
connection: Controller.activeConnection
|
||||||
|
}, {
|
||||||
|
title: i18n("Account editor")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
QQC2.MenuItem {
|
||||||
|
text: i18n("Notification settings")
|
||||||
|
icon.name: "notifications"
|
||||||
|
onTriggered: pageStack.pushDialogLayer("qrc:/SettingsPage.qml", {defaultPage: "notifications"}, { title: i18n("Configure")})
|
||||||
|
}
|
||||||
|
QQC2.MenuItem {
|
||||||
|
text: i18n("Devices")
|
||||||
|
icon.name: "computer-symbolic"
|
||||||
|
onTriggered: pageStack.pushDialogLayer("qrc:/SettingsPage.qml", {defaultPage: "devices"}, { title: i18n("Configure")})
|
||||||
|
}
|
||||||
|
QQC2.MenuItem {
|
||||||
|
text: i18n("Logout")
|
||||||
|
icon.name: "list-remove-user"
|
||||||
|
onTriggered: confirmLogoutDialog.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfirmLogoutDialog {
|
||||||
|
id: confirmLogoutDialog
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,7 +48,9 @@ Kirigami.ScrollablePage {
|
|||||||
applicationWindow().hoverLinkIndicator.text = "";
|
applicationWindow().hoverLinkIndicator.text = "";
|
||||||
messageListView.positionViewAtBeginning();
|
messageListView.positionViewAtBeginning();
|
||||||
hasScrolledUpBefore = false;
|
hasScrolledUpBefore = false;
|
||||||
chatBox.chatBar.forceActiveFocus();
|
if (!Kirigami.Settings.isMobile) {
|
||||||
|
chatBox.chatBar.forceActiveFocus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
@@ -353,7 +355,9 @@ Kirigami.ScrollablePage {
|
|||||||
visible: currentRoom && currentRoom.hasUnreadMessages && currentRoom.readMarkerLoaded
|
visible: currentRoom && currentRoom.hasUnreadMessages && currentRoom.readMarkerLoaded
|
||||||
action: Kirigami.Action {
|
action: Kirigami.Action {
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
chatBox.chatBar.forceActiveFocus();
|
if (!Kirigami.Settings.isMobile) {
|
||||||
|
chatBox.chatBar.forceActiveFocus();
|
||||||
|
}
|
||||||
messageListView.goToEvent(currentRoom.readMarkerEventId)
|
messageListView.goToEvent(currentRoom.readMarkerEventId)
|
||||||
}
|
}
|
||||||
icon.name: "go-up"
|
icon.name: "go-up"
|
||||||
@@ -378,7 +382,9 @@ Kirigami.ScrollablePage {
|
|||||||
visible: !messageListView.atYEnd
|
visible: !messageListView.atYEnd
|
||||||
action: Kirigami.Action {
|
action: Kirigami.Action {
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
chatBox.chatBar.forceActiveFocus();
|
if (!Kirigami.Settings.isMobile) {
|
||||||
|
chatBox.chatBar.forceActiveFocus();
|
||||||
|
}
|
||||||
goToLastMessage();
|
goToLastMessage();
|
||||||
currentRoom.markAllMessagesAsRead();
|
currentRoom.markAllMessagesAsRead();
|
||||||
}
|
}
|
||||||
@@ -531,7 +537,9 @@ Kirigami.ScrollablePage {
|
|||||||
showQuickReaction: true
|
showQuickReaction: true
|
||||||
onChosen: {
|
onChosen: {
|
||||||
page.currentRoom.toggleReaction(hoverActions.event.eventId, emoji);
|
page.currentRoom.toggleReaction(hoverActions.event.eventId, emoji);
|
||||||
chatBox.chatBar.forceActiveFocus();
|
if (!Kirigami.Settings.isMobile) {
|
||||||
|
chatBox.chatBar.forceActiveFocus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -209,6 +209,18 @@ Kirigami.OverlayDrawer {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Kirigami.BasicListItem {
|
||||||
|
id: locationsButton
|
||||||
|
|
||||||
|
icon: "map-flat"
|
||||||
|
text: i18n("Show locations for this room")
|
||||||
|
|
||||||
|
onClicked: pageStack.pushDialogLayer("qrc:/LocationsPage.qml", {
|
||||||
|
room: room
|
||||||
|
}, {
|
||||||
|
title: i18nc("Locations on a map", "Locations")
|
||||||
|
})
|
||||||
|
}
|
||||||
Kirigami.BasicListItem {
|
Kirigami.BasicListItem {
|
||||||
id: favouriteButton
|
id: favouriteButton
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ Kirigami.CategorizedSettings {
|
|||||||
objectName: "settingsPage"
|
objectName: "settingsPage"
|
||||||
actions: [
|
actions: [
|
||||||
Kirigami.SettingAction {
|
Kirigami.SettingAction {
|
||||||
|
actionName: "general"
|
||||||
text: i18n("General")
|
text: i18n("General")
|
||||||
icon.name: "settings-configure"
|
icon.name: "settings-configure"
|
||||||
page: Qt.resolvedUrl("General.qml")
|
page: Qt.resolvedUrl("General.qml")
|
||||||
@@ -22,6 +23,7 @@ Kirigami.CategorizedSettings {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
Kirigami.SettingAction {
|
Kirigami.SettingAction {
|
||||||
|
actionName: "security"
|
||||||
text: i18n("Security")
|
text: i18n("Security")
|
||||||
icon.name: "security-low"
|
icon.name: "security-low"
|
||||||
page: Qt.resolvedUrl("Security.qml")
|
page: Qt.resolvedUrl("Security.qml")
|
||||||
@@ -32,6 +34,7 @@ Kirigami.CategorizedSettings {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
Kirigami.SettingAction {
|
Kirigami.SettingAction {
|
||||||
|
actionName: "permissions"
|
||||||
text: i18n("Permissions")
|
text: i18n("Permissions")
|
||||||
icon.name: "visibility"
|
icon.name: "visibility"
|
||||||
page: Qt.resolvedUrl("Permissions.qml")
|
page: Qt.resolvedUrl("Permissions.qml")
|
||||||
@@ -42,6 +45,7 @@ Kirigami.CategorizedSettings {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
Kirigami.SettingAction {
|
Kirigami.SettingAction {
|
||||||
|
actionName: "notifications"
|
||||||
text: i18n("Notifications")
|
text: i18n("Notifications")
|
||||||
icon.name: "notifications"
|
icon.name: "notifications"
|
||||||
page: Qt.resolvedUrl("PushNotification.qml")
|
page: Qt.resolvedUrl("PushNotification.qml")
|
||||||
|
|||||||
@@ -263,6 +263,32 @@ Kirigami.ScrollablePage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
MobileForm.FormCard {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
contentItem: ColumnLayout {
|
||||||
|
spacing: 0
|
||||||
|
MobileForm.FormCardHeader {
|
||||||
|
title: i18n("URL Previews")
|
||||||
|
}
|
||||||
|
MobileForm.FormCheckDelegate {
|
||||||
|
text: i18n("Enable URL previews by default for room members")
|
||||||
|
checked: room.defaultUrlPreviewState
|
||||||
|
visible: room.canSendState("org.matrix.room.preview_urls")
|
||||||
|
onToggled: {
|
||||||
|
room.defaultUrlPreviewState = checked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MobileForm.FormCheckDelegate {
|
||||||
|
text: i18n("Enable URL previews")
|
||||||
|
// Most users won't see the above setting so tell them the default.
|
||||||
|
description: room.defaultUrlPreviewState ? i18n("URL previews are enabled by default in this room") : i18n("URL previews are disabled by default in this room")
|
||||||
|
checked: room.urlPreviewEnabled
|
||||||
|
onToggled: {
|
||||||
|
room.urlPreviewEnabled = checked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Kirigami.InlineMessage {
|
Kirigami.InlineMessage {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|||||||
@@ -77,8 +77,38 @@ Kirigami.ScrollablePage {
|
|||||||
title: i18n("Timeline Events")
|
title: i18n("Timeline Events")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MobileForm.FormCheckDelegate {
|
||||||
|
id: showDeletedMessages
|
||||||
|
text: i18n("Show deleted messages")
|
||||||
|
checked: Config.showDeletedMessages
|
||||||
|
enabled: !Config.isShowDeletedMessagesImmutable
|
||||||
|
onToggled: {
|
||||||
|
Config.showDeletedMessages = checked
|
||||||
|
Config.save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MobileForm.FormDelegateSeparator { above: showDeletedMessages; below: showStateEvents }
|
||||||
|
|
||||||
|
MobileForm.FormCheckDelegate {
|
||||||
|
id: showStateEvents
|
||||||
|
text: i18n("Show state events")
|
||||||
|
checked: Config.showStateEvent
|
||||||
|
enabled: !Config.isShowStateEventImmutable
|
||||||
|
onToggled: {
|
||||||
|
Config.showStateEvent = checked
|
||||||
|
Config.save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MobileForm.FormDelegateSeparator {
|
||||||
|
visible: Config.showStateEvent
|
||||||
|
above: showStateEvents
|
||||||
|
below: showLeaveJoinEventDelegate }
|
||||||
|
|
||||||
MobileForm.FormCheckDelegate {
|
MobileForm.FormCheckDelegate {
|
||||||
id: showLeaveJoinEventDelegate
|
id: showLeaveJoinEventDelegate
|
||||||
|
visible: Config.showStateEvent
|
||||||
text: i18n("Show leave and join events")
|
text: i18n("Show leave and join events")
|
||||||
checked: Config.showLeaveJoinEvent
|
checked: Config.showLeaveJoinEvent
|
||||||
enabled: !Config.isShowLeaveJoinEventImmutable
|
enabled: !Config.isShowLeaveJoinEventImmutable
|
||||||
@@ -88,10 +118,15 @@ Kirigami.ScrollablePage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MobileForm.FormDelegateSeparator { above: showLeaveJoinEventDelegate; below: showNameDelegate }
|
MobileForm.FormDelegateSeparator {
|
||||||
|
visible: Config.showStateEvent
|
||||||
|
above: showLeaveJoinEventDelegate
|
||||||
|
below: showNameDelegate
|
||||||
|
}
|
||||||
|
|
||||||
MobileForm.FormCheckDelegate {
|
MobileForm.FormCheckDelegate {
|
||||||
id: showNameDelegate
|
id: showNameDelegate
|
||||||
|
visible: Config.showStateEvent
|
||||||
text: i18n("Show name change events")
|
text: i18n("Show name change events")
|
||||||
checked: Config.showRename
|
checked: Config.showRename
|
||||||
enabled: !Config.isShowRenameImmutable
|
enabled: !Config.isShowRenameImmutable
|
||||||
@@ -101,10 +136,15 @@ Kirigami.ScrollablePage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MobileForm.FormDelegateSeparator { above: showNameDelegate; below: showAvatarChangeDelegate }
|
MobileForm.FormDelegateSeparator {
|
||||||
|
visible: Config.showStateEvent
|
||||||
|
above: showNameDelegate
|
||||||
|
below: showAvatarChangeDelegate
|
||||||
|
}
|
||||||
|
|
||||||
MobileForm.FormCheckDelegate {
|
MobileForm.FormCheckDelegate {
|
||||||
id: showAvatarChangeDelegate
|
id: showAvatarChangeDelegate
|
||||||
|
visible: Config.showStateEvent
|
||||||
text: i18n("Show avatar update events")
|
text: i18n("Show avatar update events")
|
||||||
checked: Config.showAvatarUpdate
|
checked: Config.showAvatarUpdate
|
||||||
enabled: !Config.isShowAvatarUpdateImmutable
|
enabled: !Config.isShowAvatarUpdateImmutable
|
||||||
@@ -113,19 +153,6 @@ Kirigami.ScrollablePage {
|
|||||||
Config.save()
|
Config.save()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MobileForm.FormDelegateSeparator { above: showAvatarChangeDelegate; below: showDeletedMessages }
|
|
||||||
|
|
||||||
MobileForm.FormCheckDelegate {
|
|
||||||
id: showDeletedMessages
|
|
||||||
text: i18n("Show deleted messages")
|
|
||||||
checked: Config.showDeletedMessages
|
|
||||||
enabled: !Config.isShowDeletedMessagesImmutable
|
|
||||||
onToggled: {
|
|
||||||
Config.showDeletedMessages = checked
|
|
||||||
Config.save()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,52 +9,62 @@ Kirigami.CategorizedSettings {
|
|||||||
objectName: "settingsPage"
|
objectName: "settingsPage"
|
||||||
actions: [
|
actions: [
|
||||||
Kirigami.SettingAction {
|
Kirigami.SettingAction {
|
||||||
|
actionName: "general"
|
||||||
text: i18n("General")
|
text: i18n("General")
|
||||||
icon.name: "org.kde.neochat"
|
icon.name: "org.kde.neochat"
|
||||||
page: Qt.resolvedUrl("GeneralSettingsPage.qml")
|
page: Qt.resolvedUrl("GeneralSettingsPage.qml")
|
||||||
},
|
},
|
||||||
Kirigami.SettingAction {
|
Kirigami.SettingAction {
|
||||||
|
actionName: "appearance"
|
||||||
text: i18n("Appearance")
|
text: i18n("Appearance")
|
||||||
icon.name: "preferences-desktop-theme-global"
|
icon.name: "preferences-desktop-theme-global"
|
||||||
page: Qt.resolvedUrl("AppearanceSettingsPage.qml")
|
page: Qt.resolvedUrl("AppearanceSettingsPage.qml")
|
||||||
},
|
},
|
||||||
Kirigami.SettingAction {
|
Kirigami.SettingAction {
|
||||||
|
actionName: "notifications"
|
||||||
text: i18n("Notifications")
|
text: i18n("Notifications")
|
||||||
icon.name: "preferences-desktop-notification"
|
icon.name: "preferences-desktop-notification"
|
||||||
page: Qt.resolvedUrl("GlobalNotificationsPage.qml")
|
page: Qt.resolvedUrl("GlobalNotificationsPage.qml")
|
||||||
},
|
},
|
||||||
Kirigami.SettingAction {
|
Kirigami.SettingAction {
|
||||||
|
actionName: "accounts"
|
||||||
text: i18n("Accounts")
|
text: i18n("Accounts")
|
||||||
icon.name: "preferences-system-users"
|
icon.name: "preferences-system-users"
|
||||||
page: Qt.resolvedUrl("AccountsPage.qml")
|
page: Qt.resolvedUrl("AccountsPage.qml")
|
||||||
},
|
},
|
||||||
Kirigami.SettingAction {
|
Kirigami.SettingAction {
|
||||||
|
actionName: "customEmojis"
|
||||||
text: i18n("Custom Emojis")
|
text: i18n("Custom Emojis")
|
||||||
icon.name: "preferences-desktop-emoticons"
|
icon.name: "preferences-desktop-emoticons"
|
||||||
page: Qt.resolvedUrl("Emoticons.qml")
|
page: Qt.resolvedUrl("Emoticons.qml")
|
||||||
},
|
},
|
||||||
Kirigami.SettingAction {
|
Kirigami.SettingAction {
|
||||||
|
actionName: "spellChecking"
|
||||||
text: i18n("Spell Checking")
|
text: i18n("Spell Checking")
|
||||||
icon.name: "tools-check-spelling"
|
icon.name: "tools-check-spelling"
|
||||||
page: Qt.resolvedUrl("SonnetConfigPage.qml")
|
page: Qt.resolvedUrl("SonnetConfigPage.qml")
|
||||||
visible: Qt.platform.os !== "android"
|
visible: Qt.platform.os !== "android"
|
||||||
},
|
},
|
||||||
Kirigami.SettingAction {
|
Kirigami.SettingAction {
|
||||||
|
actionName: "networkProxy"
|
||||||
text: i18n("Network Proxy")
|
text: i18n("Network Proxy")
|
||||||
icon.name: "network-connect"
|
icon.name: "network-connect"
|
||||||
page: Qt.resolvedUrl("NetworkProxyPage.qml")
|
page: Qt.resolvedUrl("NetworkProxyPage.qml")
|
||||||
},
|
},
|
||||||
Kirigami.SettingAction {
|
Kirigami.SettingAction {
|
||||||
|
actionName: "devices"
|
||||||
text: i18n("Devices")
|
text: i18n("Devices")
|
||||||
icon.name: "computer"
|
icon.name: "computer"
|
||||||
page: Qt.resolvedUrl("DevicesPage.qml")
|
page: Qt.resolvedUrl("DevicesPage.qml")
|
||||||
},
|
},
|
||||||
Kirigami.SettingAction {
|
Kirigami.SettingAction {
|
||||||
|
actionName: "aboutNeochat"
|
||||||
text: i18n("About NeoChat")
|
text: i18n("About NeoChat")
|
||||||
icon.name: "help-about"
|
icon.name: "help-about"
|
||||||
page: Qt.resolvedUrl("About.qml")
|
page: Qt.resolvedUrl("About.qml")
|
||||||
},
|
},
|
||||||
Kirigami.SettingAction {
|
Kirigami.SettingAction {
|
||||||
|
actionName: "aboutKDE"
|
||||||
text: i18n("About KDE")
|
text: i18n("About KDE")
|
||||||
icon.name: "kde"
|
icon.name: "kde"
|
||||||
page: Qt.resolvedUrl("AboutKDE.qml")
|
page: Qt.resolvedUrl("AboutKDE.qml")
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ Kirigami.ApplicationWindow {
|
|||||||
|
|
||||||
pageStack.defaultColumnWidth: roomListPage ? roomListPage.currentWidth : 0
|
pageStack.defaultColumnWidth: roomListPage ? roomListPage.currentWidth : 0
|
||||||
pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.ToolBar
|
pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.ToolBar
|
||||||
pageStack.globalToolBar.showNavigationButtons: pageStack.currentIndex > 0 ? Kirigami.ApplicationHeaderStyle.ShowBackButton : 0
|
pageStack.globalToolBar.showNavigationButtons: pageStack.currentIndex > 0 || pageStack.layers.depth > 1 ? Kirigami.ApplicationHeaderStyle.ShowBackButton : 0
|
||||||
|
|
||||||
ConfirmLogoutDialog {
|
ConfirmLogoutDialog {
|
||||||
id: confirmLogoutDialog
|
id: confirmLogoutDialog
|
||||||
|
|||||||
@@ -29,6 +29,8 @@
|
|||||||
<file alias="AttachmentPane.qml">qml/Component/ChatBox/AttachmentPane.qml</file>
|
<file alias="AttachmentPane.qml">qml/Component/ChatBox/AttachmentPane.qml</file>
|
||||||
<file alias="ReplyPane.qml">qml/Component/ChatBox/ReplyPane.qml</file>
|
<file alias="ReplyPane.qml">qml/Component/ChatBox/ReplyPane.qml</file>
|
||||||
<file alias="CompletionMenu.qml">qml/Component/ChatBox/CompletionMenu.qml</file>
|
<file alias="CompletionMenu.qml">qml/Component/ChatBox/CompletionMenu.qml</file>
|
||||||
|
<file alias="PieProgressBar.qml">qml/Component/ChatBox/PieProgressBar.qml</file>
|
||||||
|
<file alias="QuickFormatBar.qml">qml/Component/ChatBox/QuickFormatBar.qml</file>
|
||||||
<file alias="EmojiPicker.qml">qml/Component/Emoji/EmojiPicker.qml</file>
|
<file alias="EmojiPicker.qml">qml/Component/Emoji/EmojiPicker.qml</file>
|
||||||
<file alias="ReplyComponent.qml">qml/Component/Timeline/ReplyComponent.qml</file>
|
<file alias="ReplyComponent.qml">qml/Component/Timeline/ReplyComponent.qml</file>
|
||||||
<file alias="StateDelegate.qml">qml/Component/Timeline/StateDelegate.qml</file>
|
<file alias="StateDelegate.qml">qml/Component/Timeline/StateDelegate.qml</file>
|
||||||
@@ -49,6 +51,7 @@
|
|||||||
<file alias="MimeComponent.qml">qml/Component/Timeline/MimeComponent.qml</file>
|
<file alias="MimeComponent.qml">qml/Component/Timeline/MimeComponent.qml</file>
|
||||||
<file alias="StateComponent.qml">qml/Component/Timeline/StateComponent.qml</file>
|
<file alias="StateComponent.qml">qml/Component/Timeline/StateComponent.qml</file>
|
||||||
<file alias="MessageEditComponent.qml">qml/Component/Timeline/MessageEditComponent.qml</file>
|
<file alias="MessageEditComponent.qml">qml/Component/Timeline/MessageEditComponent.qml</file>
|
||||||
|
<file alias="AvatarFlow.qml">qml/Component/Timeline/AvatarFlow.qml</file>
|
||||||
<file alias="LoginStep.qml">qml/Component/Login/LoginStep.qml</file>
|
<file alias="LoginStep.qml">qml/Component/Login/LoginStep.qml</file>
|
||||||
<file alias="Login.qml">qml/Component/Login/Login.qml</file>
|
<file alias="Login.qml">qml/Component/Login/Login.qml</file>
|
||||||
<file alias="Password.qml">qml/Component/Login/Password.qml</file>
|
<file alias="Password.qml">qml/Component/Login/Password.qml</file>
|
||||||
@@ -72,6 +75,7 @@
|
|||||||
<file alias="VerificationCanceled.qml">qml/Dialog/KeyVerification/VerificationCanceled.qml</file>
|
<file alias="VerificationCanceled.qml">qml/Dialog/KeyVerification/VerificationCanceled.qml</file>
|
||||||
<file alias="GlobalMenu.qml">qml/Menu/GlobalMenu.qml</file>
|
<file alias="GlobalMenu.qml">qml/Menu/GlobalMenu.qml</file>
|
||||||
<file alias="EditMenu.qml">qml/Menu/EditMenu.qml</file>
|
<file alias="EditMenu.qml">qml/Menu/EditMenu.qml</file>
|
||||||
|
<file alias="AccountMenu.qml">qml/Menu/AccountMenu.qml</file>
|
||||||
<file alias="MessageDelegateContextMenu.qml">qml/Menu/Timeline/MessageDelegateContextMenu.qml</file>
|
<file alias="MessageDelegateContextMenu.qml">qml/Menu/Timeline/MessageDelegateContextMenu.qml</file>
|
||||||
<file alias="FileDelegateContextMenu.qml">qml/Menu/Timeline/FileDelegateContextMenu.qml</file>
|
<file alias="FileDelegateContextMenu.qml">qml/Menu/Timeline/FileDelegateContextMenu.qml</file>
|
||||||
<file alias="MessageSourceSheet.qml">qml/Menu/Timeline/MessageSourceSheet.qml</file>
|
<file alias="MessageSourceSheet.qml">qml/Menu/Timeline/MessageSourceSheet.qml</file>
|
||||||
@@ -103,5 +107,8 @@
|
|||||||
<file alias="EmojiDelegate.qml">qml/Component/Emoji/EmojiDelegate.qml</file>
|
<file alias="EmojiDelegate.qml">qml/Component/Emoji/EmojiDelegate.qml</file>
|
||||||
<file alias="EmojiGrid.qml">qml/Component/Emoji/EmojiGrid.qml</file>
|
<file alias="EmojiGrid.qml">qml/Component/Emoji/EmojiGrid.qml</file>
|
||||||
<file alias="SearchPage.qml">qml/Page/SearchPage.qml</file>
|
<file alias="SearchPage.qml">qml/Page/SearchPage.qml</file>
|
||||||
|
<file alias="LocationDelegate.qml">qml/Component/Timeline/LocationDelegate.qml</file>
|
||||||
|
<file alias="FullScreenMap.qml">qml/Component/FullScreenMap.qml</file>
|
||||||
|
<file alias="LocationsPage.qml">qml/Component/LocationPage.qml</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
|
|
||||||
#include "spacehierarchycache.h"
|
#include "spacehierarchycache.h"
|
||||||
|
|
||||||
#include "controller.h"
|
|
||||||
#ifdef QUOTIENT_07
|
#ifdef QUOTIENT_07
|
||||||
#include <csapi/space_hierarchy.h>
|
#include <csapi/space_hierarchy.h>
|
||||||
#endif
|
#endif
|
||||||
|
#include <qt_connection_util.h>
|
||||||
|
|
||||||
|
#include "controller.h"
|
||||||
#include "neochatroom.h"
|
#include "neochatroom.h"
|
||||||
|
|
||||||
using namespace Quotient;
|
using namespace Quotient;
|
||||||
@@ -17,6 +19,8 @@ SpaceHierarchyCache::SpaceHierarchyCache(QObject *parent)
|
|||||||
cacheSpaceHierarchy();
|
cacheSpaceHierarchy();
|
||||||
connect(&Controller::instance(), &Controller::activeConnectionChanged, this, [this]() {
|
connect(&Controller::instance(), &Controller::activeConnectionChanged, this, [this]() {
|
||||||
cacheSpaceHierarchy();
|
cacheSpaceHierarchy();
|
||||||
|
connect(Controller::instance().activeConnection(), &Connection::joinedRoom, this, &SpaceHierarchyCache::addSpaceToHierarchy);
|
||||||
|
connect(Controller::instance().activeConnection(), &Connection::aboutToDeleteRoom, this, &SpaceHierarchyCache::removeSpaceFromHierarchy);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +72,24 @@ void SpaceHierarchyCache::populateSpaceHierarchy(const QString &spaceId)
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SpaceHierarchyCache::addSpaceToHierarchy(Quotient::Room *room)
|
||||||
|
{
|
||||||
|
connectSingleShot(room, &Quotient::Room::baseStateLoaded, this, [this, room]() {
|
||||||
|
const auto neoChatRoom = static_cast<NeoChatRoom *>(room);
|
||||||
|
if (neoChatRoom->isSpace()) {
|
||||||
|
populateSpaceHierarchy(neoChatRoom->id());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpaceHierarchyCache::removeSpaceFromHierarchy(Quotient::Room *room)
|
||||||
|
{
|
||||||
|
const auto neoChatRoom = static_cast<NeoChatRoom *>(room);
|
||||||
|
if (neoChatRoom->isSpace()) {
|
||||||
|
m_spaceHierarchy.remove(neoChatRoom->id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QVector<QString> &SpaceHierarchyCache::getRoomListForSpace(const QString &spaceId, bool updateCache)
|
QVector<QString> &SpaceHierarchyCache::getRoomListForSpace(const QString &spaceId, bool updateCache)
|
||||||
{
|
{
|
||||||
if (updateCache) {
|
if (updateCache) {
|
||||||
|
|||||||
@@ -8,6 +8,11 @@
|
|||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QVector>
|
#include <QVector>
|
||||||
|
|
||||||
|
namespace Quotient
|
||||||
|
{
|
||||||
|
class Room;
|
||||||
|
}
|
||||||
|
|
||||||
class SpaceHierarchyCache : public QObject
|
class SpaceHierarchyCache : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -24,6 +29,10 @@ public:
|
|||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void spaceHierarchyChanged();
|
void spaceHierarchyChanged();
|
||||||
|
|
||||||
|
private Q_SLOTS:
|
||||||
|
void addSpaceToHierarchy(Quotient::Room *room);
|
||||||
|
void removeSpaceFromHierarchy(Quotient::Room *room);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
explicit SpaceHierarchyCache(QObject *parent = nullptr);
|
explicit SpaceHierarchyCache(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
|||||||
384
src/texthandler.cpp
Normal file
384
src/texthandler.cpp
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
// 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 "texthandler.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
|
#include <util.h>
|
||||||
|
|
||||||
|
#include <cmark.h>
|
||||||
|
|
||||||
|
static const QStringList allowedTags = {
|
||||||
|
QStringLiteral("font"), QStringLiteral("del"), QStringLiteral("h1"), QStringLiteral("h2"), QStringLiteral("h3"), QStringLiteral("h4"),
|
||||||
|
QStringLiteral("h5"), QStringLiteral("h6"), QStringLiteral("blockquote"), QStringLiteral("p"), QStringLiteral("a"), QStringLiteral("ul"),
|
||||||
|
QStringLiteral("ol"), QStringLiteral("sup"), QStringLiteral("sub"), QStringLiteral("li"), QStringLiteral("b"), QStringLiteral("i"),
|
||||||
|
QStringLiteral("u"), QStringLiteral("strong"), QStringLiteral("em"), QStringLiteral("strike"), QStringLiteral("code"), QStringLiteral("hr"),
|
||||||
|
QStringLiteral("br"), QStringLiteral("div"), QStringLiteral("table"), QStringLiteral("thead"), QStringLiteral("tbody"), QStringLiteral("tr"),
|
||||||
|
QStringLiteral("th"), QStringLiteral("td"), QStringLiteral("caption"), QStringLiteral("pre"), QStringLiteral("span"), QStringLiteral("img"),
|
||||||
|
QStringLiteral("details"), QStringLiteral("summary")};
|
||||||
|
static const QHash<QString, QStringList> allowedAttributes = {
|
||||||
|
{QStringLiteral("font"), {QStringLiteral("data-mx-bg-color"), QStringLiteral("data-mx-color"), QStringLiteral("color")}},
|
||||||
|
{QStringLiteral("span"), {QStringLiteral("data-mx-bg-color"), QStringLiteral("data-mx-color"), QStringLiteral("data-mx-spoiler")}},
|
||||||
|
{QStringLiteral("a"), {QStringLiteral("name"), QStringLiteral("target"), QStringLiteral("href")}},
|
||||||
|
{QStringLiteral("img"), {QStringLiteral("width"), QStringLiteral("height"), QStringLiteral("alt"), QStringLiteral("title"), QStringLiteral("src")}},
|
||||||
|
{QStringLiteral("ol"), {QStringLiteral("start")}},
|
||||||
|
{QStringLiteral("code"), {QStringLiteral("class")}}};
|
||||||
|
static const QStringList allowedLinkSchemes = {QStringLiteral("https"),
|
||||||
|
QStringLiteral("http"),
|
||||||
|
QStringLiteral("ftp"),
|
||||||
|
QStringLiteral("mailto"),
|
||||||
|
QStringLiteral("magnet")};
|
||||||
|
|
||||||
|
QString TextHandler::data() const
|
||||||
|
{
|
||||||
|
return m_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextHandler::setData(const QString &string)
|
||||||
|
{
|
||||||
|
m_data = string;
|
||||||
|
m_pos = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString TextHandler::handleSendText()
|
||||||
|
{
|
||||||
|
m_pos = 0;
|
||||||
|
m_dataBuffer = markdownToHTML(m_data);
|
||||||
|
|
||||||
|
nextTokenType();
|
||||||
|
|
||||||
|
// Strip any disallowed tags/attributes.
|
||||||
|
QString outputString;
|
||||||
|
while (m_pos < m_dataBuffer.length()) {
|
||||||
|
next();
|
||||||
|
|
||||||
|
QString nextTokenBuffer = m_nextToken;
|
||||||
|
if (m_nextTokenType == Type::Text || m_nextTokenType == Type::TextCode) {
|
||||||
|
nextTokenBuffer = escapeHtml(nextTokenBuffer);
|
||||||
|
} else if (m_nextTokenType == Type::Tag) {
|
||||||
|
if (!isAllowedTag(getTagType())) {
|
||||||
|
nextTokenBuffer = QString();
|
||||||
|
}
|
||||||
|
nextTokenBuffer = cleanAttributes(getTagType(), nextTokenBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
outputString.append(nextTokenBuffer);
|
||||||
|
|
||||||
|
nextTokenType();
|
||||||
|
}
|
||||||
|
return outputString;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString TextHandler::handleRecieveRichText(Qt::TextFormat inputFormat, const NeoChatRoom *room, const Quotient::RoomEvent *event, bool stripNewlines)
|
||||||
|
{
|
||||||
|
m_pos = 0;
|
||||||
|
m_dataBuffer = m_data;
|
||||||
|
|
||||||
|
// Strip mx-reply if present.
|
||||||
|
m_dataBuffer.remove(TextRegex::removeRichReply);
|
||||||
|
|
||||||
|
// For plain text, convert links, escape html and convert line brakes.
|
||||||
|
if (inputFormat == Qt::PlainText) {
|
||||||
|
m_dataBuffer = escapeHtml(m_dataBuffer);
|
||||||
|
m_dataBuffer.replace(u'\n', QStringLiteral("<br>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linkify any plain text urls
|
||||||
|
m_dataBuffer = linkifyUrls(m_dataBuffer);
|
||||||
|
|
||||||
|
// Apply user style
|
||||||
|
m_dataBuffer.replace(TextRegex::userPill, QStringLiteral(R"(<b>\1</b>)"));
|
||||||
|
|
||||||
|
// Make all media URLs resolvable.
|
||||||
|
if (room && event) {
|
||||||
|
QRegularExpressionMatchIterator i = TextRegex::mxcImage.globalMatch(m_dataBuffer);
|
||||||
|
while (i.hasNext()) {
|
||||||
|
const QRegularExpressionMatch match = i.next();
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
const QUrl mediaUrl = room->makeMediaUrl(event->id(), QUrl(QStringLiteral("mxc://") + match.captured(2) + u'/' + match.captured(3)));
|
||||||
|
m_dataBuffer.replace(match.captured(0),
|
||||||
|
QStringLiteral("<img ") + match.captured(1) + QStringLiteral("src=\"") + mediaUrl.toString() + u'"' + match.captured(4)
|
||||||
|
+ u'>');
|
||||||
|
#else
|
||||||
|
auto url = room->connection()->homeserver();
|
||||||
|
auto base = url.scheme() + QStringLiteral("://") + url.host() + (url.port() != -1 ? ':' + QString::number(url.port()) : QString());
|
||||||
|
m_dataBuffer.replace(match.captured(0),
|
||||||
|
QStringLiteral("<img ") + match.captured(1) + QStringLiteral("src=\"") + base + QStringLiteral("/_matrix/media/r0/download/")
|
||||||
|
+ match.captured(2) + u'/' + match.captured(3) + u'"' + match.captured(4) + u'>');
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip any disallowed tags/attributes.
|
||||||
|
QString outputString;
|
||||||
|
nextTokenType();
|
||||||
|
while (m_pos < m_dataBuffer.length()) {
|
||||||
|
next();
|
||||||
|
|
||||||
|
QString nextTokenBuffer = m_nextToken;
|
||||||
|
if (m_nextTokenType == Type::Text || m_nextTokenType == Type::TextCode) {
|
||||||
|
nextTokenBuffer = escapeHtml(nextTokenBuffer);
|
||||||
|
} else if (m_nextTokenType == Type::Tag) {
|
||||||
|
if (!isAllowedTag(getTagType())) {
|
||||||
|
nextTokenBuffer = QString();
|
||||||
|
} else if ((getTagType() == QStringLiteral("br") && stripNewlines)) {
|
||||||
|
nextTokenBuffer = u' ';
|
||||||
|
}
|
||||||
|
nextTokenBuffer = cleanAttributes(getTagType(), nextTokenBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
outputString.append(nextTokenBuffer);
|
||||||
|
|
||||||
|
nextTokenType();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace <del> with <s>
|
||||||
|
* Note: <s> is still not a valid tag for the message from the server. We
|
||||||
|
* convert as that is what is needed for Qt::RichText.
|
||||||
|
*/
|
||||||
|
outputString.replace(TextRegex::strikethrough, QStringLiteral("<s>\\1</s>"));
|
||||||
|
return outputString;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString TextHandler::handleRecievePlainText(Qt::TextFormat inputFormat, const bool &stripNewlines)
|
||||||
|
{
|
||||||
|
m_pos = 0;
|
||||||
|
m_dataBuffer = m_data;
|
||||||
|
|
||||||
|
// Strip mx-reply if present.
|
||||||
|
m_dataBuffer.remove(TextRegex::removeRichReply);
|
||||||
|
|
||||||
|
if (stripNewlines) {
|
||||||
|
m_dataBuffer.replace(QStringLiteral("<br>"), QStringLiteral(" "));
|
||||||
|
m_dataBuffer.replace(QStringLiteral("<br />"), QStringLiteral(" "));
|
||||||
|
m_dataBuffer.replace(u'\n', QStringLiteral(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escaping then unescaping allows < and > to be maintained in a plain text string
|
||||||
|
// otherwise markdownToHTML will strip what it thinks is a bad html tag entirely.
|
||||||
|
if (inputFormat == Qt::PlainText) {
|
||||||
|
m_dataBuffer = escapeHtml(m_dataBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This seems counterproductive but by converting any markup which could
|
||||||
|
* arrive (e.g. in a caption body) it can then be stripped by the same code.
|
||||||
|
*/
|
||||||
|
m_dataBuffer = markdownToHTML(m_dataBuffer);
|
||||||
|
|
||||||
|
// Strip all tags/attributes except code blocks which will be escaped.
|
||||||
|
QString outputString;
|
||||||
|
nextTokenType();
|
||||||
|
while (m_pos < m_dataBuffer.length()) {
|
||||||
|
next();
|
||||||
|
|
||||||
|
QString nextTokenBuffer = m_nextToken;
|
||||||
|
if (m_nextTokenType == Type::TextCode) {
|
||||||
|
nextTokenBuffer = unescapeHtml(nextTokenBuffer);
|
||||||
|
} else if (m_nextTokenType == Type::Tag) {
|
||||||
|
nextTokenBuffer = QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
outputString.append(nextTokenBuffer);
|
||||||
|
|
||||||
|
nextTokenType();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escaping then unescaping allows < and > to be maintained in a plain text string
|
||||||
|
// otherwise markdownToHTML will strip what it thinks is a bad html tag entirely.
|
||||||
|
if (inputFormat == Qt::PlainText) {
|
||||||
|
outputString = unescapeHtml(outputString);
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputString;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextHandler::next()
|
||||||
|
{
|
||||||
|
QString searchStr;
|
||||||
|
if (m_nextTokenType == Type::Tag) {
|
||||||
|
searchStr = u'>';
|
||||||
|
} else if (m_nextTokenType == Type::TextCode) {
|
||||||
|
// Anything between code tags is assumed to be plain text
|
||||||
|
searchStr = QStringLiteral("</code>");
|
||||||
|
} else {
|
||||||
|
searchStr = u'<';
|
||||||
|
}
|
||||||
|
|
||||||
|
int tokenEnd = m_dataBuffer.indexOf(searchStr, m_pos + 1);
|
||||||
|
if (tokenEnd == -1) {
|
||||||
|
tokenEnd = m_dataBuffer.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_nextToken = m_dataBuffer.mid(m_pos, tokenEnd - m_pos + (m_nextTokenType == Type::Tag ? 1 : 0));
|
||||||
|
m_pos = tokenEnd + (m_nextTokenType == Type::Tag ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextHandler::nextTokenType()
|
||||||
|
{
|
||||||
|
if (m_pos >= m_dataBuffer.length()) {
|
||||||
|
// This is to stop the function accessing an index outside the length of
|
||||||
|
// m_dataBuffer during the final loop.
|
||||||
|
m_nextTokenType = Type::End;
|
||||||
|
} else if (m_nextTokenType == Type::Tag && getTagType() == QStringLiteral("code") && !isCloseTag()
|
||||||
|
&& m_dataBuffer.indexOf(QStringLiteral("</code>"), m_pos) != m_pos) {
|
||||||
|
m_nextTokenType = Type::TextCode;
|
||||||
|
} else if (m_dataBuffer[m_pos] == u'<' && m_dataBuffer[m_pos + 1] != u' ') {
|
||||||
|
m_nextTokenType = Type::Tag;
|
||||||
|
} else {
|
||||||
|
m_nextTokenType = Type::Text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString TextHandler::getTagType() const
|
||||||
|
{
|
||||||
|
const int tagTypeStart = m_nextToken[1] == u'/' ? 2 : 1;
|
||||||
|
const int tagTypeEnd = m_nextToken.indexOf(TextRegex::endTagType, tagTypeStart);
|
||||||
|
return m_nextToken.mid(tagTypeStart, tagTypeEnd - tagTypeStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TextHandler::isCloseTag() const
|
||||||
|
{
|
||||||
|
return m_nextToken[1] == u'/';
|
||||||
|
}
|
||||||
|
|
||||||
|
QString TextHandler::getAttributeType(const QString &string)
|
||||||
|
{
|
||||||
|
if (!string.contains(u'=')) {
|
||||||
|
return string;
|
||||||
|
}
|
||||||
|
const int equalsPos = string.indexOf(u'=');
|
||||||
|
return string.left(equalsPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString TextHandler::getAttributeData(const QString &string)
|
||||||
|
{
|
||||||
|
if (!string.contains(u'=')) {
|
||||||
|
return QStringLiteral();
|
||||||
|
}
|
||||||
|
const int equalsPos = string.indexOf(u'=');
|
||||||
|
return string.right(string.length() - equalsPos - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TextHandler::isAllowedTag(const QString &type)
|
||||||
|
{
|
||||||
|
return allowedTags.contains(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TextHandler::isAllowedAttribute(const QString &tag, const QString &attribute)
|
||||||
|
{
|
||||||
|
return allowedAttributes[tag].contains(attribute);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TextHandler::isAllowedLink(const QString &link, bool isImg)
|
||||||
|
{
|
||||||
|
const QUrl linkUrl = QUrl(link);
|
||||||
|
|
||||||
|
if (isImg) {
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
return !linkUrl.isRelative() && linkUrl.scheme() == "mxc";
|
||||||
|
#else
|
||||||
|
return !linkUrl.isRelative() && (linkUrl.scheme() == "mxc" || linkUrl.scheme() == "https");
|
||||||
|
#endif
|
||||||
|
} else {
|
||||||
|
return !linkUrl.isRelative() && allowedLinkSchemes.contains(linkUrl.scheme());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString TextHandler::cleanAttributes(const QString &tag, const QString &tagString)
|
||||||
|
{
|
||||||
|
int nextAttributeIndex = tagString.indexOf(u' ', 1);
|
||||||
|
|
||||||
|
if (nextAttributeIndex != -1) {
|
||||||
|
QString outputString = tagString.left(nextAttributeIndex);
|
||||||
|
QString nextAttribute;
|
||||||
|
int nextSpaceIndex;
|
||||||
|
nextAttributeIndex += 1;
|
||||||
|
|
||||||
|
while (nextAttributeIndex < tagString.length()) {
|
||||||
|
nextSpaceIndex = tagString.indexOf(TextRegex::endTagType, nextAttributeIndex);
|
||||||
|
if (nextSpaceIndex == -1) {
|
||||||
|
nextSpaceIndex = tagString.length();
|
||||||
|
}
|
||||||
|
nextAttribute = tagString.mid(nextAttributeIndex, nextSpaceIndex - nextAttributeIndex);
|
||||||
|
|
||||||
|
if (isAllowedAttribute(tag, getAttributeType(nextAttribute))) {
|
||||||
|
if (tag == QStringLiteral("img") && getAttributeType(nextAttribute) == QStringLiteral("src")) {
|
||||||
|
QString attributeData = TextRegex::attributeData.match(getAttributeData(nextAttribute)).captured(1);
|
||||||
|
if (isAllowedLink(attributeData, true)) {
|
||||||
|
outputString.append(u' ' + nextAttribute);
|
||||||
|
}
|
||||||
|
} else if (tag == u'a' && getAttributeType(nextAttribute) == QStringLiteral("href")) {
|
||||||
|
QString attributeData = TextRegex::attributeData.match(getAttributeData(nextAttribute)).captured(1);
|
||||||
|
if (isAllowedLink(attributeData)) {
|
||||||
|
outputString.append(u' ' + nextAttribute);
|
||||||
|
}
|
||||||
|
} else if (tag == QStringLiteral("code") && getAttributeType(nextAttribute) == QStringLiteral("class")) {
|
||||||
|
if (getAttributeData(nextAttribute).remove(u'"').startsWith(QStringLiteral("language-"))) {
|
||||||
|
outputString.append(u' ' + nextAttribute);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
outputString.append(u' ' + nextAttribute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nextAttributeIndex = nextSpaceIndex + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
outputString += u'>';
|
||||||
|
return outputString;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tagString;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString TextHandler::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(QStringLiteral("<!-- raw HTML omitted -->"), QString());
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: make this more intelligent currently other characters are not escaped
|
||||||
|
* especially & as this can conflict with the cmark markdown to html conversion
|
||||||
|
* which already escapes characters in code blocks. The < > still need to be handled
|
||||||
|
* when the user manually types in the html.
|
||||||
|
*/
|
||||||
|
QString TextHandler::escapeHtml(QString stringIn)
|
||||||
|
{
|
||||||
|
stringIn.replace(u'<', QStringLiteral("<"));
|
||||||
|
stringIn.replace(u'>', QStringLiteral(">"));
|
||||||
|
return stringIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString TextHandler::unescapeHtml(QString stringIn)
|
||||||
|
{
|
||||||
|
// For those situations where brackets in code block get double escaped
|
||||||
|
stringIn.replace(QStringLiteral("&lt;"), QStringLiteral("<"));
|
||||||
|
stringIn.replace(QStringLiteral("&gt;"), QStringLiteral(">"));
|
||||||
|
stringIn.replace(QStringLiteral("<"), QStringLiteral("<"));
|
||||||
|
stringIn.replace(QStringLiteral(">"), QStringLiteral(">"));
|
||||||
|
stringIn.replace(QStringLiteral("&"), QStringLiteral("&"));
|
||||||
|
stringIn.replace(QStringLiteral("""), QStringLiteral("\""));
|
||||||
|
return stringIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString TextHandler::linkifyUrls(QString stringIn)
|
||||||
|
{
|
||||||
|
stringIn = stringIn.replace(TextRegex::mxId, QStringLiteral(R"(\1<a href="https://matrix.to/#/\2">\2</a>)"));
|
||||||
|
stringIn.replace(TextRegex::fullUrl, QStringLiteral(R"(<a href="\1">\1</a>)"));
|
||||||
|
stringIn = stringIn.replace(TextRegex::emailAddress, QStringLiteral(R"(<a href="mailto:\2">\1\2</a>)"));
|
||||||
|
return stringIn;
|
||||||
|
}
|
||||||
133
src/texthandler.h
Normal file
133
src/texthandler.h
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QHash>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
|
#include "neochatroom.h"
|
||||||
|
|
||||||
|
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 strikethrough{QStringLiteral("<del>(.*?)</del>"), QRegularExpression::DotMatchesEverythingOption};
|
||||||
|
static const QRegularExpression mxcImage{QStringLiteral(R"AAA(<img(.*?)src="mxc:\/\/(.*?)\/(.*?)"(.*?)>)AAA")};
|
||||||
|
static const QRegularExpression fullUrl(
|
||||||
|
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 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class TextHandler
|
||||||
|
*
|
||||||
|
* This class is designed to handle the text of both incoming and outgoing messages.
|
||||||
|
*
|
||||||
|
* This includes converting markdown to html and removing any html tags that shouldn't
|
||||||
|
* be present as per the matrix spec
|
||||||
|
* (https://spec.matrix.org/v1.5/client-server-api/#mroommessage-msgtypes).
|
||||||
|
*/
|
||||||
|
class TextHandler
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief List of token types
|
||||||
|
*/
|
||||||
|
enum Type {
|
||||||
|
Text, /*!< Anything not a tag that doesn't have special handling */
|
||||||
|
Tag, /*!< For any generic tag that doesn't have special handling */
|
||||||
|
TextCode, /*!< Text between code tags */
|
||||||
|
End, /*!< End of the input string */
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the string being handled.
|
||||||
|
*
|
||||||
|
* Setting new data resets the TextHandler.
|
||||||
|
*/
|
||||||
|
QString data() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set the string being handled.
|
||||||
|
*
|
||||||
|
* @note The TextHandler doesn't modify the input data variable so the unhandled
|
||||||
|
* text can always be retrieved.
|
||||||
|
*/
|
||||||
|
void setData(const QString &string);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handle the text for a message that is being sent.
|
||||||
|
*/
|
||||||
|
QString handleSendText();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handle the text as a rich output for a message being received.
|
||||||
|
*
|
||||||
|
* The function does the following:
|
||||||
|
* - Removes invalid html tags and attributes
|
||||||
|
* - Strips any reply from the message
|
||||||
|
* - Formats user mentions
|
||||||
|
*
|
||||||
|
* @note In this case the rich text refers to the output format. The input
|
||||||
|
* can be in either and the parameter inputFormat just needs to be set
|
||||||
|
* appropriately.
|
||||||
|
*/
|
||||||
|
QString handleRecieveRichText(Qt::TextFormat inputFormat = Qt::RichText,
|
||||||
|
const NeoChatRoom *room = nullptr,
|
||||||
|
const Quotient::RoomEvent *event = nullptr,
|
||||||
|
bool stripNewlines = false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handle the text as a plain output for a message being received.
|
||||||
|
*
|
||||||
|
* The function does the following:
|
||||||
|
* - Removes all html tags and attributes (except inside of code tags)
|
||||||
|
* - Strips any reply from the message
|
||||||
|
*
|
||||||
|
* @note In this case the plain text refers to the output format. The input
|
||||||
|
* can be in either and the parameter inputFormat just needs to be set
|
||||||
|
* appropriately.
|
||||||
|
*
|
||||||
|
* @warning The output of this function should NEVER be input into a rich text
|
||||||
|
* control. It will try to preserve < and > in the plain string which
|
||||||
|
* could be malicious tags if the control uses rich text format.
|
||||||
|
*/
|
||||||
|
QString handleRecievePlainText(Qt::TextFormat inputFormat = Qt::PlainText, const bool &stripNewlines = false);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_data;
|
||||||
|
|
||||||
|
QString m_dataBuffer;
|
||||||
|
int m_pos;
|
||||||
|
Type m_nextTokenType;
|
||||||
|
QString m_nextToken;
|
||||||
|
|
||||||
|
void next();
|
||||||
|
void nextTokenType();
|
||||||
|
|
||||||
|
QString getTagType() const;
|
||||||
|
bool isCloseTag() const;
|
||||||
|
QString getAttributeType(const QString &string);
|
||||||
|
QString getAttributeData(const QString &string);
|
||||||
|
bool isAllowedTag(const QString &type);
|
||||||
|
bool isAllowedAttribute(const QString &tag, const QString &attribute);
|
||||||
|
bool isAllowedLink(const QString &link, bool isImg = false);
|
||||||
|
QString cleanAttributes(const QString &tag, const QString &tagString);
|
||||||
|
|
||||||
|
QString markdownToHTML(const QString &markdown);
|
||||||
|
QString escapeHtml(QString stringIn);
|
||||||
|
QString unescapeHtml(QString stringIn);
|
||||||
|
QString linkifyUrls(QString stringIn);
|
||||||
|
};
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2018 Black Hat <bhat@encom.eu.org>
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
|
||||||
|
|
||||||
#include "utils.h"
|
|
||||||
16
src/utils.h
16
src/utils.h
@@ -1,16 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2018 Black Hat <bhat@encom.eu.org>
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QRegularExpression>
|
|
||||||
|
|
||||||
namespace utils
|
|
||||||
{
|
|
||||||
static const QRegularExpression removeReplyRegex{"> <.*?>.*?\\n\\n", QRegularExpression::DotMatchesEverythingOption};
|
|
||||||
static const QRegularExpression removeRichReplyRegex{"<mx-reply>.*?</mx-reply>", QRegularExpression::DotMatchesEverythingOption};
|
|
||||||
static const QRegularExpression codePillRegExp{"<pre><code[^>]*>(.*?)</code></pre>", QRegularExpression::DotMatchesEverythingOption};
|
|
||||||
static const QRegularExpression userPillRegExp{"(<a href=\"https://matrix.to/#/@.*?:.*?\">.*?</a>)", QRegularExpression::DotMatchesEverythingOption};
|
|
||||||
static const QRegularExpression strikethroughRegExp{"<del>(.*?)</del>", QRegularExpression::DotMatchesEverythingOption};
|
|
||||||
static const QRegularExpression mxcImageRegExp{R"AAA(<img(.*?)src="mxc:\/\/(.*?)\/(.*?)"(.*?)>)AAA"};
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user