Compare commits
38 Commits
work/nico/
...
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",
|
||||
"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",
|
||||
"buildsystem": "cmake-ninja",
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
include:
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/reuse-lint.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android-qt6.yml
|
||||
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android-qt6.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux-qt6.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows-qt6.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd-qt6.yml
|
||||
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd-qt6.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/flatpak.yml
|
||||
|
||||
@@ -11,6 +11,7 @@ Dependencies:
|
||||
'frameworks/kconfig': '@stable'
|
||||
'frameworks/syntax-highlighting': '@stable'
|
||||
'frameworks/kitemmodels': '@stable'
|
||||
'frameworks/kquickcharts': '@stable'
|
||||
'frameworks/knotifications': '@stable'
|
||||
'libraries/kquickimageeditor': '@stable'
|
||||
'frameworks/sonnet': '@stable'
|
||||
@@ -38,6 +39,7 @@ Dependencies:
|
||||
'frameworks/kconfig': '@latest-kf6'
|
||||
'frameworks/syntax-highlighting': '@latest-kf6'
|
||||
'frameworks/kitemmodels': '@latest-kf6'
|
||||
'frameworks/kquickcharts': '@latest-kf6'
|
||||
'frameworks/knotifications': '@latest-kf6'
|
||||
'libraries/kquickimageeditor': '@latest-kf6'
|
||||
'frameworks/sonnet': '@latest-kf6'
|
||||
|
||||
@@ -8,7 +8,7 @@ cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
# KDE Applications version, managed by release script.
|
||||
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 "${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.kitemmodels 1.0)
|
||||
ecm_find_qmlmodule(org.kde.quickcharts 1.0)
|
||||
|
||||
find_package(KQuickImageEditor COMPONENTS)
|
||||
set_package_properties(KQuickImageEditor PROPERTIES
|
||||
@@ -130,13 +131,12 @@ set_package_properties(KF${QT_MAJOR_VERSION}DocTools PROPERTIES DESCRIPTION
|
||||
TYPE OPTIONAL
|
||||
)
|
||||
|
||||
find_package(Sqlite3)
|
||||
|
||||
if(NOT Quotient_VERSION_MINOR GREATER 6)
|
||||
cmake_policy(SET CMP0063 OLD)
|
||||
endif()
|
||||
|
||||
if(ANDROID)
|
||||
find_package(Sqlite3)
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/android/version.gradle.in ${CMAKE_BINARY_DIR}/version.gradle)
|
||||
endif()
|
||||
|
||||
|
||||
@@ -8,3 +8,9 @@ ecm_add_test(
|
||||
LINK_LIBRARIES neochat Qt::Test Quotient
|
||||
TEST_NAME neochatroomtest
|
||||
)
|
||||
|
||||
ecm_add_test(
|
||||
texthandlertest.cpp
|
||||
LINK_LIBRARIES neochat Qt::Test
|
||||
TEST_NAME texthandlertest
|
||||
)
|
||||
|
||||
@@ -136,7 +136,7 @@ void NeoChatRoomTest::initTestCase()
|
||||
void NeoChatRoomTest::subtitleTextTest()
|
||||
{
|
||||
QCOMPARE(room->timelineSize(), 1);
|
||||
QCOMPARE(room->subtitleText(), QStringLiteral("@example:example.org: This is an example text message"));
|
||||
QCOMPARE(room->lastEventToString(), QStringLiteral("@example:example.org: This is an example text message"));
|
||||
}
|
||||
|
||||
void NeoChatRoomTest::eventTest()
|
||||
|
||||
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/userdirectorylistmodel.cpp
|
||||
models/keywordnotificationrulemodel.cpp
|
||||
utils.cpp
|
||||
notificationsmanager.cpp
|
||||
models/sortfilterroomlistmodel.cpp
|
||||
chatdocumenthandler.cpp
|
||||
@@ -47,6 +46,8 @@ add_library(neochat STATIC
|
||||
models/statemodel.cpp
|
||||
filetransferpseudojob.cpp
|
||||
models/searchmodel.cpp
|
||||
texthandler.cpp
|
||||
models/locationsmodel.cpp
|
||||
)
|
||||
|
||||
add_executable(neochat-app
|
||||
@@ -175,6 +176,9 @@ if(ANDROID)
|
||||
"window-new"
|
||||
"globe"
|
||||
"visibility"
|
||||
"home"
|
||||
"preferences-desktop-notification"
|
||||
"computer-symbolic"
|
||||
)
|
||||
else()
|
||||
target_link_libraries(neochat PUBLIC Qt::Widgets KF${QT_MAJOR_VERSION}::KIOWidgets)
|
||||
|
||||
@@ -20,25 +20,10 @@
|
||||
#include "neochatroom.h"
|
||||
#include "neochatuser.h"
|
||||
#include "roommanager.h"
|
||||
#include "texthandler.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
QString markdownToHTML(const QString &markdown)
|
||||
{
|
||||
const auto str = markdown.toUtf8();
|
||||
char *tmp_buf = cmark_markdown_to_html(str.constData(), str.size(), CMARK_OPT_HARDBREAKS | CMARK_OPT_UNSAFE);
|
||||
|
||||
const std::string html(tmp_buf);
|
||||
|
||||
free(tmp_buf);
|
||||
|
||||
auto result = QString::fromStdString(html).trimmed();
|
||||
|
||||
result.replace("<!-- raw HTML omitted -->", "");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
ActionsHandler::ActionsHandler(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
@@ -169,7 +154,10 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, con
|
||||
}
|
||||
|
||||
handledText = CustomEmojiModel::instance().preprocessText(handledText);
|
||||
handledText = markdownToHTML(handledText);
|
||||
TextHandler textHandler;
|
||||
textHandler.setData(handledText);
|
||||
handledText = textHandler.handleSendText();
|
||||
|
||||
if (handledText.count("<p>") == 1 && handledText.count("</p>") == 1) {
|
||||
handledText.remove("<p>");
|
||||
handledText.remove("</p>");
|
||||
|
||||
@@ -50,5 +50,3 @@ private:
|
||||
QString handleMentions(QString handledText, const bool &isEdit = false);
|
||||
void handleMessage(const QString &text, QString handledText, const bool &isEdit = false);
|
||||
};
|
||||
|
||||
QString markdownToHTML(const QString &markdown);
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
#include "clipboard.h"
|
||||
#include "controller.h"
|
||||
#include "filetypesingleton.h"
|
||||
#include "joinrulesevent.h"
|
||||
#include "linkpreviewer.h"
|
||||
#include "login.h"
|
||||
#include "matriximageprovider.h"
|
||||
@@ -53,6 +52,7 @@
|
||||
#include "models/devicesmodel.h"
|
||||
#include "models/emojimodel.h"
|
||||
#include "models/keywordnotificationrulemodel.h"
|
||||
#include "models/locationsmodel.h"
|
||||
#include "models/messageeventmodel.h"
|
||||
#include "models/messagefiltermodel.h"
|
||||
#include "models/publicroomlistmodel.h"
|
||||
@@ -236,6 +236,7 @@ int main(int argc, char *argv[])
|
||||
qmlRegisterType<CompletionModel>("org.kde.neochat", 1, 0, "CompletionModel");
|
||||
qmlRegisterType<StateModel>("org.kde.neochat", 1, 0, "StateModel");
|
||||
qmlRegisterType<SearchModel>("org.kde.neochat", 1, 0, "SearchModel");
|
||||
qmlRegisterType<LocationsModel>("org.kde.neochat", 1, 0, "LocationsModel");
|
||||
#ifdef QUOTIENT_07
|
||||
qmlRegisterType<PollHandler>("org.kde.neochat", 1, 0, "PollHandler");
|
||||
#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 "neochatuser.h"
|
||||
#include "utils.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
@@ -54,6 +53,9 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
||||
roles[UserMarkerRole] = "userMarker";
|
||||
roles[ShowAuthorRole] = "showAuthor";
|
||||
roles[ShowSectionRole] = "showSection";
|
||||
roles[ReadMarkersRole] = "readMarkers";
|
||||
roles[ReadMarkersStringRole] = "readMarkersString";
|
||||
roles[ShowReadMarkersRole] = "showReadMarkers";
|
||||
roles[ReactionRole] = "reaction";
|
||||
roles[IsEditedRole] = "isEdited";
|
||||
roles[SourceRole] = "source";
|
||||
@@ -64,8 +66,6 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
||||
roles[VerifiedRole] = "verified";
|
||||
roles[DisplayNameForInitialsRole] = "displayNameForInitials";
|
||||
roles[AuthorDisplayNameRole] = "authorDisplayName";
|
||||
roles[IsNameChangeRole] = "isNameChange";
|
||||
roles[IsAvatarChangeRole] = "isAvatarChange";
|
||||
roles[IsRedactedRole] = "isRedacted";
|
||||
roles[GenericDisplayRole] = "genericDisplay";
|
||||
roles[IsPendingRole] = "isPending";
|
||||
@@ -216,6 +216,12 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
}
|
||||
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::fileTransferProgress, 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;
|
||||
case MessageEventType::Video:
|
||||
return DelegateType::Video;
|
||||
case MessageEventType::Location:
|
||||
return DelegateType::Location;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -558,6 +566,9 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
}
|
||||
|
||||
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
|
||||
if(e->msgtype() == Quotient::MessageEventType::Location) {
|
||||
return e->contentJson();
|
||||
}
|
||||
// Cannot use e.contentJson() here because some
|
||||
// EventContent classes inject values into the copy of the
|
||||
// content JSON stored in EventContent::Base
|
||||
@@ -599,12 +610,25 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
|
||||
if (role == SpecialMarksRole) {
|
||||
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();
|
||||
}
|
||||
|
||||
auto *memberEvent = timelineIt->viewAs<RoomMemberEvent>();
|
||||
if (memberEvent) {
|
||||
if ((memberEvent->isJoin() || memberEvent->isLeave()) && !NeoChatConfig::self()->showLeaveJoinEvent()) {
|
||||
if (evt.isStateEvent() && !NeoChatConfig::self()->showStateEvent()) {
|
||||
return EventStatus::Hidden;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -791,6 +815,65 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
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) {
|
||||
const auto &annotations = m_currentRoom->relatedEvents(evt, EventRelation::Annotation());
|
||||
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) {
|
||||
return evt.isRedacted();
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ public:
|
||||
Encrypted,
|
||||
ReadMarker,
|
||||
Poll,
|
||||
Location,
|
||||
Other,
|
||||
};
|
||||
Q_ENUM(DelegateType);
|
||||
@@ -57,6 +58,9 @@ public:
|
||||
ShowAuthorRole,
|
||||
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,
|
||||
|
||||
IsEditedRole,
|
||||
@@ -70,8 +74,6 @@ public:
|
||||
DisplayNameForInitialsRole,
|
||||
// The displayname for the event's sender; for name change events, the old displayname
|
||||
AuthorDisplayNameRole,
|
||||
IsNameChangeRole,
|
||||
IsAvatarChangeRole,
|
||||
IsRedactedRole,
|
||||
IsPendingRole,
|
||||
LastRole, // Keep this last
|
||||
|
||||
@@ -11,23 +11,20 @@ using namespace Quotient;
|
||||
MessageFilterModel::MessageFilterModel(QObject *parent)
|
||||
: QSortFilterProxyModel(parent)
|
||||
{
|
||||
connect(NeoChatConfig::self(), &NeoChatConfig::ShowStateEventChanged, this, [this] {
|
||||
invalidateFilter();
|
||||
});
|
||||
connect(NeoChatConfig::self(), &NeoChatConfig::ShowLeaveJoinEventChanged, this, [this] {
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
invalidateFilter();
|
||||
});
|
||||
|
||||
connect(NeoChatConfig::self(), &NeoChatConfig::ShowRenameChanged, this, [this] {
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
invalidateFilter();
|
||||
});
|
||||
|
||||
connect(NeoChatConfig::self(), &NeoChatConfig::ShowAvatarUpdateChanged, this, [this] {
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
invalidateFilter();
|
||||
});
|
||||
connect(NeoChatConfig::self(), &NeoChatConfig::ShowDeletedMessagesChanged, this, [this] {
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
invalidateFilter();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -35,18 +32,11 @@ bool MessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sour
|
||||
{
|
||||
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()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int specialMarks = index.data(MessageEventModel::SpecialMarksRole).toInt();
|
||||
if (specialMarks == EventStatus::Hidden || specialMarks == EventStatus::Replaced) {
|
||||
return false;
|
||||
}
|
||||
@@ -57,9 +47,5 @@ bool MessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sour
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!NeoChatConfig::self()->showLeaveJoinEvent() && eventType == MessageEventModel::State) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -180,28 +180,7 @@ void RoomListModel::connectRoomSignals(NeoChatRoom *room)
|
||||
#ifndef QUOTIENT_07
|
||||
connect(room, &Room::notificationCountChanged, this, &RoomListModel::handleNotifications);
|
||||
#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
|
||||
connect(room, &Room::notificationCountChanged, this, &RoomListModel::refreshNotificationCount);
|
||||
#else
|
||||
@@ -417,7 +396,7 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
|
||||
return m_categoryVisibility.value(data(index, CategoryRole).toInt(), true);
|
||||
}
|
||||
if (role == SubtitleTextRole) {
|
||||
return room->subtitleText();
|
||||
return room->lastEventToString(Qt::PlainText, true);
|
||||
}
|
||||
if (role == AvatarImageRole) {
|
||||
return room->avatar(128);
|
||||
|
||||
@@ -116,5 +116,4 @@ Q_SIGNALS:
|
||||
void notificationCountChanged();
|
||||
|
||||
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>
|
||||
<default>false</default>
|
||||
</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">
|
||||
<label>Use s/text/replacement syntax to edit your last message.</label>
|
||||
<default>false</default>
|
||||
@@ -72,6 +68,14 @@
|
||||
<label>Use a compact room list layout</label>
|
||||
<default>false</default>
|
||||
</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">
|
||||
<label>Show rename events in the timeline</label>
|
||||
<default>true</default>
|
||||
|
||||
@@ -13,9 +13,11 @@
|
||||
#include <QMediaMetaData>
|
||||
#include <QMediaPlayer>
|
||||
|
||||
#include <jobs/basejob.h>
|
||||
#include <qcoro/qcorosignal.h>
|
||||
|
||||
#include <connection.h>
|
||||
#include <csapi/account-data.h>
|
||||
#include <csapi/directory.h>
|
||||
#include <csapi/pushrules.h>
|
||||
#include <csapi/redaction.h>
|
||||
@@ -47,7 +49,7 @@
|
||||
#endif
|
||||
#include "filetransferpseudojob.h"
|
||||
#include "stickerevent.h"
|
||||
#include "utils.h"
|
||||
#include "texthandler.h"
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
#include <KIO/Job>
|
||||
@@ -98,6 +100,14 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
|
||||
Q_EMIT canEncryptRoomChanged();
|
||||
});
|
||||
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)
|
||||
@@ -205,7 +215,7 @@ void NeoChatRoom::sendTypingNotification(bool isTyping)
|
||||
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++) {
|
||||
const RoomEvent *event = timelineItem->get();
|
||||
@@ -217,8 +227,21 @@ const RoomEvent *NeoChatRoom::lastEvent(bool ignoreStateEvent) const
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event->isStateEvent()
|
||||
&& (ignoreStateEvent || !NeoChatConfig::self()->showLeaveJoinEvent() || static_cast<const StateEventBase &>(*event).repeatsState())) {
|
||||
if (event->isStateEvent() && !NeoChatConfig::self()->showStateEvent()) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -232,6 +255,14 @@ const RoomEvent *NeoChatRoom::lastEvent(bool ignoreStateEvent) const
|
||||
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)) {
|
||||
return lastEvent;
|
||||
}
|
||||
@@ -257,10 +288,11 @@ bool NeoChatRoom::lastEventIsSpoiler() const
|
||||
return false;
|
||||
}
|
||||
|
||||
QString NeoChatRoom::lastEventToString() const
|
||||
QString NeoChatRoom::lastEventToString(Qt::TextFormat format, bool stripNewlines) const
|
||||
{
|
||||
if (auto event = lastEvent()) {
|
||||
return roomMembername(event->senderId()) + (event->isStateEvent() ? " " : ": ") + eventToString(*event);
|
||||
return roomMembername(event->senderId()) + (event->isStateEvent() ? QLatin1String(" ") : QLatin1String(": "))
|
||||
+ eventToString(*event, format, stripNewlines);
|
||||
}
|
||||
return QLatin1String("");
|
||||
}
|
||||
@@ -321,7 +353,7 @@ QDateTime NeoChatRoom::lastActiveTime()
|
||||
return QDateTime();
|
||||
}
|
||||
|
||||
if (auto event = lastEvent(true)) {
|
||||
if (auto event = lastEvent()) {
|
||||
return event->originTimestamp();
|
||||
}
|
||||
|
||||
@@ -329,45 +361,6 @@ QDateTime NeoChatRoom::lastActiveTime()
|
||||
return messageEvents().rbegin()->get()->originTimestamp();
|
||||
}
|
||||
|
||||
QString NeoChatRoom::subtitleText()
|
||||
{
|
||||
static const QRegularExpression blockquote("(\r\n\t|\n|\r\t|)> ");
|
||||
static const QRegularExpression heading("(\r\n\t|\n|\r\t|)\\#{1,6} ");
|
||||
static const QRegularExpression newlines("(\r\n\t|\n|\r\t|\r\n)");
|
||||
static const QRegularExpression bold1("(\\*\\*|__)(?=\\S)([^\\r]*\\S)\\1");
|
||||
static const QRegularExpression bold2("(\\*|_)(?=\\S)([^\\r]*\\S)\\1");
|
||||
static const QRegularExpression strike1("~~(.*)~~");
|
||||
static const QRegularExpression strike2("~(.*)~");
|
||||
static const QRegularExpression del("<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
|
||||
{
|
||||
return firstDisplayedMarker() == historyEdge() ? 0 : int(firstDisplayedMarker() - messageEvents().rbegin());
|
||||
@@ -451,7 +444,7 @@ QString NeoChatRoom::avatarMediaId() const
|
||||
return {};
|
||||
}
|
||||
|
||||
QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format, bool removeReply) const
|
||||
QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format, bool stripNewlines) const
|
||||
{
|
||||
const bool prettyPrint = (format == Qt::RichText);
|
||||
|
||||
@@ -462,58 +455,48 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
|
||||
return visit(
|
||||
#endif
|
||||
evt,
|
||||
[this, prettyPrint, removeReply](const RoomMessageEvent &e) {
|
||||
[this, format, stripNewlines](const RoomMessageEvent &e) {
|
||||
using namespace MessageEventContent;
|
||||
|
||||
// 1. prettyPrint/HTML
|
||||
if (prettyPrint && e.hasTextContent() && e.mimeType().name() != "text/plain") {
|
||||
auto htmlBody = static_cast<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;
|
||||
}
|
||||
TextHandler textHandler;
|
||||
|
||||
if (e.hasFileContent()) {
|
||||
auto fileCaption = e.content()->fileInfo()->originalName.toHtmlEscaped();
|
||||
auto fileCaption = e.content()->fileInfo()->originalName;
|
||||
if (fileCaption.isEmpty()) {
|
||||
fileCaption = prettyPrint ? Quotient::prettyPrint(e.plainBody()) : e.plainBody();
|
||||
fileCaption = e.plainBody();
|
||||
} else if (e.content()->fileInfo()->originalName != e.plainBody()) {
|
||||
fileCaption = e.plainBody() + " | " + fileCaption;
|
||||
}
|
||||
return !fileCaption.isEmpty() ? fileCaption : i18n("a file");
|
||||
textHandler.setData(fileCaption);
|
||||
return !fileCaption.isEmpty() ? textHandler.handleRecievePlainText() : i18n("a file");
|
||||
}
|
||||
|
||||
// 2. prettyPrint/text 3. plainText/HTML 4. plainText/text
|
||||
QString plainBody;
|
||||
if (e.hasTextContent() && e.content() && e.mimeType().name() == "text/plain") { // 2/4
|
||||
plainBody = static_cast<const TextContent *>(e.content())->body;
|
||||
} else { // 3
|
||||
plainBody = e.plainBody();
|
||||
QString body;
|
||||
if (e.hasTextContent() && e.content()) {
|
||||
body = static_cast<const TextContent *>(e.content())->body;
|
||||
} else {
|
||||
body = e.plainBody();
|
||||
}
|
||||
|
||||
if (prettyPrint) {
|
||||
if (removeReply) {
|
||||
plainBody.remove(utils::removeReplyRegex);
|
||||
}
|
||||
return Quotient::prettyPrint(plainBody);
|
||||
textHandler.setData(body);
|
||||
|
||||
Qt::TextFormat inputFormat;
|
||||
if (e.mimeType().name() == "text/plain") {
|
||||
inputFormat = Qt::PlainText;
|
||||
} else {
|
||||
inputFormat = Qt::RichText;
|
||||
}
|
||||
if (removeReply) {
|
||||
return plainBody.remove(utils::removeReplyRegex);
|
||||
|
||||
if (format == Qt::RichText) {
|
||||
return textHandler.handleRecieveRichText(inputFormat, this, &e, stripNewlines);
|
||||
} else {
|
||||
return textHandler.handleRecievePlainText(inputFormat, stripNewlines);
|
||||
}
|
||||
return plainBody;
|
||||
},
|
||||
[](const StickerEvent &e) {
|
||||
return e.body();
|
||||
},
|
||||
[this](const RoomMemberEvent &e) {
|
||||
[this, prettyPrint](const RoomMemberEvent &e) {
|
||||
// FIXME: Rewind to the name that was at the time of this event
|
||||
auto subjectName = this->htmlSafeMemberName(e.userId());
|
||||
if (e.membership() == MembershipType::Leave) {
|
||||
@@ -526,8 +509,11 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
|
||||
#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
|
||||
switch (e.membership()) {
|
||||
@@ -985,7 +971,7 @@ bool NeoChatRoom::canSendState(const QString &eventType) const
|
||||
auto currentPl = plEvent->powerLevelForUser(localUser()->id());
|
||||
|
||||
#ifndef QUOTIENT_07
|
||||
if (eventType == "m.room.history_visibility") {
|
||||
if (eventType == "m.room.history_visibility" || eventType == "org.matrix.room.preview_urls") {
|
||||
return false;
|
||||
} else {
|
||||
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.
|
||||
}
|
||||
|
||||
int NeoChatRoom::getUserPowerLevel(const QString &userId) const
|
||||
bool NeoChatRoom::defaultUrlPreviewState() const
|
||||
{
|
||||
auto powerLevelEvent = getCurrentState<RoomPowerLevelsEvent>();
|
||||
return powerLevelEvent->powerLevelForUser(userId);
|
||||
#ifdef QUOTIENT_07
|
||||
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)
|
||||
@@ -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
|
||||
{
|
||||
#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 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
|
||||
Q_PROPERTY(int defaultUserPowerLevel READ defaultUserPowerLevel WRITE setDefaultUserPowerLevel NOTIFY defaultUserPowerLevelChanged)
|
||||
Q_PROPERTY(int invitePowerLevel READ invitePowerLevel WRITE setInvitePowerLevel NOTIFY invitePowerLevelChanged)
|
||||
@@ -118,13 +131,13 @@ public:
|
||||
/// This function respect the showLeaveJoinEvent setting and discard
|
||||
/// other not interesting events. This function can return an empty pointer
|
||||
/// 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.
|
||||
///
|
||||
/// \see lastEvent
|
||||
/// \see lastEventIsSpoiler
|
||||
[[nodiscard]] QString lastEventToString() const;
|
||||
[[nodiscard]] QString lastEventToString(Qt::TextFormat format = Qt::PlainText, bool stripNewlines = false) const;
|
||||
|
||||
/// Convenient way to check if the last event looks like it has spoilers.
|
||||
///
|
||||
@@ -137,12 +150,6 @@ public:
|
||||
/// \see lastEvent
|
||||
[[nodiscard]] QDateTime lastActiveTime();
|
||||
|
||||
/// Get subtitle text for room
|
||||
///
|
||||
/// Fetches last event and removes markdown formatting
|
||||
/// \see lastEventToString
|
||||
[[nodiscard]] QString subtitleText();
|
||||
|
||||
[[nodiscard]] bool isSpace();
|
||||
|
||||
bool isEventHighlighted(const Quotient::RoomEvent *e) const;
|
||||
@@ -153,6 +160,12 @@ public:
|
||||
[[nodiscard]] QString historyVisibility() const;
|
||||
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.
|
||||
*
|
||||
@@ -262,7 +275,7 @@ public:
|
||||
|
||||
[[nodiscard]] QString avatarMediaId() const;
|
||||
|
||||
[[nodiscard]] QString eventToString(const Quotient::RoomEvent &evt, Qt::TextFormat format = Qt::PlainText, bool removeReply = true) const;
|
||||
[[nodiscard]] QString eventToString(const Quotient::RoomEvent &evt, Qt::TextFormat format = Qt::PlainText, bool stripNewlines = false) const;
|
||||
[[nodiscard]] QString eventToGenericString(const Quotient::RoomEvent &evt) const;
|
||||
|
||||
Q_INVOKABLE [[nodiscard]] bool containsUser(const QString &userID) const;
|
||||
@@ -395,6 +408,8 @@ Q_SIGNALS:
|
||||
void canEncryptRoomChanged();
|
||||
void joinRuleChanged();
|
||||
void historyVisibilityChanged();
|
||||
void defaultUrlPreviewStateChanged();
|
||||
void urlPreviewEnabledChanged();
|
||||
void maxRoomVersionChanged();
|
||||
void defaultUserPowerLevelChanged();
|
||||
void invitePowerLevelChanged();
|
||||
|
||||
@@ -22,11 +22,11 @@
|
||||
#include <jobs/basejob.h>
|
||||
#include <user.h>
|
||||
|
||||
#include "actionshandler.h"
|
||||
#include "controller.h"
|
||||
#include "neochatconfig.h"
|
||||
#include "neochatroom.h"
|
||||
#include "roommanager.h"
|
||||
#include "texthandler.h"
|
||||
#include "windowcontroller.h"
|
||||
|
||||
using namespace Quotient;
|
||||
@@ -85,7 +85,9 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
|
||||
std::unique_ptr<KNotificationReplyAction> replyAction(new KNotificationReplyAction(i18n("Reply")));
|
||||
replyAction->setPlaceholderText(i18n("Reply..."));
|
||||
connect(replyAction.get(), &KNotificationReplyAction::replied, this, [room, replyEventId](const QString &text) {
|
||||
room->postMessage(text, markdownToHTML(text), RoomMessageEvent::MsgType::Text, replyEventId, QString());
|
||||
TextHandler textHandler;
|
||||
textHandler.setData(text);
|
||||
room->postMessage(text, textHandler.handleSendText(), RoomMessageEvent::MsgType::Text, replyEventId, QString());
|
||||
});
|
||||
notification->setReplyAction(std::move(replyAction));
|
||||
}
|
||||
|
||||
@@ -139,11 +139,41 @@ QQC2.Control {
|
||||
currentRoom.chatBoxText = text
|
||||
}
|
||||
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: {
|
||||
if (completionMenu.visible) {
|
||||
completionMenu.complete()
|
||||
} else if (event.modifiers & Qt.ShiftModifier) {
|
||||
} else if (event.modifiers & Qt.ShiftModifier || Kirigami.Settings.isMobile) {
|
||||
textField.insert(cursorPosition, "\n")
|
||||
} else {
|
||||
chatBar.postMessage();
|
||||
@@ -152,7 +182,7 @@ QQC2.Control {
|
||||
Keys.onReturnPressed: {
|
||||
if (completionMenu.visible) {
|
||||
completionMenu.complete()
|
||||
} else if (event.modifiers & Qt.ShiftModifier) {
|
||||
} else if (event.modifiers & Qt.ShiftModifier || Kirigami.Settings.isMobile) {
|
||||
textField.insert(cursorPosition, "\n")
|
||||
} else {
|
||||
chatBar.postMessage();
|
||||
@@ -180,9 +210,14 @@ QQC2.Control {
|
||||
completionMenu.decrementIndex()
|
||||
} else if (event.key === Qt.Key_Down && completionMenu.visible) {
|
||||
completionMenu.incrementIndex()
|
||||
} else if (event.key === Qt.Key_Backspace && textField.text.length <= 1) {
|
||||
currentRoom.sendTypingNotification(false)
|
||||
repeatTimer.stop()
|
||||
} else if (event.key === Qt.Key_Backspace) {
|
||||
if (textField.text == selectedText || textField.text.length <= 1) {
|
||||
currentRoom.sendTypingNotification(false)
|
||||
repeatTimer.stop()
|
||||
}
|
||||
if (quickFormatBar.visible && selectedText.length > 0) {
|
||||
quickFormatBar.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
Keys.onShortcutOverride: {
|
||||
@@ -313,14 +348,9 @@ QQC2.Control {
|
||||
QQC2.ToolTip.text: modelData.tooltip
|
||||
HoverHandler { id: hoverHandler }
|
||||
|
||||
QQC2.BusyIndicator {
|
||||
anchors.fill: parent
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
visible: running
|
||||
running: modelData.isBusy
|
||||
PieProgressBar {
|
||||
visible: modelData.isBusy
|
||||
progress: currentRoom.fileUploadingProgress
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -398,4 +428,47 @@ QQC2.Control {
|
||||
currentRoom.chatBoxReplyId = "";
|
||||
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 {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageEventModel.Location
|
||||
delegate: LocationDelegate {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageEventModel.Other
|
||||
delegate: Item {}
|
||||
|
||||
@@ -49,7 +49,7 @@ Loader {
|
||||
*/
|
||||
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
|
||||
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 isReplyLabel: false
|
||||
|
||||
readonly property var linkRegex: /(href=["'])?(\b(https?):\/\/[^\s\<\>\"\'\\\?\:\)\(]+(\(.*?\))*(\?(?=[a-z])[^\s\\\)]+|$)?)/g
|
||||
property string textMessage: model.display.includes("http")
|
||||
? model.display.replace(linkRegex, function() {
|
||||
if (arguments[0].includes("/_matrix/media/r0/download/")) {
|
||||
return arguments[0];
|
||||
}
|
||||
if (arguments[1]) {
|
||||
return arguments[0];
|
||||
}
|
||||
const l = arguments[2];
|
||||
if ([".", ","].includes(l[l.length-1])) {
|
||||
const link = l.substring(0, l.length-1);
|
||||
const leftover = l[l.length-1];
|
||||
return `<a href="${link}">${link}</a>${leftover}`;
|
||||
}
|
||||
return `<a href="${l}">${l}</a>`;
|
||||
})
|
||||
: model.display
|
||||
property string textMessage: model.display
|
||||
property bool spoilerRevealed: !hasSpoiler.test(textMessage)
|
||||
|
||||
ListView.onReused: Qt.binding(() => !hasSpoiler.test(textMessage))
|
||||
|
||||
@@ -134,5 +134,12 @@ QQC2.Control {
|
||||
folded = !folded
|
||||
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
|
||||
}
|
||||
AvatarFlow {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||
visible: showReadMarkers
|
||||
model: readMarkers
|
||||
toolTipText: readMarkersString
|
||||
}
|
||||
|
||||
function isVisibleInTimeline() {
|
||||
let yoff = Math.round(y - ListView.view.contentY);
|
||||
|
||||
@@ -144,12 +144,17 @@ QQC2.ToolBar {
|
||||
actions.main: Kirigami.Action {
|
||||
text: i18n("Edit this account")
|
||||
icon.name: "document-edit"
|
||||
onTriggered: pageStack.pushDialogLayer(Qt.resolvedUrl('./AccountEditorPage.qml'), {
|
||||
onTriggered: pageStack.pushDialogLayer(Qt.resolvedUrl('qrc:/AccountEditorPage.qml'), {
|
||||
connection: Controller.activeConnection
|
||||
}, {
|
||||
title: i18n("Account editor")
|
||||
});
|
||||
}
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
acceptedDevices: PointerDevice.Mouse
|
||||
onTapped: accountMenu.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
ColumnLayout {
|
||||
@@ -214,6 +219,11 @@ QQC2.ToolBar {
|
||||
Item {
|
||||
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 = "";
|
||||
messageListView.positionViewAtBeginning();
|
||||
hasScrolledUpBefore = false;
|
||||
chatBox.chatBar.forceActiveFocus();
|
||||
if (!Kirigami.Settings.isMobile) {
|
||||
chatBox.chatBar.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
@@ -353,7 +355,9 @@ Kirigami.ScrollablePage {
|
||||
visible: currentRoom && currentRoom.hasUnreadMessages && currentRoom.readMarkerLoaded
|
||||
action: Kirigami.Action {
|
||||
onTriggered: {
|
||||
chatBox.chatBar.forceActiveFocus();
|
||||
if (!Kirigami.Settings.isMobile) {
|
||||
chatBox.chatBar.forceActiveFocus();
|
||||
}
|
||||
messageListView.goToEvent(currentRoom.readMarkerEventId)
|
||||
}
|
||||
icon.name: "go-up"
|
||||
@@ -378,7 +382,9 @@ Kirigami.ScrollablePage {
|
||||
visible: !messageListView.atYEnd
|
||||
action: Kirigami.Action {
|
||||
onTriggered: {
|
||||
chatBox.chatBar.forceActiveFocus();
|
||||
if (!Kirigami.Settings.isMobile) {
|
||||
chatBox.chatBar.forceActiveFocus();
|
||||
}
|
||||
goToLastMessage();
|
||||
currentRoom.markAllMessagesAsRead();
|
||||
}
|
||||
@@ -531,7 +537,9 @@ Kirigami.ScrollablePage {
|
||||
showQuickReaction: true
|
||||
onChosen: {
|
||||
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 {
|
||||
id: favouriteButton
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ Kirigami.CategorizedSettings {
|
||||
objectName: "settingsPage"
|
||||
actions: [
|
||||
Kirigami.SettingAction {
|
||||
actionName: "general"
|
||||
text: i18n("General")
|
||||
icon.name: "settings-configure"
|
||||
page: Qt.resolvedUrl("General.qml")
|
||||
@@ -22,6 +23,7 @@ Kirigami.CategorizedSettings {
|
||||
}
|
||||
},
|
||||
Kirigami.SettingAction {
|
||||
actionName: "security"
|
||||
text: i18n("Security")
|
||||
icon.name: "security-low"
|
||||
page: Qt.resolvedUrl("Security.qml")
|
||||
@@ -32,6 +34,7 @@ Kirigami.CategorizedSettings {
|
||||
}
|
||||
},
|
||||
Kirigami.SettingAction {
|
||||
actionName: "permissions"
|
||||
text: i18n("Permissions")
|
||||
icon.name: "visibility"
|
||||
page: Qt.resolvedUrl("Permissions.qml")
|
||||
@@ -42,6 +45,7 @@ Kirigami.CategorizedSettings {
|
||||
}
|
||||
},
|
||||
Kirigami.SettingAction {
|
||||
actionName: "notifications"
|
||||
text: i18n("Notifications")
|
||||
icon.name: "notifications"
|
||||
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 {
|
||||
Layout.fillWidth: true
|
||||
|
||||
@@ -77,8 +77,38 @@ Kirigami.ScrollablePage {
|
||||
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 {
|
||||
id: showLeaveJoinEventDelegate
|
||||
visible: Config.showStateEvent
|
||||
text: i18n("Show leave and join events")
|
||||
checked: Config.showLeaveJoinEvent
|
||||
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 {
|
||||
id: showNameDelegate
|
||||
visible: Config.showStateEvent
|
||||
text: i18n("Show name change events")
|
||||
checked: Config.showRename
|
||||
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 {
|
||||
id: showAvatarChangeDelegate
|
||||
visible: Config.showStateEvent
|
||||
text: i18n("Show avatar update events")
|
||||
checked: Config.showAvatarUpdate
|
||||
enabled: !Config.isShowAvatarUpdateImmutable
|
||||
@@ -113,19 +153,6 @@ Kirigami.ScrollablePage {
|
||||
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"
|
||||
actions: [
|
||||
Kirigami.SettingAction {
|
||||
actionName: "general"
|
||||
text: i18n("General")
|
||||
icon.name: "org.kde.neochat"
|
||||
page: Qt.resolvedUrl("GeneralSettingsPage.qml")
|
||||
},
|
||||
Kirigami.SettingAction {
|
||||
actionName: "appearance"
|
||||
text: i18n("Appearance")
|
||||
icon.name: "preferences-desktop-theme-global"
|
||||
page: Qt.resolvedUrl("AppearanceSettingsPage.qml")
|
||||
},
|
||||
Kirigami.SettingAction {
|
||||
actionName: "notifications"
|
||||
text: i18n("Notifications")
|
||||
icon.name: "preferences-desktop-notification"
|
||||
page: Qt.resolvedUrl("GlobalNotificationsPage.qml")
|
||||
},
|
||||
Kirigami.SettingAction {
|
||||
actionName: "accounts"
|
||||
text: i18n("Accounts")
|
||||
icon.name: "preferences-system-users"
|
||||
page: Qt.resolvedUrl("AccountsPage.qml")
|
||||
},
|
||||
Kirigami.SettingAction {
|
||||
actionName: "customEmojis"
|
||||
text: i18n("Custom Emojis")
|
||||
icon.name: "preferences-desktop-emoticons"
|
||||
page: Qt.resolvedUrl("Emoticons.qml")
|
||||
},
|
||||
Kirigami.SettingAction {
|
||||
actionName: "spellChecking"
|
||||
text: i18n("Spell Checking")
|
||||
icon.name: "tools-check-spelling"
|
||||
page: Qt.resolvedUrl("SonnetConfigPage.qml")
|
||||
visible: Qt.platform.os !== "android"
|
||||
},
|
||||
Kirigami.SettingAction {
|
||||
actionName: "networkProxy"
|
||||
text: i18n("Network Proxy")
|
||||
icon.name: "network-connect"
|
||||
page: Qt.resolvedUrl("NetworkProxyPage.qml")
|
||||
},
|
||||
Kirigami.SettingAction {
|
||||
actionName: "devices"
|
||||
text: i18n("Devices")
|
||||
icon.name: "computer"
|
||||
page: Qt.resolvedUrl("DevicesPage.qml")
|
||||
},
|
||||
Kirigami.SettingAction {
|
||||
actionName: "aboutNeochat"
|
||||
text: i18n("About NeoChat")
|
||||
icon.name: "help-about"
|
||||
page: Qt.resolvedUrl("About.qml")
|
||||
},
|
||||
Kirigami.SettingAction {
|
||||
actionName: "aboutKDE"
|
||||
text: i18n("About KDE")
|
||||
icon.name: "kde"
|
||||
page: Qt.resolvedUrl("AboutKDE.qml")
|
||||
|
||||
@@ -165,7 +165,7 @@ Kirigami.ApplicationWindow {
|
||||
|
||||
pageStack.defaultColumnWidth: roomListPage ? roomListPage.currentWidth : 0
|
||||
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 {
|
||||
id: confirmLogoutDialog
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
<file alias="AttachmentPane.qml">qml/Component/ChatBox/AttachmentPane.qml</file>
|
||||
<file alias="ReplyPane.qml">qml/Component/ChatBox/ReplyPane.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="ReplyComponent.qml">qml/Component/Timeline/ReplyComponent.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="StateComponent.qml">qml/Component/Timeline/StateComponent.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="Login.qml">qml/Component/Login/Login.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="GlobalMenu.qml">qml/Menu/GlobalMenu.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="FileDelegateContextMenu.qml">qml/Menu/Timeline/FileDelegateContextMenu.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="EmojiGrid.qml">qml/Component/Emoji/EmojiGrid.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>
|
||||
</RCC>
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
|
||||
#include "spacehierarchycache.h"
|
||||
|
||||
#include "controller.h"
|
||||
#ifdef QUOTIENT_07
|
||||
#include <csapi/space_hierarchy.h>
|
||||
#endif
|
||||
#include <qt_connection_util.h>
|
||||
|
||||
#include "controller.h"
|
||||
#include "neochatroom.h"
|
||||
|
||||
using namespace Quotient;
|
||||
@@ -17,6 +19,8 @@ SpaceHierarchyCache::SpaceHierarchyCache(QObject *parent)
|
||||
cacheSpaceHierarchy();
|
||||
connect(&Controller::instance(), &Controller::activeConnectionChanged, this, [this]() {
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (updateCache) {
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
namespace Quotient
|
||||
{
|
||||
class Room;
|
||||
}
|
||||
|
||||
class SpaceHierarchyCache : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -24,6 +29,10 @@ public:
|
||||
Q_SIGNALS:
|
||||
void spaceHierarchyChanged();
|
||||
|
||||
private Q_SLOTS:
|
||||
void addSpaceToHierarchy(Quotient::Room *room);
|
||||
void removeSpaceFromHierarchy(Quotient::Room *room);
|
||||
|
||||
private:
|
||||
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