Compare commits
208 Commits
work/tobia
...
work/jz/fl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae155805e9 | ||
|
|
70bff21632 | ||
|
|
f58c390a47 | ||
|
|
089a9abcb4 | ||
|
|
bf1c76d0a6 | ||
|
|
879da627b1 | ||
|
|
9b93eb44d5 | ||
|
|
b30220eca9 | ||
|
|
d270d4e5e1 | ||
|
|
21da6cb0f4 | ||
|
|
6ac75df935 | ||
|
|
f29781349c | ||
|
|
bb776d5c2b | ||
|
|
6cfab9e3ea | ||
|
|
6373186c15 | ||
|
|
e342de3bc1 | ||
|
|
4cd7b69ea5 | ||
|
|
988e8529da | ||
|
|
6a32d1e961 | ||
|
|
0552c798fb | ||
|
|
a53ad41879 | ||
|
|
92351edcd0 | ||
|
|
878eb48cb0 | ||
|
|
053ca6bed8 | ||
|
|
78ae14ab2f | ||
|
|
5fdc2ad765 | ||
|
|
b75dbe8d5c | ||
|
|
eaf4663c84 | ||
|
|
64b8cd5bcc | ||
|
|
482d61ee47 | ||
|
|
276dcce95e | ||
|
|
217f9e2e02 | ||
|
|
f40a0a6f5f | ||
|
|
9bd67acc2f | ||
|
|
e87da0feb0 | ||
|
|
2608d879fa | ||
|
|
6ab61fd41f | ||
|
|
30dd6297ee | ||
|
|
ce02183f82 | ||
|
|
7c74a6cbe1 | ||
|
|
e6a11b2ad8 | ||
|
|
158942d1b5 | ||
|
|
aaa97ec029 | ||
|
|
882ead5715 | ||
|
|
ab4519dedd | ||
|
|
c3fd2428a2 | ||
|
|
fbb4b962fa | ||
|
|
9bf65de649 | ||
|
|
75f069cb7d | ||
|
|
87d50125ab | ||
|
|
dc2cf21cb8 | ||
|
|
bae14ecd35 | ||
|
|
b48c1c3b80 | ||
|
|
0f9eb4beeb | ||
|
|
0ab8624d79 | ||
|
|
e872c934c3 | ||
|
|
c3d5d18aae | ||
|
|
ff5853a850 | ||
|
|
f772906324 | ||
|
|
07eabb2dc1 | ||
|
|
b3c88763a4 | ||
|
|
f7081f8829 | ||
|
|
ceef2167fd | ||
|
|
1dcfd94328 | ||
|
|
a1aa0804e2 | ||
|
|
77176478eb | ||
|
|
bf4ebfa7a8 | ||
|
|
6e7d622b41 | ||
|
|
8398b7d24d | ||
|
|
aef9b7375a | ||
|
|
ba45318b56 | ||
|
|
7d4f8780ad | ||
|
|
b504c990f8 | ||
|
|
e07b876677 | ||
|
|
a0bfd34951 | ||
|
|
b173714bbe | ||
|
|
db4021b601 | ||
|
|
2f46fd1d2c | ||
|
|
1f85f848e2 | ||
|
|
d7e0954e86 | ||
|
|
1671e05d12 | ||
|
|
33c55d1563 | ||
|
|
4984181613 | ||
|
|
af75136269 | ||
|
|
c8eb75a148 | ||
|
|
5109b4fcd1 | ||
|
|
1b7f482d0b | ||
|
|
6f9a273d39 | ||
|
|
51d354a9c8 | ||
|
|
40b2b9554b | ||
|
|
ba1aca84ff | ||
|
|
17688a49d5 | ||
|
|
5ff199cc3e | ||
|
|
81a79105d7 | ||
|
|
e39760ccfb | ||
|
|
1c43da2532 | ||
|
|
2846def00f | ||
|
|
e2eb6ab33c | ||
|
|
35b08d085c | ||
|
|
064b0581a7 | ||
|
|
0cc38aa69a | ||
|
|
8312483659 | ||
|
|
0ceb0b4421 | ||
|
|
cc373365fb | ||
|
|
75d9b6e2a1 | ||
|
|
78fa38ba68 | ||
|
|
0b28712a34 | ||
|
|
48937c8d9a | ||
|
|
8c966a5e1a | ||
|
|
f49dd371b7 | ||
|
|
6947fbc12a | ||
|
|
550dc43dc0 | ||
|
|
23c9a4fea7 | ||
|
|
7d26f3351f | ||
|
|
b546554fef | ||
|
|
cc058a7cd3 | ||
|
|
7654b83339 | ||
|
|
93426546ad | ||
|
|
23bc38ca6c | ||
|
|
5ccce364d3 | ||
|
|
b488b55a71 | ||
|
|
0bace17074 | ||
|
|
ad6c7dbd1f | ||
|
|
2a6e63595e | ||
|
|
4d62ad1938 | ||
|
|
e7c3a24011 | ||
|
|
37468607fe | ||
|
|
5b007129e3 | ||
|
|
93ceb4d49c | ||
|
|
85b806fcba | ||
|
|
20596aabb8 | ||
|
|
825108c59e | ||
|
|
09c31b20e6 | ||
|
|
78271a3738 | ||
|
|
e029aaadfc | ||
|
|
f6efa35ed2 | ||
|
|
728bad00b4 | ||
|
|
97f3013f7a | ||
|
|
269a832ac9 | ||
|
|
3b5b7af531 | ||
|
|
76e7eb7009 | ||
|
|
fa99b8829d | ||
|
|
98b749ee9f | ||
|
|
cb9fd02dc7 | ||
|
|
1f4a271dd6 | ||
|
|
bdf192df58 | ||
|
|
1249304907 | ||
|
|
10e3ab1f78 | ||
|
|
59ea270b2f | ||
|
|
3b748e2e21 | ||
|
|
432fae5ce2 | ||
|
|
f557ceda19 | ||
|
|
943f6c762c | ||
|
|
5a9293e293 | ||
|
|
a1bc52c1a5 | ||
|
|
dcd752be8f | ||
|
|
deb2c61b28 | ||
|
|
5e346283a9 | ||
|
|
dc1e4219a0 | ||
|
|
a916f70698 | ||
|
|
77da4d2d53 | ||
|
|
1ae3fc86da | ||
|
|
0c24996b44 | ||
|
|
1cbb4a93eb | ||
|
|
1fac6ca34a | ||
|
|
ca439b0f86 | ||
|
|
f70febc8d3 | ||
|
|
628de56087 | ||
|
|
bc67033c00 | ||
|
|
e0ba2a179c | ||
|
|
e0ce5d9544 | ||
|
|
38acfe04b9 | ||
|
|
5b0068d9e4 | ||
|
|
38edad2ac5 | ||
|
|
e95f191dc6 | ||
|
|
8ae92ff4d4 | ||
|
|
40e15d456a | ||
|
|
a1b3308cd7 | ||
|
|
48ef19e4be | ||
|
|
d3f00b9246 | ||
|
|
89de676982 | ||
|
|
6dec624d22 | ||
|
|
d4eb9ea320 | ||
|
|
1cfaa88989 | ||
|
|
82ae0891a8 | ||
|
|
58c43b0cd4 | ||
|
|
1c373031bd | ||
|
|
e93842a95f | ||
|
|
9cdb34cb1d | ||
|
|
864f9b8f74 | ||
|
|
d99fae333a | ||
|
|
b27bec3e16 | ||
|
|
5fb4838734 | ||
|
|
1296255a86 | ||
|
|
e3ed2fcaa9 | ||
|
|
69d6b90a12 | ||
|
|
6937a4b2fe | ||
|
|
621c36f821 | ||
|
|
940929d044 | ||
|
|
bac5fa8c14 | ||
|
|
a10da64378 | ||
|
|
f2c12c582e | ||
|
|
a686c44555 | ||
|
|
8e96217ae6 | ||
|
|
ec906813fc | ||
|
|
36543d0872 | ||
|
|
fc6ea0b779 | ||
|
|
dae23ccd4b |
@@ -3,5 +3,4 @@
|
||||
|
||||
[BlueprintSettings]
|
||||
kde/frameworks/extra-cmake-modules.version=master
|
||||
kde/unreleased/kirigami-addons.version=master
|
||||
libs/qt.qtMajorVersion=6
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"id": "org.kde.neochat",
|
||||
"branch": "master",
|
||||
"runtime": "org.kde.Platform",
|
||||
"runtime-version": "6.6-kf6preview",
|
||||
"runtime-version": "6.7",
|
||||
"sdk": "org.kde.Sdk",
|
||||
"command": "neochat",
|
||||
"tags": [
|
||||
@@ -27,7 +27,7 @@
|
||||
"name": "kirigamiaddons",
|
||||
"config-opts": [ "-DBUILD_TESTING=OFF" ],
|
||||
"buildsystem": "cmake-ninja",
|
||||
"sources": [ { "type": "git", "url": "https://invent.kde.org/libraries/kirigami-addons.git" } ]
|
||||
"sources": [ { "type": "git", "url": "https://invent.kde.org/libraries/kirigami-addons.git", "commit": "34d311219e8b7209746a98b3a29b91ded05ff936" } ]
|
||||
},
|
||||
{
|
||||
"name": "kquickimageeditor",
|
||||
|
||||
@@ -31,6 +31,7 @@ Dependencies:
|
||||
- 'on': ['Linux', 'FreeBSD']
|
||||
'require':
|
||||
'frameworks/kdbusaddons': '@latest-kf6'
|
||||
'frameworks/purpose': '@latest-kf6'
|
||||
|
||||
- 'on': ['Linux']
|
||||
'require':
|
||||
|
||||
@@ -49,3 +49,7 @@ License: CC0-1.0
|
||||
Files: appiumtests/data/*
|
||||
Copyright: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
License: CC0-1.0
|
||||
|
||||
Files: src/purpose/purposeplugin.json
|
||||
Copyright: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
License: BSD-2-Clause
|
||||
|
||||
@@ -14,7 +14,7 @@ set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_
|
||||
|
||||
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
|
||||
|
||||
set(KF_MIN_VERSION "5.240.0")
|
||||
set(KF_MIN_VERSION "6.0")
|
||||
set(QT_MIN_VERSION "6.5")
|
||||
|
||||
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)
|
||||
@@ -24,7 +24,7 @@ set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
set(KDE_COMPILERSETTINGS_LEVEL 5.105)
|
||||
set(KDE_COMPILERSETTINGS_LEVEL 6.0)
|
||||
|
||||
include(FeatureSummary)
|
||||
include(ECMSetupVersion)
|
||||
@@ -58,6 +58,9 @@ set_package_properties(Qt6 PROPERTIES
|
||||
TYPE REQUIRED
|
||||
PURPOSE "Basic application components"
|
||||
)
|
||||
|
||||
qt_policy(SET QTP0001 NEW)
|
||||
|
||||
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami I18n Notifications Config CoreAddons Sonnet ItemModels ColorScheme)
|
||||
set_package_properties(KF6 PROPERTIES
|
||||
TYPE REQUIRED
|
||||
@@ -69,6 +72,10 @@ set_package_properties(KF6Kirigami PROPERTIES
|
||||
)
|
||||
find_package(KF6KirigamiAddons 0.7.2 REQUIRED)
|
||||
|
||||
if (UNIX AND NOT APPLE AND NOT ANDROID AND NOT NEOCHAT_FLATPAK AND NOT NEOCHAT_APPIMAGE)
|
||||
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS Purpose)
|
||||
endif ()
|
||||
|
||||
if(ANDROID)
|
||||
find_package(OpenSSL)
|
||||
set_package_properties(OpenSSL PROPERTIES
|
||||
@@ -78,6 +85,7 @@ if(ANDROID)
|
||||
else()
|
||||
find_package(Qt6 ${QT_MIN_VERSION} COMPONENTS Widgets)
|
||||
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle KIO WindowSystem StatusNotifierItem)
|
||||
find_package(KF6SyntaxHighlighting ${KF_MIN_VERSION} REQUIRED)
|
||||
set_package_properties(KF6QQC2DesktopStyle PROPERTIES
|
||||
TYPE RUNTIME
|
||||
)
|
||||
|
||||
@@ -55,6 +55,7 @@ private Q_SLOTS:
|
||||
void genericBody_data();
|
||||
void genericBody();
|
||||
void nullGenericBody();
|
||||
void markdownBody();
|
||||
void subtitle();
|
||||
void nullSubtitle();
|
||||
void mediaInfo();
|
||||
@@ -293,6 +294,13 @@ void EventHandlerTest::nullGenericBody()
|
||||
QCOMPARE(noEventHandler.getGenericBody(), QString());
|
||||
}
|
||||
|
||||
void EventHandlerTest::markdownBody()
|
||||
{
|
||||
EventHandler eventHandler(room, room->messageEvents().at(0).get());
|
||||
|
||||
QCOMPARE(eventHandler.getMarkdownBody(), QStringLiteral("This is an example\ntext message"));
|
||||
}
|
||||
|
||||
void EventHandlerTest::subtitle()
|
||||
{
|
||||
EventHandler eventHandler(room, room->messageEvents().at(0).get());
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
#include <Quotient/syncdata.h>
|
||||
#include <qnamespace.h>
|
||||
|
||||
#include "enums/messagecomponenttype.h"
|
||||
#include "models/customemojimodel.h"
|
||||
#include "models/messagecontentmodel.h"
|
||||
#include "neochatconnection.h"
|
||||
#include "utils.h"
|
||||
|
||||
@@ -33,7 +35,6 @@ private Q_SLOTS:
|
||||
void stripDisallowedTags();
|
||||
void stripDisallowedAttributes();
|
||||
void emptyCodeTags();
|
||||
void formatBlockQuote();
|
||||
|
||||
void sendSimpleStringCase();
|
||||
void sendSingleParaMarkup();
|
||||
@@ -59,11 +60,13 @@ private Q_SLOTS:
|
||||
void receiveRichtextIn();
|
||||
void receiveRichMxcUrl();
|
||||
void receiveRichPlainUrl();
|
||||
void receiveRichEmote();
|
||||
void receiveRichEdited_data();
|
||||
void receiveRichEdited();
|
||||
void receiveLineSeparator();
|
||||
void receiveRichCodeUrl();
|
||||
|
||||
void componentOutput_data();
|
||||
void componentOutput();
|
||||
};
|
||||
|
||||
void TextHandlerTest::initTestCase()
|
||||
@@ -139,16 +142,6 @@ void TextHandlerTest::emptyCodeTags()
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
|
||||
}
|
||||
|
||||
void TextHandlerTest::formatBlockQuote()
|
||||
{
|
||||
auto input = QStringLiteral("<blockquote>\n<p>Lorem Ispum</p>\n</blockquote>");
|
||||
auto expectedOutput = QStringLiteral("<blockquote><table><tr><td>\u201CLorem Ispum\u201D</td></tr></table></blockquote>");
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(input);
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(), expectedOutput);
|
||||
}
|
||||
|
||||
void TextHandlerTest::sendSimpleStringCase()
|
||||
{
|
||||
const QString testInputString = QStringLiteral("This data should just be put in a paragraph.");
|
||||
@@ -470,22 +463,6 @@ void TextHandlerTest::receiveRichPlainUrl()
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringMxId);
|
||||
}
|
||||
|
||||
// Test that user pill is add to an emote message.
|
||||
// N.B. The second message in the test timeline is marked as an emote.
|
||||
void TextHandlerTest::receiveRichEmote()
|
||||
{
|
||||
auto event = room->messageEvents().at(1).get();
|
||||
auto author = room->user(event->senderId());
|
||||
const QString testInputString = QStringLiteral("This is an emote.");
|
||||
const QString testOutputString = QStringLiteral("* <a href=\"https://matrix.to/#/@example:example.org\" style=\"color:")
|
||||
+ Utils::getUserColor(author->hueF()).name() + QStringLiteral("\">@example:example.org</a> This is an emote.");
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, event), testOutputString);
|
||||
}
|
||||
|
||||
void TextHandlerTest::receiveRichEdited_data()
|
||||
{
|
||||
QTest::addColumn<QString>("testInputString");
|
||||
@@ -494,9 +471,6 @@ void TextHandlerTest::receiveRichEdited_data()
|
||||
QTest::newRow("basic") << QStringLiteral("Edited") << QStringLiteral("Edited <span style=\"color:#000000\">(edited)</span>");
|
||||
QTest::newRow("multiple paragraphs") << QStringLiteral("<p>Edited</p>\n<p>Edited</p>")
|
||||
<< QStringLiteral("<p>Edited</p>\n<p>Edited <span style=\"color:#000000\">(edited)</span></p>");
|
||||
QTest::newRow("blockquote")
|
||||
<< QStringLiteral("<blockquote>Edited</blockquote>")
|
||||
<< QStringLiteral("<blockquote><table><tr><td>\u201CEdited\u201D</td></tr></table></blockquote><p> <span style=\"color:#000000\">(edited)</span></p>");
|
||||
}
|
||||
|
||||
void TextHandlerTest::receiveRichEdited()
|
||||
@@ -507,7 +481,8 @@ void TextHandlerTest::receiveRichEdited()
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, room->messageEvents().at(2).get()), testOutputString);
|
||||
const auto event = eventCast<const Quotient::RoomMessageEvent>(room->messageEvents().at(2).get());
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, event, false, event->isReplaced()), testOutputString);
|
||||
}
|
||||
|
||||
void TextHandlerTest::receiveLineSeparator()
|
||||
@@ -526,5 +501,57 @@ void TextHandlerTest::receiveRichCodeUrl()
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(), input);
|
||||
}
|
||||
|
||||
void TextHandlerTest::componentOutput_data()
|
||||
{
|
||||
QTest::addColumn<QString>("testInputString");
|
||||
QTest::addColumn<QList<MessageComponent>>("testOutputComponents");
|
||||
|
||||
QTest::newRow("multiple paragraphs") << QStringLiteral("<p>Text</p>\n<p>Text</p>")
|
||||
<< QList<MessageComponent>{MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}},
|
||||
MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}}};
|
||||
QTest::newRow("code") << QStringLiteral("<p>Text</p>\n<pre><code class=\"language-html\">Some code\n</code></pre>")
|
||||
<< QList<MessageComponent>{MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}},
|
||||
MessageComponent{MessageComponentType::Code,
|
||||
QStringLiteral("Some code"),
|
||||
QVariantMap{{QStringLiteral("class"), QStringLiteral("html")}}}};
|
||||
QTest::newRow("quote") << QStringLiteral("<p>Text</p>\n<blockquote>\n<p>blockquote</p>\n</blockquote>")
|
||||
<< QList<MessageComponent>{MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}},
|
||||
MessageComponent{MessageComponentType::Quote, QStringLiteral("\"blockquote\""), {}}};
|
||||
QTest::newRow("no tag first paragraph") << QStringLiteral("Text\n<p>Text</p>")
|
||||
<< QList<MessageComponent>{MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}},
|
||||
MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}}};
|
||||
QTest::newRow("no tag last paragraph") << QStringLiteral("<p>Text</p>\nText")
|
||||
<< QList<MessageComponent>{MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}},
|
||||
MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}}};
|
||||
QTest::newRow("inline code") << QStringLiteral("<p><code>https://kde.org</code></p>\n<p>Text</p>")
|
||||
<< QList<MessageComponent>{MessageComponent{MessageComponentType::Text, QStringLiteral("<code>https://kde.org</code>"), {}},
|
||||
MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}}};
|
||||
QTest::newRow("inline code single block") << QStringLiteral("<code>https://kde.org</code>")
|
||||
<< QList<MessageComponent>{
|
||||
MessageComponent{MessageComponentType::Text, QStringLiteral("<code>https://kde.org</code>"), {}}};
|
||||
QTest::newRow("long start tag")
|
||||
<< QStringLiteral(
|
||||
"Ah, you mean something like<br/><pre data-md=\"```\"><code class=\"language-qml\"># main.qml\nimport CustomQml\n...\nControls.TextField { id: "
|
||||
"someField }\nCustomQml {\n someTextProperty: someField.text\n}\n</code></pre>Sure you can, it's still local to the same file where you "
|
||||
"defined the id")
|
||||
<< QList<MessageComponent>{
|
||||
MessageComponent{MessageComponentType::Text, QStringLiteral("Ah, you mean something like"), {}},
|
||||
MessageComponent{
|
||||
MessageComponentType::Code,
|
||||
QStringLiteral(
|
||||
"# main.qml\nimport CustomQml\n...\nControls.TextField { id: someField }\nCustomQml {\n someTextProperty: someField.text\n}"),
|
||||
QVariantMap{{QStringLiteral("class"), QStringLiteral("qml")}}},
|
||||
MessageComponent{MessageComponentType::Text, QStringLiteral("Sure you can, it's still local to the same file where you defined the id"), {}}};
|
||||
}
|
||||
|
||||
void TextHandlerTest::componentOutput()
|
||||
{
|
||||
QFETCH(QString, testInputString);
|
||||
QFETCH(QList<MessageComponent>, testOutputComponents);
|
||||
|
||||
TextHandler testTextHandler;
|
||||
QCOMPARE(testTextHandler.textComponents(testInputString), testOutputComponents);
|
||||
}
|
||||
|
||||
QTEST_MAIN(TextHandlerTest)
|
||||
#include "texthandlertest.moc"
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<name xml:lang="it">NeoChat</name>
|
||||
<name xml:lang="ka">NeoChat</name>
|
||||
<name xml:lang="ko">NeoChat</name>
|
||||
<name xml:lang="lv">NeoChat</name>
|
||||
<name xml:lang="nl">NeoChat</name>
|
||||
<name xml:lang="nn">NeoChat</name>
|
||||
<name xml:lang="pa">ਨਿਓ-ਚੈਟ</name>
|
||||
@@ -59,49 +60,43 @@
|
||||
<summary xml:lang="fi">Keskustelu ystäviesi kanssa Matrixissa</summary>
|
||||
<summary xml:lang="fr">Discuter avec vos ami(e)s sur le réseau Matrix</summary>
|
||||
<summary xml:lang="gl">Charle coas súas amizades en Matrix.</summary>
|
||||
<summary xml:lang="ia">Starta Conversation conntu amicos sur matrix</summary>
|
||||
<summary xml:lang="hu">Csevegjen barátaival a matrixon</summary>
|
||||
<summary xml:lang="ia">Starta Conversation con tu amicos sur matrix</summary>
|
||||
<summary xml:lang="it">Conversa con i tuoi contatti su matrix</summary>
|
||||
<summary xml:lang="ka">ესაუბრეთ მეგობრებს Matrix-ზე</summary>
|
||||
<summary xml:lang="ko">Matrix를 사용하여 친구들과 대화하기</summary>
|
||||
<summary xml:lang="lv">Tērzējiet ar saviem draugiem „Matrix“ tīklā</summary>
|
||||
<summary xml:lang="nl">Met uw vrienden chatten op matrix</summary>
|
||||
<summary xml:lang="nn">Prat med vennar på Matrix</summary>
|
||||
<summary xml:lang="pl">Rozmawiaj ze swoimi znajomymi w Matriksie</summary>
|
||||
<summary xml:lang="sl">Klepet z vašimi prijatelji na matrixu</summary>
|
||||
<summary xml:lang="sv">Chatta med dina vänner på Matrix</summary>
|
||||
<summary xml:lang="ta">மேட்ரிக்ஸு மூலம் உங்கள் நண்பர்களிடம் பேசலாம்</summary>
|
||||
<summary xml:lang="tr">Matrix'te arkadaşlarınızla sohbet edin</summary>
|
||||
<summary xml:lang="tr">Matrix’te arkadaşlarınızla sohbet edin</summary>
|
||||
<summary xml:lang="uk">Спілкуйтеся з вашими друзями у matrix</summary>
|
||||
<summary xml:lang="x-test">xxChat with your friends on matrixxx</summary>
|
||||
<summary xml:lang="zh-CN">在 Matrix 上与朋友聊天</summary>
|
||||
<summary xml:lang="zh-TW">在 Matrix 上與您的朋友聊天</summary>
|
||||
<description>
|
||||
<p>NeoChat is a client for Matrix, the decentralized communication protocol for instant messaging. It allows you to send text messages, videos and audio files to your family, colleagues and friends. It uses KDE frameworks and most notably Kirigami
|
||||
to provide a convergent experience across multiple platforms.</p>
|
||||
<p xml:lang="ar">نيوتشات هو عميل ماتركس Matrix، (ميفاق الاتصال اللامركزي للمراسلة الفورية). يتيح لك نيوتشات إرسال رسائل نصية ومقاطع فيديو وملفات صوتية إلى عائلتك وزملائك وأصدقائك. يستخدم أطر عمل كيدي وأبرزها Kirigami لتوفير تجربة متقاربة عبر منصات متعددة.</p>
|
||||
<p xml:lang="ca">El NeoChat és un client de Matrix, el protocol descentralitzat de comunicacions de missatgeria instantània. Permet enviar missatges de text, fitxers de vídeo i d'àudio a la família, col·legues i amics. Fa servir els Frameworks de KDE i, sobretot, el Kirigami per a proporcionar una experiència convergent a través de diverses plataformes.</p>
|
||||
<p xml:lang="ca-valencia">NeoChat és un client de Matrix, el protocol descentralitzat de comunicacions de missatgeria instantània. Permet enviar missatges de text, fitxers de vídeo i d'àudio a la família, col·legues i amics. Utilitza els Frameworks de KDE i, sobretot, Kirigami per a proporcionar una experiència convergent a través de diverses plataformes.</p>
|
||||
<p xml:lang="en-GB">NeoChat is a client for Matrix, the decentralised communication protocol for instant messaging. It allows you to send text messages, videos and audio files to your family, colleagues and friends. It uses KDE frameworks and most notably Kirigami to provide a convergent experience across multiple platforms.</p>
|
||||
<p xml:lang="eo">NeoChat estas kliento por Matrix, la malcentra komunikoprotokolo por tuja mesaĝado. Ĝi ebligas al vi sendi tekstmesaĝojn, filmetojn kaj sondosierojn al via familio, kolegoj kaj amikoj. Ĝi uzas KDE-framojn kaj precipe Kirigami por disponigi konverĝan sperton tra pluraj platformoj.</p>
|
||||
<p xml:lang="es">NeoChat es un cliente para Matrix, el protocolo de comunicaciones descentralizado para mensajería instantánea. Le permite enviar mensajes de texto, vídeos y archivos de sonido a su familia, compañeros de trabajo y amigos. Usa la infraestructura de KDE y, en particular, Kirigami para proporcionar una experiencia convergente en muchas plataformas.</p>
|
||||
<p xml:lang="eu">NeoChat «Matrix»erako, bat-bateko mezularitzarako komunikazio deszentralizatuko protokolorako, bezero bat da. Zure sendiari, kide eta lagunei testu mezuak, bideo eta audio fitxategiak bidaltzeko aukera ematen dizu. «KDE Frameworks» eta bereziki «Kirigami» erabiltzen ditu plataforma anitzen artean esperientzia konbergente bat eskaintzeko.</p>
|
||||
<p xml:lang="fi">NeoChat on asiakassovellus Matrixille, hajautetulle pikaviestinyhteyskäytännölle. Sillä voi lähettää teksti-, video- ja ääniviestejä perheelle, tutuille ja ystäville. Se käyttää KDE-kehystä ja erityisesti Kirigamia tuottaakseen mukautuvan monialustaisen käyttökokemuksen.</p>
|
||||
<p xml:lang="fr">NeoChat est un client pour le protocole Matrix, un protocole décentralisé de communications pour messagerie instantané. Il vous permet d'envoyer des messages de texte, des vidéos et des fichiers audio à votre famille, vos collègues et vos amis. Il utilise les environnements de développement et plus précisément Kirigami pour fournir une expérience convergente sur plusieurs plate-formes. </p>
|
||||
<p xml:lang="gl">NeoChat é un cliente para Matrix, o protocolo de comunicación descentralizada para mensaxaría instantánea. Podes enviar mensaxes de texto, vídeos e ficheiros de son á túa familia, colegas e amizades. Usas infraestruturas de KDE e principalmente Kirigami para proporcionar unha experiencia de uso converxente para varias plataformas.</p>
|
||||
<p xml:lang="hu">A NeoChat egy kliens a Matrixhoz, az azonnali üzenetküldés decentralizált komunikációs protokolljához.. Szöveges üzeneteket, videókat és hangfájlokat küldhet családjának, kollégáinak és barátainak. A KDE keretrendszert használja, a Kirigaminak köszönhetően konvergens élményt nyújt több platformon is.</p>
|
||||
<p xml:lang="ia">NeoChat es un cliente per Matrix, le protocollo de communication decentralisate per messager instantanee. Illo te permitte inviar messager de texto, files de video e audio a tu familia, collegas e amicos usante. Illo usa KDE frameworks e super toto Kirigamii forni un experientia convergente trans platteforme multiple.</p>
|
||||
<p xml:lang="it">NeoChat è un client per Matrix, il protocollo di comunicazione decentralizzato per la messaggistica istantanea. Ti consente di inviare messaggi di testo, video e file audio a familiari, colleghi e amici. Utilizza i framework KDE e in particolare Kirigami per fornire un'esperienza convergente su più piattaforme.</p>
|
||||
<p xml:lang="ka">NeoChat არის Matrix კლიენტი. ის საშუალებას გაძლევთ გაგზავნოთ ტექსტური შეტყობინებები, ვიდეოები და აუდიო ფაილები თქვენს ოჯახს, კოლეგებსა და მეგობრებს მატრიქსის პროტოკოლის გამოყენებით.</p>
|
||||
<p xml:lang="ko">NeoChat은 분산형 인스턴트 메시징 통신 프로토콜인 Matrix 클라이언트입니다. 가족, 동료, 친구에게 텍스트 메시지, 동영상, 오디오 파일을 전송할 수 있습니다. KDE 프레임워크와 Kirigami를 사용하여 다양한 플랫폼에서 일관적인 사용자 경험을 제공합니다.</p>
|
||||
<p xml:lang="nl">NeoChat is een client voor Matrix, het gedecentraliseerde communicatieprotocol voor instant messages. Het biedt u het verzenden van tekstberichten, video's en geluidsbestanden naar uw familie, collega's en vrienden. Het gebruik KDE frameworks en het meest opmerkelijk Kirigami om een convergente ervaring te leveren op meerdere platforms.</p>
|
||||
<p xml:lang="nn">NeoChat er ein klient for Matrix, ein protokoll for desentralisert kommunikasjon. Du kan utveksla tekst, lyd og videoar med kollegaar, vennar og familie. Programmet brukar KDE Frameworks og Kirigami for å gje ei brukarflate tilpassa ulike plattformer.</p>
|
||||
<p xml:lang="pl">NeoChat jest programem do Matriksa, protokołu rozproszonego porozumiewania się w czasie rzeczywistym. Umożliwia wysyłanie wiadomości tekstowych, filmów oraz dźwięku do twojej rodziny, znajomych oraz przyjaciół. Używa szkieletów KDE i głównie Kirigami, aby zapewnić spójne wrażenia na wielu platformach</p>
|
||||
<p xml:lang="pt">O NeoChat é um cliente do Matrix. O mesmo permite-lhe enviar mensagens de texto, ficheiros de vídeo e áudio para a sua família, colegas e amigos com o protocolo Matrix. Usa as plataformas do KDE, e principalmente o Kirigami, para oferecer uma experiência convergente entre várias plataformas.</p>
|
||||
<p xml:lang="sl">Neochat je odjemalec za Matrix, decentralizirani komunikacijski protokol za takojšnje sporočanje. Omogoča vam pošiljanje besedilnih sporočil, videoposnetkov in zvočnih datotek svoji družini, sodelavcem in prijateljem. Uporablja okvire ogrodje KDE frameworks in predvsem Kirigami za zagotavljanje konvergentne izkušnje na več platformah.</p>
|
||||
<p xml:lang="sv">NeoChat är en klient för Matrix, det decentraliserade kommunikationsprotokollet för direktmeddelanden. Den låter dig skicka textmeddelanden, videor och ljudfiler till din familj, kollegor och vänner. Den använder KDE Ramverk, i synnerhet Kirigami, för att tillhandahålla en konvergent upplevelse på flera plattformar.</p>
|
||||
<p xml:lang="tr">NeoChat, anlık iletileşme için merkezi olmayan iletişim protokolü olan Matrix için bir istemcidir. Ailenize, iş arkadaşlarınıza ve arkadaşlarınıza metin iletiler, videolar ve ses dosyaları göndermenize olanak tanır. Birden çok platformda yakınsak bir deneyim sağlamak için KDE Frameworks ve en önemlilerinden Kirigami'yi kullanır.</p>
|
||||
<p xml:lang="uk">NeoChat — клієнт Matrix, децентралізованого протоколу спілкування для миттєвого обміну повідомленнями. За його допомогою ви можете надсилати текстові повідомлення, відео та звукові файли вашій родин, колегами та друзям. У програмі використано бібліотеки KDE, зокрема Kirigami, для надання однорідного середовища на декількох програмних та апаратних платформах.</p>
|
||||
<p xml:lang="x-test">xxNeoChat is a client for Matrix, the decentralized communication protocol for instant messaging. It allows you to send text messages, videos and audio files to your family, colleagues and friends. It uses KDE frameworks and most notably Kirigami to provide a convergent experience across multiple platforms.xx</p>
|
||||
<p xml:lang="zh-TW">NeoChat 是去中心化即時通訊協定 Matrix 的一個用戶端。它讓您可以傳送文字訊息、影片、音訊檔案給您的家人、同事或朋友。NeoChat 使用 KDE frameworks,尤其是 Kirigami,來提供跨平台的響應式體驗。</p>
|
||||
<p>NeoChat is a chat app that lets you take full advantage of the Matrix network. It provides you with a secure way to send text messages, videos and audio files to your family, colleagues and friends.</p>
|
||||
<p xml:lang="ca">El NeoChat és una aplicació de xat que us permet aprofitar plenament la xarxa Matrix. Proporciona una manera segura d'enviar missatges de text, vídeos i arxius d'àudio a la vostra família, companys i amics.</p>
|
||||
<p xml:lang="ca-valencia">NeoChat és una aplicació de xat que us permet aprofitar plenament la xarxa Matrix. Proporciona una manera segura d'enviar missatges de text, vídeos i arxius d'àudio a la vostra família, companys i amics.</p>
|
||||
<p xml:lang="eo">NeoChat estas babilej-apo, kiu ebligas al vi plene profiti de la Matrix-reto. Ĝi provizas al vi sekuran manieron sendi tekstmesaĝojn, filmetojn kaj sondosierojn al via familio, kolegoj kaj amikoj.</p>
|
||||
<p xml:lang="es">NeoChat es una aplicación de chat que le permite aprovechar al máximo la red Matrix. Le proporciona un modo seguro de enviar mensajes de texto, vídeos y archivos de sonido a su familia, colegas y amigos.</p>
|
||||
<p xml:lang="eu">NeoChat, Matrix sarearen abantaila guztiei probetsua ateratzeko aukera ematen dizun berriketa aplikaizo bat da. Zure familiari, kideei eta lagunei testu mezuak, bideoak eta audio fitxategiak era seguruan bidaltzeko aukera ematen dizu.</p>
|
||||
<p xml:lang="fr">NeoChat est une application de discussions vous permettant de profiter pleinement du réseau Matrix. Elle vous offre un moyen sécurisé d’envoyer des messages de texte, des vidéos et des fichiers audio à votre famille, vos collègues et vos ami(e)s.</p>
|
||||
<p xml:lang="hu">A NeoChat egy olyan csevegőalkalmazás, amellyel teljes mértékben kihasználhatja a Matrix hálózatot. Biztonságos módot biztosít szöveges üzenetek, videók és hangfájlok küldéséhez családtagjainak, kollégáinak és barátainak.</p>
|
||||
<p xml:lang="ia">NeoChat es un app de conversation que te permitte prender avantage plen del rete Matrix. Il te forni un modo secur de inviar messages de texto, videos e files audio a tui familia, collegas e amicos.</p>
|
||||
<p xml:lang="it">NeoChat è un'applicazione di chat che ti consente di sfruttare appieno la rete Matrix. Ti fornisce un modo sicuro per inviare messaggi di testo, video e file audio a familiari, colleghi e amici.</p>
|
||||
<p xml:lang="ka">NeoChat ჩატის აპია, რომელიც საშუალება გაძლევთ, Matrix-ის ქსელის საშუალებები ბოლომდე გამოიყენოთ. ის გაძლევთ უსაფრთხო გზას, გააგზავნოთ ტექსტური შეტყობინებები, ვიდეოებ და აუდიოფაილები თქვენს ოჯახთან, კოლეგებთან და მეგობრებთან.</p>
|
||||
<p xml:lang="lv">„NeoChat“ ir tērzēšanas programma, kas ļauj pilnvērtīgi izmantot „Matrix“ tīklu. Tā sniedz drošu veidu teksta ziņu, video un audio sūtīšanai ģimenes locekļiem, kolēģiem un draugiem.</p>
|
||||
<p xml:lang="nl">NeoChat is een chat-toepassing die u het volledige voordeel van het Matrix-netwerk laat genieten. Het levert u op een veilige manier tekstberichten, video's en geluidsbestanden naar uw familie, collega's en vrienden te verzenden.</p>
|
||||
<p xml:lang="pl">NoeChat to aplikacja do rozmów, która umożliwia wykorzystanie wszystkich możliwości Matriksa. Umożliwia wysyłanie wiadomości tekstowych, filmów i dźwięków w bezpieczny sposób do twojej rodziny, kolegów i przyjaciół.</p>
|
||||
<p xml:lang="sl">NeoChat je aplikacija za klepet, ki vam omogoča, da v celoti izkoristite omrežje Matrix. Zagotavlja vam varen način za pošiljanje besedilnih sporočil, videoposnetkov in zvočnih datotek vaši družini, sodelavcem in prijateljem.</p>
|
||||
<p xml:lang="tr">NeoChat, Matrix ağının tüm özelliklerini kullanan bir sohbet uygulamasıdır. Ailenize, arkadaşlarınıza ve iş arkadaşlarınıza metin iletileri, ses ve video dosyaları göndermenin kolay bir yolunu sunar.</p>
|
||||
<p xml:lang="uk">NeoChat є програмою для спілкування, за допомогою якої ви можете скористатися усіма перевагами мережі Matrix. За її допомогою ви можете безпечно надсилати текстові повідомлення, відео та звукові файли вашим родичам, колегам та друзям.</p>
|
||||
<p xml:lang="x-test">xxNeoChat is a chat app that lets you take full advantage of the Matrix network. It provides you with a secure way to send text messages, videos and audio files to your family, colleagues and friends.xx</p>
|
||||
<p xml:lang="zh-TW">NeoChat 是一個讓您能夠完全利用 Matrix 網路的聊天應用程式。它讓您安全地傳送文字訊息、影片或音訊檔給家人、同事或朋友等等。</p>
|
||||
<p>NeoChat aims to be a fully featured application for the Matrix specification. As such everything in the current stable specification with the notable exceptions of VoIP, threads and some aspects of End-to-End Encryption are supported. There are a few other smaller omissions due to the fact that the Matrix spec is constantly evolving but the aim remains to provide eventual support for the entire spec.</p>
|
||||
<p xml:lang="ar">يهدف نيوتشات إلى أن يكون تطبيقًا كامل الميزات لمواصفات ماتركس. على هذا النحو يتم دعم كل شيء في المواصفات المستقرة الحالية مع الاستثناءات الملحوظة لـ VoIP والخيوط وبعض جوانب التشفير من طرف إلى طرف. هناك عدد قليل من الإغفالات الصغيرة الأخرى بسبب حقيقة أن مواصفات ماتركس تتطور باستمرار ، ولكن يبقى الهدف توفير الدعم النهائي للمواصفات بأكملها.</p>
|
||||
<p xml:lang="ca">NeoChat pretén ser una aplicació amb totes les característiques per a l'especificació de Matrix. Com a tal, s'ha implementat tota l'especificació actual estable amb les notables excepcions de la VoIP, fils i alguns aspectes de l'encriptatge d'extrem a extrem. Hi ha algunes altres omissions més petites a causa del fet que l'especificació de Matrix està evolucionant constantment, però l'objectiu segueix sent proporcionar suport eventual per a tota l'especificació.</p>
|
||||
@@ -113,17 +108,19 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<p xml:lang="fi">NeoChat pyrkii olemaan Matrix-määritelmän täysominaisuuksinen sovellus, joten se tukee kaikkea nykyisessä vakaassa määritelmässä muutamaa huomattavaa poikkeusta lukuun ottamatta (VoIP, säikeet ja jotkin piirteet päästä päähän -salauksessa). Joitakin pienempiäkin puutteita on Matrix-määritelmän jatkuvan kehityksen vuoksi, mutta lopputavoitteena on tarjota määritelmän täysi tuki.</p>
|
||||
<p xml:lang="fr">L'objectif de NeoChat est d'être une application complète pour le protocole Matrix. En tant que tel, tout dans la spécification stable actuelle avec les exceptions notables de VoIP, les processus et certains aspects du chiffrement de bout en bout sont pris en charge. Il y a quelques autres petites omissions en raison du fait que la spécification du protocole Matrix est en constante évolution. Cependant, l'objectif reste de fournir un soutien éventuel pour l'ensemble de la spécification.</p>
|
||||
<p xml:lang="gl">NeoChat pretende ser unha aplicación completa para a especificación de Matrix. Coas excepcións de VoIP, conversas fiadas e algúns aspectos da cifraxe de extremo a extremo, a versión estábel segue as especificacións. Existen algunhas outras pequenas omisións debido ao feito de que Matrix está en continua evolución pero a intención é implementar a especificación completa.</p>
|
||||
<p xml:lang="hu">A NeoChat célja, hogy a Matrix specifikációnak megfelelő teljes funkcionalitású alkalmazás legyen. Mint ilyen, a jelenlegi stabil specifikáció támogatott a VoIP, a szálak és a végpontok közötti titkosítás egyes elemeinek kivételével. Van még néhány kisebb hiányosság annak köszönhetően, hogy a Matrix specifikáció folyamatosan fejlődik, de végső cél a teljes specifikáció megvalósítása.</p>
|
||||
<p xml:lang="ia">NeoChat aspira a esser un application plenemente eminente per le specification de Matrix. Tal como omne cosas in le specification currentemente stabile con le exceptiones notabile de VOIP, threads e alcun aspectos del cryptation End-to-End es supportate. Il ha ltere pauc omissiones, debite al facto que le specification de Matrix es in evolution constante ma le aspiration remane a fornir supporto eventual per le integre specification.</p>
|
||||
<p xml:lang="it">NeoChat mira ad essere un'applicazione completa per le specifiche Matrix. Pertanto, sono supportati tutti gli elementi dell'attuale specifica stabile con le notevoli eccezioni di VoIP, conversazioni e alcuni aspetti della cifratura end-to-end. Ci sono alcune altre piccole omissioni dovute al fatto che le specifiche Matrix sono in continua evoluzione, ma l'obiettivo rimane quello di fornire un eventuale supporto per l'intera specifica.</p>
|
||||
<p xml:lang="ka">NeoChat-ი მიზნად ისახავს Matrix სპეციფიკაციის სრული განხორციელება ჰქონდეს. როგორც ასეთი, ყველაფერი მიმდინარე სპეციფიკაციიდან, VoIP-ის, ძაფებისა და გამჭოლი დაშიფვრის ზოგიერთი ასპექტის გარდა, მხარდაჭერილია. შეძლება ასევე იყოს მცირე ლაფსუსებიც იმის გამო, რომ Matrix-ის სპეციფიკაცია მუდმივად ვითარგდება, მაგრამ ჩვენი მიზანი მისი სრული მხარდაჭერაა.</p>
|
||||
<p xml:lang="ka">NeoChat მიზნად ისახავს Matrix სპეციფიკაციის სრული განხორციელება ჰქონდეს. როგორც ასეთი, ყველაფერი მიმდინარე სპეციფიკაციიდან, VoIP-ის, ძაფებისა და გამჭოლი დაშიფვრის ზოგიერთი ასპექტის გარდა, მხარდაჭერილია. შეძლება ასევე იყოს მცირე ლაფსუსებიც იმის გამო, რომ Matrix-ის სპეციფიკაცია მუდმივად ვითარდება, მაგრამ ჩვენი მიზანი მისი სრული მხარდაჭერაა.</p>
|
||||
<p xml:lang="ko">NeoChat은 Matrix 표준을 따르는 프로그램을 목표로 합니다. 현재 안정 버전의 표준에서 제공하는 기능의 대부분을 지원하며, VoIP, 스레드, 일부 종단간 암호화와 같은 기능은 아직 지원하지 않습니다. Matrix 표준은 계속하여 진화 중이기 때문에 일부 기능이 빠져 있을 수도 있지만 장기적으로는 전체 표준을 지원하는 것이 목표입니다.</p>
|
||||
<p xml:lang="lv">„NeoChat“ mērķis ir piedāvāt plašas iespējas atbilstoši „Matrix“ specifikācijai. Līdz ar to programma atbalsta visu pašreizējā stabilajā specifikācijā, izņemot VoIP, pavedienus un dažos aspektos galšifrēšanu. Pastāv citas atsevišķas sīkas neieviestas daļas, jo „Matrix“ specifikācija nepārtraukti attīstās, tomēr mērķis ir ar laiku nodrošināt atbalstu pilnai specifikācijai.</p>
|
||||
<p xml:lang="nl">NeoChat richt zich op het volledig bieden van alle mogelijkheden van de Matrix-specificatie. Alles in de huidige stabiele specificatie met merkbare uitzondering van VoIP, gekoppelde discussies en sommige aspecten van eind-tot-eind versleuteling worden ondersteund. Er zijn een paar andere kleinere omissies vanwege het feit dat de Matrix specificatie constant evolueert maar het doel blijft het eventueel bieden van ondersteuning van de gehele specificatie.</p>
|
||||
<p xml:lang="nn">NeoChat har som mål å støtta all funksjonalitet i Matrix-spesifikasjonen. Førebels er alt i den gjeldande stabile spesifikasjonen støtta, med unntak av VoIP, trådar og nokre delar av ende-til-kryptering. Det finst òg andre småting som ikkje er støtta, sidan Matrix-spesifikasjon er i stadig endring, men målet er altså støtte for alt.</p>
|
||||
<p xml:lang="pl">NeoChat w zamyśle ma być pełnowartościową aplikacją wg wytycznych Matriksa. Z tego powodu, wszystko, co jest obecnie w stabilnych wytycznych z pominięciem VoIP, wątków i niektórych części szyfrowania Użytkownik-do-Użytkownika są obecnie obsługiwane. Pominięto też kilka mniejszych rzeczy ze względu na ciągły rozwój wytycznych Matriksa, lecz celem nadal jest zapewnienie obsługi wszystkich wytycznych.</p>
|
||||
<p xml:lang="pt">O NeoChat pretende ser uma aplicação completa para a especificação do Matrix. Como tal, tudo o que existe na especificação estável actual, com as notáveis excepções do VoIP, tópicos e alguns aspectos da Encriptação Ponto-a-Ponto, são suportados. Existem mais algumas omissões, devido ao facto que a norma do Matrix está em constante evolução, mas o objectivo continua a ser oferecer o suporte eventual para a norma por inteiro.</p>
|
||||
<p xml:lang="sl">Neochat cilja, da bi bila popolna aplikacija po specifikaciji Matrixa. Kot takšna vsebuje vse v trenutni stabilni specifikaciji z pomembnimi izjemami pri VoIP, nitih in nekaterih vidikov šifriranja od konca do konca. Obstaja nekaj drugih manjših opustitev zaradi dejstva, da se specifikacija Matrix nenehno razvija, vendar cilj ostaja zagotoviti morebitno podporo celotni specifikaciji.</p>
|
||||
<p xml:lang="sv">NeoChat har som mål att vara ett fullständigt program enligt Matrix-specifikationen. Som sådant stöds allt i den nuvarande stabila specifikationen, med de nämnvärda undantagen VoIP, trådar och några aspekter av kryptering hela vägen. Det finns några ytterligare utelämnanden på grund av att Matrix-specifikationen hela tiden utvecklas, men målet förblir att till slut erbjuda stöd för hela specifikationen.</p>
|
||||
<p xml:lang="tr">NeoChat, Matrix belirtimi için tam özellikli bir uygulama olmayı hedefler. Bu nedenle; VoIP, ileti zincirleri ve Uçtan Uca Şifreleme'nin bazı yönleri gibi dikkate değer istisnalar dışında var olan kararlı belirtimdeki her şey desteklenir. Matrix belirtiminin sürekli gelişmesi nedeniyle birkaç küçük eksiklik daha var; ancak amaç tüm belirtim için nihai destek sağlamak olmayı sürdürüyor.</p>
|
||||
<p xml:lang="tr">NeoChat, Matrix belirtimi için tam özellikli bir uygulama olmayı hedefler. Bu nedenle; VoIP, ileti zincirleri ve Uçtan Uca Şifreleme’nin bazı yönleri gibi dikkate değer istisnalar dışında var olan kararlı belirtimdeki her şey desteklenir. Matrix belirtiminin sürekli gelişmesi nedeniyle birkaç küçük eksiklik daha var; ancak amaç tüm belirtim için nihai destek sağlamak olmayı sürdürüyor.</p>
|
||||
<p xml:lang="uk">Метою створення NeoChat є повноцінна реалізація програми для специфікації Matrix. Як наслідок, реалізовано усе у поточній стабільній специфікації, окрім голосового інтернет-зв'язку, потоків та деяких аспектів міжвузлового шифрування. Є також декілька інших незначних прогалин через те, що специфікація Matrix постійно змінюється, але метою лишається повна підтримка специфікації.</p>
|
||||
<p xml:lang="x-test">xxNeoChat aims to be a fully featured application for the Matrix specification. As such everything in the current stable specification with the notable exceptions of VoIP, threads and some aspects of End-to-End Encryption are supported. There are a few other smaller omissions due to the fact that the Matrix spec is constantly evolving but the aim remains to provide eventual support for the entire spec.xx</p>
|
||||
<p xml:lang="zh-TW">NeoChat 以完整支援 Matrix 標準為目標,因此目前穩定版標準除了 VoIP、對話串與端對端加密的某些部分以外的所有部分都有支援。其他部分還有一些較小的不支援的部分,這是因為 Matrix 標準隨時都在改進,但目標仍然時最終提供整個標準的完整支援。</p>
|
||||
@@ -138,10 +135,12 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<p xml:lang="fi">Matrix-määritelmän kehittyessä NeoChat tukee myös monia epävakaita ominaisuuksia. Tällä hetkellä näitä ovat:</p>
|
||||
<p xml:lang="fr">En raison de la nature du développement des spécifications du protocole Matrix, NeoChat prend également en charge de nombreuses fonctionnalités instables. Actuellement, ce sont :</p>
|
||||
<p xml:lang="gl">Debido á natureza do desenvolvemento da especificación de Matrix, NeoChat tamén inclúe varias funcionalidades non estábeis:</p>
|
||||
<p xml:lang="hu">A Matrix specifikáció fejlesztésének jellegéből adódóan a NeoChat számos instabil funkciót is támogat. Jelenleg a következőket:</p>
|
||||
<p xml:lang="ia">Debite al natura del disveloppamento de specification de Matrix NeoChat tamben supporta numerose characteristicas instabile. Currentemente istes es:</p>
|
||||
<p xml:lang="it">A causa della natura dello sviluppo delle specifiche Matrix, NeoChat supporta anche numerose funzionalità instabili. Attualmente queste sono:</p>
|
||||
<p xml:lang="ka">Matrix-ის სპეციფიკაციის განვითარების ბუნების გამო NeoChat-ს ასევე აქვს უამრავი არასტაბილური ფუნქციაც. ახლა ისინია:</p>
|
||||
<p xml:lang="ko">Matrix 표준 개발의 특징으로 인하여 NeoChat은 일부 실험적인 기능을 지원합니다. 현재 지원하는 기능은 다음과 같습니다.</p>
|
||||
<p xml:lang="lv">„Matrix“ specifikācijas veida dēļ „NeoChat“ attīstība atbalsta arī vairākas nestabilas iespējas, šobrīd šādas ir:</p>
|
||||
<p xml:lang="nl">Vanwege de aard van de ontwikkeling van de Matrix specificatie ondersteunt NeoChat ook talloze onstabiele mogelijkheden. Dit zijn nu:</p>
|
||||
<p xml:lang="nn">På grunn av måten Matrix-spesifikasjonen vert utvikla på, støttar NeoChat òg nokre uferdige funksjonar:</p>
|
||||
<p xml:lang="pl">Ze względu na sposób rozwoju Matriksa, NeoChat obsługuje także kilka niestabilnych możliwości. Obecnie są to:</p>
|
||||
@@ -165,10 +164,12 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<li xml:lang="fi">Kyselyt – MSC3381</li>
|
||||
<li xml:lang="fr">Sondages - MSC3381</li>
|
||||
<li xml:lang="gl">Enquisas — MSC3381</li>
|
||||
<li xml:lang="hu">Szavazások - MSC3381</li>
|
||||
<li xml:lang="ia">Inquestas - MSC3381</li>
|
||||
<li xml:lang="it">Sondaggi - MSC3381</li>
|
||||
<li xml:lang="ka">Polls - MSC3381</li>
|
||||
<li xml:lang="ko">투표 - MSC3381</li>
|
||||
<li xml:lang="lv">Aptaujas — MSC3381</li>
|
||||
<li xml:lang="nl">Polls - MSC3381</li>
|
||||
<li xml:lang="nn">Avstemmingar – MSC3381</li>
|
||||
<li xml:lang="pl">Ankiety - MSC3381</li>
|
||||
@@ -176,7 +177,7 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<li xml:lang="sl">Polls - MSC3381</li>
|
||||
<li xml:lang="sv">Polls - MSC3381</li>
|
||||
<li xml:lang="ta">வாக்கெடுப்புகள் - MSC3381</li>
|
||||
<li xml:lang="tr">Anketler - MSC3381</li>
|
||||
<li xml:lang="tr">Anketler — MSC3381</li>
|
||||
<li xml:lang="uk">Опитування - MSC3381</li>
|
||||
<li xml:lang="x-test">xxPolls - MSC3381xx</li>
|
||||
<li xml:lang="zh-TW">投票 - MSC3381</li>
|
||||
@@ -191,10 +192,12 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<li xml:lang="fi">Tarrapakkaukset – MSC2545</li>
|
||||
<li xml:lang="fr">Paquets d'auto-collants - MSC2545</li>
|
||||
<li xml:lang="gl">Paquetes de adhesivos — MSC2545</li>
|
||||
<li xml:lang="hu">Matricacsomagok - MSC2545</li>
|
||||
<li xml:lang="ia">Etiquetta gummate (sticker) -MSC2545</li>
|
||||
<li xml:lang="it">Pacchetti di adesivi - MSC2545</li>
|
||||
<li xml:lang="ka">სტიკერების პაკეტები - MSC2545</li>
|
||||
<li xml:lang="ko">스티커 팩 - MSC2545</li>
|
||||
<li xml:lang="lv">Uzlīmju pakas — MSC2545</li>
|
||||
<li xml:lang="nl">Sticker Packs - MSC2545</li>
|
||||
<li xml:lang="nn">Klistremerke-pakkar – MSC2545</li>
|
||||
<li xml:lang="pl">Paczki naklejek - MSC2545</li>
|
||||
@@ -202,7 +205,7 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<li xml:lang="sl">Sticker Packs - MSC2545</li>
|
||||
<li xml:lang="sv">Sticker Packs - MSC2545</li>
|
||||
<li xml:lang="ta">ஒட்டி தொகுப்புகள் - MSC2545</li>
|
||||
<li xml:lang="tr">Yapışkan Paketleri - MSC2545</li>
|
||||
<li xml:lang="tr">Yapışkan Paketleri — MSC2545</li>
|
||||
<li xml:lang="uk">Пакунки наліпок - MSC2545</li>
|
||||
<li xml:lang="x-test">xxSticker Packs - MSC2545xx</li>
|
||||
<li xml:lang="zh-TW">貼圖包 - MSC2545</li>
|
||||
@@ -217,10 +220,12 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<li xml:lang="fi">Sijaintitapahtumat – MSC3488</li>
|
||||
<li xml:lang="fr">Événements de lieu - MSC3488</li>
|
||||
<li xml:lang="gl">Localización de eventos — MSC3488</li>
|
||||
<li xml:lang="hu">Események helyadatai - MSC3488</li>
|
||||
<li xml:lang="ia">Eventos de Location - MSC3488</li>
|
||||
<li xml:lang="it">Località eventi - MSC3488</li>
|
||||
<li xml:lang="ka">მდებარეობის მოვლენები - MSC3488</li>
|
||||
<li xml:lang="ko">위치 이벤트 - MSC3488</li>
|
||||
<li xml:lang="lv">Atrašanās vietas notikumi — MSC3488</li>
|
||||
<li xml:lang="nl">Locatie gebeurtenissen - MSC3488</li>
|
||||
<li xml:lang="nn">Posisjonshendingar – MSC3488</li>
|
||||
<li xml:lang="pl">Wydarzenia w miejscach - MSC3488</li>
|
||||
@@ -228,7 +233,7 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<li xml:lang="sl">Location Events - MSC3488</li>
|
||||
<li xml:lang="sv">Location Events - MSC3488</li>
|
||||
<li xml:lang="ta">இட நிகழ்வுகள் - MSC3488</li>
|
||||
<li xml:lang="tr">Konum Etkinlikleri - MSC3488</li>
|
||||
<li xml:lang="tr">Konum Etkinlikleri — MSC3488</li>
|
||||
<li xml:lang="uk">Місцеві зустрічі - MSC3488</li>
|
||||
<li xml:lang="x-test">xxLocation Events - MSC3488xx</li>
|
||||
<li xml:lang="zh-TW">位置事件 - MSC3488</li>
|
||||
@@ -236,11 +241,18 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
</description>
|
||||
<url type="homepage">https://apps.kde.org/neochat</url>
|
||||
<url type="bugtracker">https://bugs.kde.org/enter_bug.cgi?product=NeoChat</url>
|
||||
<url type="vcs-browser">https://invent.kde.org/network/neochat</url>
|
||||
<url type="contact">https://go.kde.org/matrix/#/#neochat:kde.org</url>
|
||||
<url type="donation">https://kde.org/community/donations/?app=neochat</url>
|
||||
<url type="contribute">https://community.kde.org/Get_Involved/</url>
|
||||
<categories>
|
||||
<category>Network</category>
|
||||
</categories>
|
||||
<developer>
|
||||
<id>kde.org</id>
|
||||
<keywords>
|
||||
<keyword>Matrix</keyword>
|
||||
<keyword>Kirigami</keyword>
|
||||
</keywords>
|
||||
<developer id="kde.org">
|
||||
<name>The KDE Community</name>
|
||||
<url>https://kde.org</url>
|
||||
</developer>
|
||||
@@ -258,11 +270,64 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<launchable type="desktop-id">org.kde.neochat.desktop</launchable>
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<image>https://cdn.kde.org/screenshots/neochat/application-mobile.png</image>
|
||||
<image>https://cdn.kde.org/screenshots/neochat/application.png</image>
|
||||
<caption>Main view with room list, chat, and room information</caption>
|
||||
<caption xml:lang="ar">العرض الرئيسة مع قائمة الغرف والدردشات و معلومات الغرفة</caption>
|
||||
<caption xml:lang="ca">Vista principal amb la llista de sales, xats i informació de les sales</caption>
|
||||
<caption xml:lang="ca-valencia">Vista principal amb la llista de sales, xats i informació de les sales</caption>
|
||||
<caption xml:lang="eo">Ĉefa vido kun ĉambra listo, babilejo kaj ĉambra informo</caption>
|
||||
<caption xml:lang="es">Vista principal con la lista de salas, chat e información de la sala</caption>
|
||||
<caption xml:lang="eu">Ikuspegi nagusia gela-zerrenda, berriketa, eta gelako informazioarekin</caption>
|
||||
<caption xml:lang="fi">Päänäkymä, jossa huoneluettelo, keskustelu ja huoneen tiedot</caption>
|
||||
<caption xml:lang="fr">Vue principale avec la liste des salons ainsi que des informations sur les salons et forums de discussions</caption>
|
||||
<caption xml:lang="gl">Vista principal coa lista de salas, a charla, e información da sala.</caption>
|
||||
<caption xml:lang="hu">A fő nézet a szobalistával, csevegéssel és szobainformációkkal</caption>
|
||||
<caption xml:lang="ia">Vista principal con lista de sala, chat e information de sala</caption>
|
||||
<caption xml:lang="it">Vista principale con elenco delle stanze, chat e informazioni sulla stanza</caption>
|
||||
<caption xml:lang="ka">მთავარი ხედი სურათების სიით, ჩატით და ოთახის ინფორმაციით</caption>
|
||||
<caption xml:lang="ko">대화방 목록, 채팅, 대화방 정보가 표시된 주 보기</caption>
|
||||
<caption xml:lang="lv">Pamata skats ar istabu sarakstu, tērzēšanu un istabas informāciju</caption>
|
||||
<caption xml:lang="nl">Hoofdweergave met lijst met rooms, chat en roominformatie</caption>
|
||||
<caption xml:lang="nn">Hovudvising med romliste, pratevindauge og rominformasjon</caption>
|
||||
<caption xml:lang="pl">Główny widok z wykazem pokojów, rozmowami i szczegółami pokojów</caption>
|
||||
<caption xml:lang="pt">A área principal com a lista de salas e com informações sobre a conversa e a sala</caption>
|
||||
<caption xml:lang="sl">Glavni pogled s seznamom sob, klepetom in informacijami o sobah</caption>
|
||||
<caption xml:lang="sv">Huvudvy med rumslista, chatt, och rumsinformation</caption>
|
||||
<caption xml:lang="ta">அரங்குப்பட்டியல், உரையாடல், மற்றும் அரங்குவிவரங்களைக் கொண்டுள்ள பிரதான காட்சி</caption>
|
||||
<caption xml:lang="tr">Oda listesini, sohbet penceresini ve oda bilgisini gösteren ana görünüm</caption>
|
||||
<caption xml:lang="uk">Головна панель із списком кімнат, спілкуванням та даними щодо кімнати</caption>
|
||||
<caption xml:lang="x-test">xxMain view with room list, chat, and room informationxx</caption>
|
||||
<caption xml:lang="zh-TW">主頁面,包含聊天室列表、聊天內容,與聊天室資訊</caption>
|
||||
</screenshot>
|
||||
<screenshot type="default">
|
||||
<image>https://cdn.kde.org/screenshots/neochat/application.png</image>
|
||||
<image>https://cdn.kde.org/screenshots/neochat/spaces.png</image>
|
||||
<caption>Discover new communities with Matrix Spaces</caption>
|
||||
<caption xml:lang="ca">Descobriu comunitats noves amb els espais de Matrix</caption>
|
||||
<caption xml:lang="ca-valencia">Descobriu comunitats noves amb els espais de Matrix</caption>
|
||||
<caption xml:lang="eo">Malkovru novajn komunumojn per Matrix Spaces</caption>
|
||||
<caption xml:lang="es">Descubra nuevas comunidades con los espacios de Matrix</caption>
|
||||
<caption xml:lang="eu">Ezagutu komunitate berriak Matrixeko Tokiak erabiliz</caption>
|
||||
<caption xml:lang="fr">Découvrez de nouvelles communautés avec les espaces sous Matrix</caption>
|
||||
<caption xml:lang="hu">Fedezzen fel új közösségeket a Matrix Terek segítségével</caption>
|
||||
<caption xml:lang="ia">Discoperi nove communitate con Matrix Spaces (Spatios de Matrix)</caption>
|
||||
<caption xml:lang="it">Scopri nuove comunità con Matrix Spaces</caption>
|
||||
<caption xml:lang="ka">აღმოაჩინეთ ახალი საზოგადოებები Matrix Spaces-თან ერთად</caption>
|
||||
<caption xml:lang="lv">Atklājiet jaunas kopienas ar „Matrix“ telpām</caption>
|
||||
<caption xml:lang="nl">Ontdek nieuwe gemeenschappen met Matrix-ruimten</caption>
|
||||
<caption xml:lang="pl">Odkrywaj nowe społeczności w Przestrzeniach Matriksa</caption>
|
||||
<caption xml:lang="sl">Odkrijte nove skupnosti z Matrix Spaces</caption>
|
||||
<caption xml:lang="tr">Matrix Alanlar ile yeni topluluklar keşfedin</caption>
|
||||
<caption xml:lang="uk">Пошук нових спільнот за допомогою Matrix Spaces</caption>
|
||||
<caption xml:lang="x-test">xxDiscover new communities with Matrix Spacesxx</caption>
|
||||
<caption xml:lang="zh-TW">利用 Matrix 聊天空間發現新的社群</caption>
|
||||
</screenshot>
|
||||
<!--
|
||||
Currently invalid. See https://github.com/ximion/appstream/issues/611
|
||||
<screenshot type="default" environment="plasma-mobile">
|
||||
<image>https://cdn.kde.org/screenshots/neochat/neochat-1.png</image>
|
||||
<caption>List of chats on mobile</caption>
|
||||
</screenshot>
|
||||
-->
|
||||
<screenshot environment="windows">
|
||||
<image>https://cdn.kde.org/screenshots/neochat/NeoChat-Windows-Timeline.png</image>
|
||||
<caption>Main view with room list, chat, and room information</caption>
|
||||
@@ -275,10 +340,12 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<caption xml:lang="fi">Päänäkymä, jossa huoneluettelo, keskustelu ja huoneen tiedot</caption>
|
||||
<caption xml:lang="fr">Vue principale avec la liste des salons ainsi que des informations sur les salons et forums de discussions</caption>
|
||||
<caption xml:lang="gl">Vista principal coa lista de salas, a charla, e información da sala.</caption>
|
||||
<caption xml:lang="hu">A fő nézet a szobalistával, csevegéssel és szobainformációkkal</caption>
|
||||
<caption xml:lang="ia">Vista principal con lista de sala, chat e information de sala</caption>
|
||||
<caption xml:lang="it">Vista principale con elenco delle stanze, chat e informazioni sulla stanza</caption>
|
||||
<caption xml:lang="ka">მთავარი ხედი სურათების სიით, ჩატით და ოთახის ინფორმაციით</caption>
|
||||
<caption xml:lang="ko">대화방 목록, 채팅, 대화방 정보가 표시된 주 보기</caption>
|
||||
<caption xml:lang="lv">Pamata skats ar istabu sarakstu, tērzēšanu un istabas informāciju</caption>
|
||||
<caption xml:lang="nl">Hoofdweergave met lijst met rooms, chat en roominformatie</caption>
|
||||
<caption xml:lang="nn">Hovudvising med romliste, pratevindauge og rominformasjon</caption>
|
||||
<caption xml:lang="pl">Główny widok z wykazem pokojów, rozmowami i szczegółami pokojów</caption>
|
||||
@@ -304,10 +371,12 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<caption xml:lang="fi">Kirjautumisnäkymä</caption>
|
||||
<caption xml:lang="fr">Écran de connexion</caption>
|
||||
<caption xml:lang="gl">Pantalla de identificación.</caption>
|
||||
<caption xml:lang="hu">Bejelentkező képernyő</caption>
|
||||
<caption xml:lang="ia">Schermo de accesso</caption>
|
||||
<caption xml:lang="it">Schermata di accesso</caption>
|
||||
<caption xml:lang="ka">შესვლის ეკრანი</caption>
|
||||
<caption xml:lang="ko">로그인 화면</caption>
|
||||
<caption xml:lang="lv">Ierakstīšanās logs</caption>
|
||||
<caption xml:lang="nl">Aanmeldscherm</caption>
|
||||
<caption xml:lang="nn">Innloggingsbilete</caption>
|
||||
<caption xml:lang="pl">Ekran logowania</caption>
|
||||
@@ -325,6 +394,7 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<content_attribute id="social-chat">intense</content_attribute>
|
||||
</content_rating>
|
||||
<releases>
|
||||
<release version="24.02.1" date="2024-03-21"/>
|
||||
<release version="24.02.0" date="2024-02-28">
|
||||
<url>https://kde.org/announcements/megarelease/6/#neochat</url>
|
||||
<description>
|
||||
@@ -490,4 +560,8 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<url>https://carlschwan.eu/2020/12/23/announcing-neochat-1.0-the-kde-matrix-client/</url>
|
||||
</release>
|
||||
</releases>
|
||||
<branding>
|
||||
<color type="primary" scheme_preference="light">#a6e4f3</color>
|
||||
<color type="primary" scheme_preference="dark">#235670</color>
|
||||
</branding>
|
||||
</component>
|
||||
|
||||
@@ -26,6 +26,7 @@ Name[it]=NeoChat
|
||||
Name[ka]=NeoChat
|
||||
Name[ko]=NeoChat
|
||||
Name[lt]=NeoChat
|
||||
Name[lv]=NeoChat
|
||||
Name[nl]=NeoChat
|
||||
Name[nn]=NeoChat
|
||||
Name[pa]=ਨਿਓ-ਚੈਟ
|
||||
@@ -66,6 +67,7 @@ GenericName[it]=Client Matrix
|
||||
GenericName[ka]=Matrix -ის კლიენტი
|
||||
GenericName[ko]=Matrix 클라이언트
|
||||
GenericName[lt]=Matrix kliento programa
|
||||
GenericName[lv]=„Matrix“ klients
|
||||
GenericName[nl]=Matrix-client
|
||||
GenericName[nn]=Matrix-klient
|
||||
GenericName[pa]=ਮੈਟਰਿਕਸ ਕਲਾਈਂਟ
|
||||
@@ -105,6 +107,7 @@ Comment[it]=Client per il protocollo Matrix
|
||||
Comment[ka]=კლიენტი Matrix-ის პროტოკოლისთვის
|
||||
Comment[ko]=Matrix 프로토콜용 클라이언트
|
||||
Comment[lt]=Matrix protokolo kliento programa
|
||||
Comment[lv]=Klients „Matrix“ protokolam
|
||||
Comment[nl]=Client voor het Matrix-protocol
|
||||
Comment[nn]=Klient for Matrix-protokollen
|
||||
Comment[pa]=ਮੈਟਰਿਕਸ ਪਰੋਟੋਕਾਲ ਲਈ ਕਲਾਈਂਟ ਹੈ
|
||||
|
||||
4297
po/ar/neochat.po
4297
po/ar/neochat.po
File diff suppressed because it is too large
Load Diff
4037
po/ast/neochat.po
4037
po/ast/neochat.po
File diff suppressed because it is too large
Load Diff
4450
po/az/neochat.po
4450
po/az/neochat.po
File diff suppressed because it is too large
Load Diff
4228
po/ca/neochat.po
4228
po/ca/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
4100
po/cs/neochat.po
4100
po/cs/neochat.po
File diff suppressed because it is too large
Load Diff
4197
po/da/neochat.po
4197
po/da/neochat.po
File diff suppressed because it is too large
Load Diff
4332
po/de/neochat.po
4332
po/de/neochat.po
File diff suppressed because it is too large
Load Diff
4360
po/el/neochat.po
4360
po/el/neochat.po
File diff suppressed because it is too large
Load Diff
4309
po/en_GB/neochat.po
4309
po/en_GB/neochat.po
File diff suppressed because it is too large
Load Diff
4141
po/eo/neochat.po
4141
po/eo/neochat.po
File diff suppressed because it is too large
Load Diff
4221
po/es/neochat.po
4221
po/es/neochat.po
File diff suppressed because it is too large
Load Diff
4179
po/eu/neochat.po
4179
po/eu/neochat.po
File diff suppressed because it is too large
Load Diff
4228
po/fi/neochat.po
4228
po/fi/neochat.po
File diff suppressed because it is too large
Load Diff
4236
po/fr/neochat.po
4236
po/fr/neochat.po
File diff suppressed because it is too large
Load Diff
5126
po/hu/neochat.po
5126
po/hu/neochat.po
File diff suppressed because it is too large
Load Diff
4193
po/ia/neochat.po
4193
po/ia/neochat.po
File diff suppressed because it is too large
Load Diff
4291
po/id/neochat.po
4291
po/id/neochat.po
File diff suppressed because it is too large
Load Diff
4355
po/ie/neochat.po
4355
po/ie/neochat.po
File diff suppressed because it is too large
Load Diff
4230
po/it/neochat.po
4230
po/it/neochat.po
File diff suppressed because it is too large
Load Diff
4032
po/ja/neochat.po
4032
po/ja/neochat.po
File diff suppressed because it is too large
Load Diff
4181
po/ka/neochat.po
4181
po/ka/neochat.po
File diff suppressed because it is too large
Load Diff
4507
po/ko/neochat.po
4507
po/ko/neochat.po
File diff suppressed because it is too large
Load Diff
4045
po/lt/neochat.po
4045
po/lt/neochat.po
File diff suppressed because it is too large
Load Diff
5166
po/lv/neochat.po
Normal file
5166
po/lv/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
4209
po/nl/neochat.po
4209
po/nl/neochat.po
File diff suppressed because it is too large
Load Diff
4256
po/nn/neochat.po
4256
po/nn/neochat.po
File diff suppressed because it is too large
Load Diff
4403
po/pa/neochat.po
4403
po/pa/neochat.po
File diff suppressed because it is too large
Load Diff
4249
po/pl/neochat.po
4249
po/pl/neochat.po
File diff suppressed because it is too large
Load Diff
4287
po/pt/neochat.po
4287
po/pt/neochat.po
File diff suppressed because it is too large
Load Diff
4489
po/pt_BR/neochat.po
4489
po/pt_BR/neochat.po
File diff suppressed because it is too large
Load Diff
4282
po/ru/neochat.po
4282
po/ru/neochat.po
File diff suppressed because it is too large
Load Diff
4410
po/sk/neochat.po
4410
po/sk/neochat.po
File diff suppressed because it is too large
Load Diff
4588
po/sl/neochat.po
4588
po/sl/neochat.po
File diff suppressed because it is too large
Load Diff
4267
po/sv/neochat.po
4267
po/sv/neochat.po
File diff suppressed because it is too large
Load Diff
4175
po/ta/neochat.po
4175
po/ta/neochat.po
File diff suppressed because it is too large
Load Diff
4159
po/tok/neochat.po
4159
po/tok/neochat.po
File diff suppressed because it is too large
Load Diff
4281
po/tr/neochat.po
4281
po/tr/neochat.po
File diff suppressed because it is too large
Load Diff
4219
po/uk/neochat.po
4219
po/uk/neochat.po
File diff suppressed because it is too large
Load Diff
4085
po/zh_CN/neochat.po
4085
po/zh_CN/neochat.po
File diff suppressed because it is too large
Load Diff
4175
po/zh_TW/neochat.po
4175
po/zh_TW/neochat.po
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,10 @@
|
||||
# SPDX-FileCopyrightText: 2020-2021 Tobias Fella <tobias.fella@kde.org>
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE AND NOT NEOCHAT_FLATPAK AND NOT NEOCHAT_APPIMAGE)
|
||||
add_subdirectory(purpose)
|
||||
endif()
|
||||
|
||||
add_library(neochat STATIC
|
||||
controller.cpp
|
||||
controller.h
|
||||
@@ -56,6 +60,8 @@ add_library(neochat STATIC
|
||||
notificationsmanager.h
|
||||
models/sortfilterroomlistmodel.cpp
|
||||
models/sortfilterroomlistmodel.h
|
||||
models/roomtreemodel.cpp
|
||||
models/roomtreemodel.h
|
||||
chatdocumenthandler.cpp
|
||||
chatdocumenthandler.h
|
||||
models/devicesmodel.cpp
|
||||
@@ -120,6 +126,7 @@ add_library(neochat STATIC
|
||||
events/pollevent.cpp
|
||||
pollhandler.cpp
|
||||
utils.h
|
||||
utils.cpp
|
||||
registration.cpp
|
||||
neochatconnection.cpp
|
||||
neochatconnection.h
|
||||
@@ -156,9 +163,25 @@ add_library(neochat STATIC
|
||||
enums/messagecomponenttype.h
|
||||
models/messagecontentmodel.cpp
|
||||
models/messagecontentmodel.h
|
||||
enums/neochatroomtype.h
|
||||
models/sortfilterroomtreemodel.cpp
|
||||
models/sortfilterroomtreemodel.h
|
||||
mediamanager.cpp
|
||||
mediamanager.h
|
||||
models/statekeysmodel.cpp
|
||||
models/statekeysmodel.h
|
||||
sharehandler.cpp
|
||||
sharehandler.h
|
||||
models/roomtreeitem.cpp
|
||||
models/roomtreeitem.h
|
||||
)
|
||||
|
||||
set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
|
||||
QT_QML_SINGLETON_TYPE TRUE
|
||||
)
|
||||
|
||||
qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
|
||||
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat
|
||||
QML_FILES
|
||||
qml/main.qml
|
||||
qml/AccountMenu.qml
|
||||
@@ -172,18 +195,12 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
|
||||
qml/UserInfo.qml
|
||||
qml/UserInfoDesktop.qml
|
||||
qml/RoomPage.qml
|
||||
qml/RoomWindow.qml
|
||||
qml/ExploreRoomsPage.qml
|
||||
qml/ManualRoomDialog.qml
|
||||
qml/ExplorerDelegate.qml
|
||||
qml/InviteUserPage.qml
|
||||
qml/ImageEditorPage.qml
|
||||
qml/WelcomePage.qml
|
||||
qml/General.qml
|
||||
qml/RoomSecurity.qml
|
||||
qml/PushNotification.qml
|
||||
qml/Categories.qml
|
||||
qml/Permissions.qml
|
||||
qml/NeochatMaximizeComponent.qml
|
||||
qml/FancyEffectsContainer.qml
|
||||
qml/TypingPane.qml
|
||||
@@ -195,22 +212,7 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
|
||||
qml/CompletionMenu.qml
|
||||
qml/PieProgressBar.qml
|
||||
qml/QuickFormatBar.qml
|
||||
qml/RoomData.qml
|
||||
qml/ServerData.qml
|
||||
qml/EmojiPicker.qml
|
||||
qml/TimelineDelegate.qml
|
||||
qml/ReplyComponent.qml
|
||||
qml/StateDelegate.qml
|
||||
qml/MessageDelegate.qml
|
||||
qml/Bubble.qml
|
||||
qml/SectionDelegate.qml
|
||||
qml/ReactionDelegate.qml
|
||||
qml/EventDelegate.qml
|
||||
qml/ReadMarkerDelegate.qml
|
||||
qml/MimeComponent.qml
|
||||
qml/StateComponent.qml
|
||||
qml/MessageEditComponent.qml
|
||||
qml/AvatarFlow.qml
|
||||
qml/LoginStep.qml
|
||||
qml/Login.qml
|
||||
qml/Homeserver.qml
|
||||
@@ -243,26 +245,6 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
|
||||
qml/FileDelegateContextMenu.qml
|
||||
qml/MessageSourceSheet.qml
|
||||
qml/ReportSheet.qml
|
||||
qml/SettingsPage.qml
|
||||
qml/ThemeRadioButton.qml
|
||||
qml/ColorScheme.qml
|
||||
qml/GeneralSettingsPage.qml
|
||||
qml/EmoticonsPage.qml
|
||||
qml/EmoticonEditorPage.qml
|
||||
qml/EmoticonFormCard.qml
|
||||
qml/GlobalNotificationsPage.qml
|
||||
qml/NotificationRuleItem.qml
|
||||
qml/AppearanceSettingsPage.qml
|
||||
qml/AccountsPage.qml
|
||||
qml/AccountEditorPage.qml
|
||||
qml/DevicesPage.qml
|
||||
qml/DeviceDelegate.qml
|
||||
qml/DevicesCard.qml
|
||||
qml/About.qml
|
||||
qml/AboutKDE.qml
|
||||
qml/SonnetConfigPage.qml
|
||||
qml/NetworkProxyPage.qml
|
||||
qml/DevtoolsPage.qml
|
||||
qml/ConfirmEncryptionDialog.qml
|
||||
qml/RemoveSheet.qml
|
||||
qml/BanSheet.qml
|
||||
@@ -286,40 +268,48 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
|
||||
qml/RoomInformation.qml
|
||||
qml/RoomMedia.qml
|
||||
qml/ChooseRoomDialog.qml
|
||||
qml/ShareAction.qml
|
||||
qml/SpaceHomePage.qml
|
||||
qml/SpaceHierarchyDelegate.qml
|
||||
qml/RemoveChildDialog.qml
|
||||
qml/SelectParentDialog.qml
|
||||
qml/Security.qml
|
||||
qml/QrCodeMaximizeComponent.qml
|
||||
qml/SelectSpacesDialog.qml
|
||||
qml/AttachDialog.qml
|
||||
qml/NotificationsView.qml
|
||||
qml/LoadingDelegate.qml
|
||||
qml/TimelineEndDelegate.qml
|
||||
qml/SearchPage.qml
|
||||
qml/ServerComboBox.qml
|
||||
qml/UserSearchPage.qml
|
||||
qml/ManualUserDialog.qml
|
||||
qml/MessageComponentChooser.qml
|
||||
qml/TextComponent.qml
|
||||
qml/ImageComponent.qml
|
||||
qml/VideoComponent.qml
|
||||
qml/AudioComponent.qml
|
||||
qml/EncryptedComponent.qml
|
||||
qml/FileComponent.qml
|
||||
qml/LocationComponent.qml
|
||||
qml/LiveLocationComponent.qml
|
||||
qml/PollComponent.qml
|
||||
qml/LinkPreviewComponent.qml
|
||||
qml/LoadComponent.qml
|
||||
qml/RecommendedSpaceDialog.qml
|
||||
qml/RoomTreeSection.qml
|
||||
qml/DelegateContextMenu.qml
|
||||
qml/ShareDialog.qml
|
||||
qml/UnlockSSSSDialog.qml
|
||||
qml/QrScannerPage.qml
|
||||
qml/JoinRoomDialog.qml
|
||||
qml/ConfirmUrlDialog.qml
|
||||
qml/AccountSwitchDialog.qml
|
||||
qml/ConfirmLeaveDialog.qml
|
||||
qml/CodeMaximizeComponent.qml
|
||||
RESOURCES
|
||||
qml/confetti.png
|
||||
qml/glowdot.png
|
||||
)
|
||||
|
||||
add_subdirectory(settings)
|
||||
add_subdirectory(timeline)
|
||||
add_subdirectory(devtools)
|
||||
|
||||
if(UNIX)
|
||||
qt_target_qml_sources(neochat QML_FILES qml/ShareAction.qml)
|
||||
else()
|
||||
set_source_files_properties(qml/ShareActionStub.qml PROPERTIES
|
||||
QT_RESOURCE_ALIAS qml/ShareAction.qml
|
||||
)
|
||||
qt_target_qml_sources(neochat QML_FILES qml/ShareActionStub.qml)
|
||||
endif()
|
||||
|
||||
|
||||
configure_file(config-neochat.h.in ${CMAKE_CURRENT_BINARY_DIR}/config-neochat.h)
|
||||
|
||||
if(WIN32)
|
||||
@@ -393,9 +383,14 @@ if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
|
||||
target_compile_definitions(neochat PUBLIC -DHAVE_RUNNER)
|
||||
target_compile_definitions(neochat PUBLIC -DHAVE_X11)
|
||||
target_sources(neochat PRIVATE runner.cpp)
|
||||
|
||||
if (TARGET KUnifiedPush)
|
||||
target_sources(neochat PRIVATE fakerunner.cpp)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/models ${CMAKE_CURRENT_SOURCE_DIR}/enums)
|
||||
target_link_libraries(neochat PRIVATE settingsplugin timelineplugin devtoolsplugin)
|
||||
target_link_libraries(neochat PUBLIC
|
||||
Qt::Core
|
||||
Qt::Quick
|
||||
@@ -521,7 +516,7 @@ if(ANDROID)
|
||||
)
|
||||
ecm_add_android_apk(neochat-app ANDROID_DIR ${CMAKE_SOURCE_DIR}/android)
|
||||
else()
|
||||
target_link_libraries(neochat PUBLIC Qt::Widgets KF6::KIOWidgets)
|
||||
target_link_libraries(neochat PUBLIC Qt::Widgets KF6::KIOWidgets KF6::SyntaxHighlighting)
|
||||
install(FILES neochat.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFYRCDIR})
|
||||
endif()
|
||||
|
||||
@@ -529,7 +524,7 @@ if(NOT ANDROID)
|
||||
set_target_properties(neochat-app PROPERTIES OUTPUT_NAME "neochat")
|
||||
endif()
|
||||
|
||||
if(TARGET KF6::DBusAddons)
|
||||
if(TARGET KF6::DBusAddons AND NOT WIN32)
|
||||
target_link_libraries(neochat PUBLIC KF6::DBusAddons)
|
||||
target_compile_definitions(neochat PUBLIC -DHAVE_KDBUSADDONS)
|
||||
endif()
|
||||
|
||||
@@ -80,6 +80,7 @@ QString ActionsHandler::handleMentions(QString handledText, QList<Mention> *ment
|
||||
|
||||
void ActionsHandler::handleMessage(const QString &text, QString handledText, ChatBarCache *chatBarCache)
|
||||
{
|
||||
Q_ASSERT(m_room);
|
||||
if (NeoChatConfig::allowQuickEdit()) {
|
||||
QRegularExpression sed(QStringLiteral("^s/([^/]*)/([^/]*)(/g)?$"));
|
||||
auto match = sed.match(text);
|
||||
|
||||
@@ -58,7 +58,7 @@ public Q_SLOTS:
|
||||
void handleMessageEvent(ChatBarCache *chatBarCache);
|
||||
|
||||
private:
|
||||
NeoChatRoom *m_room = nullptr;
|
||||
QPointer<NeoChatRoom> m_room;
|
||||
void checkEffects(const QString &text);
|
||||
|
||||
QString handleMentions(QString handledText, QList<Mention> *mentions);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "chatbarcache.h"
|
||||
|
||||
#include "chatdocumenthandler.h"
|
||||
#include "eventhandler.h"
|
||||
#include "neochatroom.h"
|
||||
|
||||
@@ -117,7 +118,7 @@ QString ChatBarCache::relationMessage() const
|
||||
|
||||
if (auto event = room->findInTimeline(m_relationId); event != room->historyEdge()) {
|
||||
EventHandler eventhandler(room, &**event);
|
||||
return eventhandler.getPlainBody();
|
||||
return eventhandler.getMarkdownBody();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -163,6 +164,54 @@ QList<Mention> *ChatBarCache::mentions()
|
||||
return &m_mentions;
|
||||
}
|
||||
|
||||
void ChatBarCache::updateMentions(QQuickTextDocument *document, ChatDocumentHandler *documentHandler)
|
||||
{
|
||||
documentHandler->setDocument(document);
|
||||
|
||||
if (parent() == nullptr) {
|
||||
qWarning() << "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.";
|
||||
return;
|
||||
}
|
||||
if (m_relationId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
auto room = dynamic_cast<NeoChatRoom *>(parent());
|
||||
if (room == nullptr) {
|
||||
qWarning() << "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto event = room->findInTimeline(m_relationId); event != room->historyEdge()) {
|
||||
if (const auto &roomMessageEvent = &*event->viewAs<Quotient::RoomMessageEvent>()) {
|
||||
// Replaces the mentions that are baked into the HTML but plaintext in the original markdown
|
||||
const QRegularExpression re(QStringLiteral(R"lit(<a\shref="https:\/\/matrix.to\/#\/([\S]*)"\s?>([\S]*)<\/a>)lit"));
|
||||
|
||||
m_mentions.clear();
|
||||
|
||||
int linkSize = 0;
|
||||
auto matches = re.globalMatch(EventHandler::rawMessageBody(*roomMessageEvent));
|
||||
while (matches.hasNext()) {
|
||||
const QRegularExpressionMatch match = matches.next();
|
||||
if (match.hasMatch()) {
|
||||
const QString id = match.captured(1);
|
||||
const QString name = match.captured(2);
|
||||
|
||||
const int position = match.capturedStart(0) - linkSize;
|
||||
const int end = position + name.length();
|
||||
linkSize += match.capturedLength(0) - name.length();
|
||||
|
||||
QTextCursor cursor(documentHandler->document()->textDocument());
|
||||
cursor.setPosition(position);
|
||||
cursor.setPosition(end, QTextCursor::KeepAnchor);
|
||||
cursor.setKeepPositionOnInsert(true);
|
||||
|
||||
m_mentions.push_back(Mention{.cursor = cursor, .text = name, .start = position, .position = end, .id = id});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString ChatBarCache::savedText() const
|
||||
{
|
||||
return m_savedText;
|
||||
|
||||
@@ -5,8 +5,11 @@
|
||||
|
||||
#include <QObject>
|
||||
#include <QQmlEngine>
|
||||
#include <QQuickTextDocument>
|
||||
#include <QTextCursor>
|
||||
|
||||
class ChatDocumentHandler;
|
||||
|
||||
/**
|
||||
* @brief Defines a user mention in the current chat or edit text.
|
||||
*/
|
||||
@@ -174,6 +177,11 @@ public:
|
||||
*/
|
||||
QList<Mention> *mentions();
|
||||
|
||||
/**
|
||||
* @brief Update the mentions in @p document when editing a message.
|
||||
*/
|
||||
Q_INVOKABLE void updateMentions(QQuickTextDocument *document, ChatDocumentHandler *documentHandler);
|
||||
|
||||
/**
|
||||
* @brief Get the saved chat bar text.
|
||||
*/
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <Quotient/qt_connection_util.h>
|
||||
|
||||
#include "neochatconfig.h"
|
||||
#include "neochatconnection.h"
|
||||
#include "neochatroom.h"
|
||||
#include "notificationsmanager.h"
|
||||
#include "proxycontroller.h"
|
||||
@@ -37,6 +38,14 @@
|
||||
#include "trayicon_sni.h"
|
||||
#endif
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
|
||||
#ifndef Q_OS_ANDROID
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusInterface>
|
||||
#include <QDBusMessage>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
bool testMode = false;
|
||||
|
||||
using namespace Quotient;
|
||||
@@ -102,7 +111,9 @@ Controller::Controller(QObject *parent)
|
||||
NotificationsManager::instance().handleNotifications(connection);
|
||||
});
|
||||
connectSingleShot(connection, &NeoChatConnection::syncDone, this, [this, connection] {
|
||||
connection->setupPushNotifications(m_endpoint);
|
||||
if (!m_endpoint.isEmpty()) {
|
||||
connection->setupPushNotifications(m_endpoint);
|
||||
}
|
||||
});
|
||||
}
|
||||
oldAccountCount = m_accountRegistry.size();
|
||||
@@ -118,7 +129,9 @@ Controller::Controller(QObject *parent)
|
||||
}
|
||||
});
|
||||
|
||||
connector->registerClient(i18n("Receiving push notifications"));
|
||||
connector->registerClient(
|
||||
i18nc("The reason for using push notifications, as in: '[Push notifications are used for] Receiving notifications for new messages'",
|
||||
"Receiving notifications for new messages"));
|
||||
|
||||
m_endpoint = connector->endpoint();
|
||||
#endif
|
||||
@@ -143,8 +156,18 @@ void Controller::addConnection(NeoChatConnection *c)
|
||||
c->saveState();
|
||||
});
|
||||
connect(c, &NeoChatConnection::loggedOut, this, [this, c] {
|
||||
if (accounts().count() > 1) {
|
||||
// Only set the connection if the the account being logged out is currently active
|
||||
if (c == activeConnection()) {
|
||||
setActiveConnection(dynamic_cast<NeoChatConnection *>(accounts().accounts()[0]));
|
||||
}
|
||||
} else {
|
||||
setActiveConnection(nullptr);
|
||||
}
|
||||
|
||||
dropConnection(c);
|
||||
});
|
||||
connect(c, &NeoChatConnection::badgeNotificationCountChanged, this, &Controller::updateBadgeNotificationCount);
|
||||
|
||||
c->sync();
|
||||
|
||||
@@ -287,8 +310,15 @@ void Controller::setActiveConnection(NeoChatConnection *connection)
|
||||
if (connection == m_connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_connection = connection;
|
||||
Q_EMIT activeConnectionChanged();
|
||||
|
||||
if (m_connection != nullptr) {
|
||||
m_connection->refreshBadgeNotificationCount();
|
||||
updateBadgeNotificationCount(m_connection, m_connection->badgeNotificationCount());
|
||||
}
|
||||
|
||||
Q_EMIT activeConnectionChanged(m_connection);
|
||||
}
|
||||
|
||||
void Controller::listenForNotifications()
|
||||
@@ -312,6 +342,36 @@ void Controller::listenForNotifications()
|
||||
#endif
|
||||
}
|
||||
|
||||
void Controller::updateBadgeNotificationCount(NeoChatConnection *connection, int count)
|
||||
{
|
||||
if (connection == m_connection) {
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
|
||||
#ifndef Q_OS_ANDROID
|
||||
// copied from Telegram desktop
|
||||
const auto launcherUrl = "application://org.kde.neochat.desktop"_ls;
|
||||
// Gnome requires that count is a 64bit integer
|
||||
const qint64 counterSlice = std::min(count, 9999);
|
||||
QVariantMap dbusUnityProperties;
|
||||
|
||||
if (counterSlice > 0) {
|
||||
dbusUnityProperties["count"_ls] = counterSlice;
|
||||
dbusUnityProperties["count-visible"_ls] = true;
|
||||
} else {
|
||||
dbusUnityProperties["count-visible"_ls] = false;
|
||||
}
|
||||
|
||||
auto signal = QDBusMessage::createSignal("/com/canonical/unity/launcherentry/neochat"_ls, "com.canonical.Unity.LauncherEntry"_ls, "Update"_ls);
|
||||
|
||||
signal.setArguments({launcherUrl, dbusUnityProperties});
|
||||
|
||||
QDBusConnection::sessionBus().send(signal);
|
||||
#endif // Q_OS_ANDROID
|
||||
#else
|
||||
qGuiApp->setBadgeNumber(count);
|
||||
#endif // QT_VERSION_CHECK(6, 6, 0)
|
||||
}
|
||||
}
|
||||
|
||||
bool Controller::isFlatpak() const
|
||||
{
|
||||
#ifdef NEOCHAT_FLATPAK
|
||||
@@ -326,6 +386,14 @@ AccountRegistry &Controller::accounts()
|
||||
return m_accountRegistry;
|
||||
}
|
||||
|
||||
QString Controller::loadFileContent(const QString &path) const
|
||||
{
|
||||
QUrl url(path);
|
||||
QFile file(url.isLocalFile() ? url.toLocalFile() : url.toString());
|
||||
file.open(QFile::ReadOnly);
|
||||
return QString::fromLatin1(file.readAll());
|
||||
}
|
||||
|
||||
#include "moc_controller.cpp"
|
||||
|
||||
void Controller::setTestMode(bool test)
|
||||
@@ -342,3 +410,12 @@ void Controller::removeConnection(const QString &userId)
|
||||
SettingsGroup("Accounts"_ls).remove(userId);
|
||||
}
|
||||
}
|
||||
|
||||
bool Controller::ssssSupported() const
|
||||
{
|
||||
#if __has_include("Quotient/e2ee/sssshandler.h")
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -56,6 +56,8 @@ class Controller : public QObject
|
||||
|
||||
Q_PROPERTY(QStringList accountsLoading MEMBER m_accountsLoading NOTIFY accountsLoadingChanged)
|
||||
|
||||
Q_PROPERTY(bool ssssSupported READ ssssSupported CONSTANT)
|
||||
|
||||
public:
|
||||
static Controller &instance();
|
||||
static Controller *create(QQmlEngine *engine, QJSEngine *)
|
||||
@@ -92,12 +94,16 @@ public:
|
||||
*/
|
||||
static void listenForNotifications();
|
||||
|
||||
Q_INVOKABLE QString loadFileContent(const QString &path) const;
|
||||
|
||||
Quotient::AccountRegistry &accounts();
|
||||
|
||||
static void setTestMode(bool testMode);
|
||||
|
||||
Q_INVOKABLE void removeConnection(const QString &userId);
|
||||
|
||||
bool ssssSupported() const;
|
||||
|
||||
private:
|
||||
explicit Controller(QObject *parent = nullptr);
|
||||
|
||||
@@ -117,11 +123,12 @@ private:
|
||||
private Q_SLOTS:
|
||||
void invokeLogin();
|
||||
void setQuitOnLastWindowClosed();
|
||||
void updateBadgeNotificationCount(NeoChatConnection *connection, int count);
|
||||
|
||||
Q_SIGNALS:
|
||||
void errorOccured(const QString &error, const QString &detail);
|
||||
void connectionAdded(NeoChatConnection *connection);
|
||||
void connectionDropped(NeoChatConnection *connection);
|
||||
void activeConnectionChanged();
|
||||
void activeConnectionChanged(NeoChatConnection *connection);
|
||||
void accountsLoadingChanged();
|
||||
};
|
||||
|
||||
35
src/devtools/AccountData.qml
Normal file
35
src/devtools/AccountData.qml
Normal file
@@ -0,0 +1,35 @@
|
||||
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property NeoChatConnection connection
|
||||
|
||||
FormCard.FormHeader {
|
||||
title: i18nc("@title:group", "Account Data")
|
||||
}
|
||||
FormCard.FormCard {
|
||||
Repeater {
|
||||
model: root.connection.accountDataEventTypes
|
||||
delegate: FormCard.FormButtonDelegate {
|
||||
text: modelData
|
||||
onClicked: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet.qml'), {
|
||||
sourceText: root.connection.accountDataJsonString(modelData)
|
||||
}, {
|
||||
title: i18nc("@title:window", "Event Source"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/devtools/CMakeLists.txt
Normal file
15
src/devtools/CMakeLists.txt
Normal file
@@ -0,0 +1,15 @@
|
||||
# SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
qt_add_library(devtools STATIC)
|
||||
qt_add_qml_module(devtools
|
||||
URI org.kde.neochat.devtools
|
||||
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat/devtools
|
||||
QML_FILES
|
||||
DevtoolsPage.qml
|
||||
AccountData.qml
|
||||
FeatureFlagPage.qml
|
||||
RoomData.qml
|
||||
ServerData.qml
|
||||
StateKeys.qml
|
||||
)
|
||||
@@ -23,11 +23,27 @@ FormCard.FormCardPage {
|
||||
header: QQC2.TabBar {
|
||||
id: tabBar
|
||||
|
||||
readonly property real tabWidth: tabBar.width / tabBar.count
|
||||
|
||||
QQC2.TabButton {
|
||||
text: qsTr("Room Data")
|
||||
|
||||
implicitWidth: tabBar.tabWidth
|
||||
}
|
||||
QQC2.TabButton {
|
||||
text: qsTr("Server Info")
|
||||
|
||||
implicitWidth: tabBar.tabWidth
|
||||
}
|
||||
QQC2.TabButton {
|
||||
text: i18nc("@title:tab", "Account Data")
|
||||
|
||||
implicitWidth: tabBar.tabWidth
|
||||
}
|
||||
QQC2.TabButton {
|
||||
text: i18nc("@title:tab", "Feature Flags")
|
||||
|
||||
implicitWidth: tabBar.tabWidth
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,5 +59,9 @@ FormCard.FormCardPage {
|
||||
ServerData {
|
||||
connection: root.connection
|
||||
}
|
||||
AccountData {
|
||||
connection: root.connection
|
||||
}
|
||||
FeatureFlagPage {}
|
||||
}
|
||||
}
|
||||
32
src/devtools/FeatureFlagPage.qml
Normal file
32
src/devtools/FeatureFlagPage.qml
Normal file
@@ -0,0 +1,32 @@
|
||||
// SPDX-FileCopyrightText: 2024 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
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
|
||||
import org.kde.neochat.config
|
||||
|
||||
FormCard.FormCardPage {
|
||||
id: root
|
||||
|
||||
FormCard.FormCard {
|
||||
Layout.topMargin: Kirigami.Units.largeSpacing
|
||||
|
||||
FormCard.FormCheckDelegate {
|
||||
id: roomAccountDataVisibleCheck
|
||||
text: i18nc("@option:check Enable the matrix 'threads' feature", "Threads")
|
||||
checked: Config.threads
|
||||
|
||||
onToggled: Config.threads = checked
|
||||
}
|
||||
FormCard.FormCheckDelegate {
|
||||
text: i18nc("@option:check Enable the matrix 'secret backup' feature", "Secret Backup")
|
||||
checked: Config.secretBackup
|
||||
|
||||
onToggled: Config.secretBackup = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
99
src/devtools/RoomData.qml
Normal file
99
src/devtools/RoomData.qml
Normal file
@@ -0,0 +1,99 @@
|
||||
// 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
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
import org.kde.kitemmodels
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property NeoChatRoom room
|
||||
required property NeoChatConnection connection
|
||||
|
||||
FormCard.FormHeader {
|
||||
title: i18nc("@title", "Choose Room")
|
||||
}
|
||||
FormCard.FormCard {
|
||||
FormCard.FormComboBoxDelegate {
|
||||
id: roomComboBox
|
||||
text: i18n("Room")
|
||||
textRole: "escapedDisplayName"
|
||||
valueRole: "roomId"
|
||||
displayText: RoomManager.roomListModel.data(RoomManager.roomListModel.index(currentIndex, 0), RoomListModel.DisplayNameRole)
|
||||
model: RoomManager.roomListModel
|
||||
currentIndex: 0
|
||||
Component.onCompleted: currentIndex = RoomManager.roomListModel.rowForRoom(root.room)
|
||||
onCurrentValueChanged: root.room = RoomManager.roomListModel.roomByAliasOrId(roomComboBox.currentValue)
|
||||
}
|
||||
FormCard.FormTextDelegate {
|
||||
text: i18n("Room Id: %1", root.room.id)
|
||||
}
|
||||
}
|
||||
FormCard.FormHeader {
|
||||
title: i18n("Room Account Data")
|
||||
visible: roomAccountData.count > 0
|
||||
}
|
||||
FormCard.FormCard {
|
||||
visible: roomAccountData.count > 0
|
||||
Repeater {
|
||||
id: roomAccountData
|
||||
model: root.room.accountDataEventTypes
|
||||
delegate: FormCard.FormButtonDelegate {
|
||||
text: modelData
|
||||
onClicked: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet.qml'), {
|
||||
sourceText: root.room.roomAcountDataJson(text)
|
||||
}, {
|
||||
title: i18n("Event Source"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
FormCard.FormHeader {
|
||||
id: stateEventListHeader
|
||||
title: i18n("Room State")
|
||||
visible: roomState.count > 0
|
||||
}
|
||||
FormCard.FormCard {
|
||||
visible: roomState.count > 0
|
||||
Repeater {
|
||||
id: roomState
|
||||
model: StateModel {
|
||||
id: stateModel
|
||||
room: root.room
|
||||
}
|
||||
|
||||
delegate: FormCard.FormButtonDelegate {
|
||||
text: model.type
|
||||
description: i18ncp("'Event' being some JSON data, not something physically happening.", "%1 event of this type", "%1 events of this type", model.eventCount)
|
||||
onClicked: {
|
||||
if (model.eventCount === 1) {
|
||||
onClicked: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet.qml'), {
|
||||
sourceText: stateModel.stateEventJson(stateModel.index(model.index, 0))
|
||||
}, {
|
||||
title: i18n("Event Source"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
})
|
||||
} else {
|
||||
pageStack.pushDialogLayer(stateKeysComponent, {
|
||||
room: root.room,
|
||||
eventType: model.type
|
||||
}, {
|
||||
title: i18nc("'Event' being some JSON data, not something physically happening.", "Event Information")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: stateKeysComponent
|
||||
StateKeys {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ ColumnLayout {
|
||||
}
|
||||
FormCard.FormCard {
|
||||
Repeater {
|
||||
model: room.connection.getSupportedRoomVersions()
|
||||
model: root.connection.getSupportedRoomVersions()
|
||||
|
||||
delegate: FormCard.FormTextDelegate {
|
||||
text: modelData.id
|
||||
43
src/devtools/StateKeys.qml
Normal file
43
src/devtools/StateKeys.qml
Normal file
@@ -0,0 +1,43 @@
|
||||
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
import org.kde.kitemmodels
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
FormCard.FormCardPage {
|
||||
id: root
|
||||
|
||||
required property NeoChatRoom room
|
||||
required property string eventType
|
||||
|
||||
title: root.eventType
|
||||
|
||||
FormCard.FormHeader {
|
||||
title: i18nc("The name of one instance of a state of configuration. Unless you really know what you're doing, best leave this untranslated.", "State Keys")
|
||||
}
|
||||
FormCard.FormCard {
|
||||
Repeater {
|
||||
model: StateKeysModel {
|
||||
id: stateKeysModel
|
||||
room: root.room
|
||||
eventType: root.eventType
|
||||
}
|
||||
|
||||
delegate: FormCard.FormButtonDelegate {
|
||||
text: model.stateKey
|
||||
onClicked: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet.qml'), {
|
||||
sourceText: stateKeysModel.stateEventJson(stateKeysModel.index(model.index, 0))
|
||||
}, {
|
||||
title: i18nc("@title:window", "Event Source"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,11 @@ public:
|
||||
Image, /**< A message that is an image. */
|
||||
Audio, /**< A message that is an audio recording. */
|
||||
Video, /**< A message that is a video. */
|
||||
Code, /**< A code section. */
|
||||
Quote, /**< A quote section. */
|
||||
File, /**< A message that is a file. */
|
||||
Itinerary, /**< A preview for a file that can integrate with KDE itinerary. */
|
||||
Pdf, /**< A preview for a PDF file. */
|
||||
Poll, /**< The initial event for a poll. */
|
||||
Location, /**< A location event. */
|
||||
LiveLocation, /**< The initial event of a shared live location (i.e., the place where this is supposed to be shown in the timeline). */
|
||||
@@ -47,6 +51,7 @@ public:
|
||||
LinkPreview, /**< A preview of a URL in the message. */
|
||||
LinkPreviewLoad, /**< A loading dialog for a link preview. */
|
||||
Edit, /**< A text edit for editing a message. */
|
||||
Verification, /**< A user verification session start message. */
|
||||
Other, /**< Anything that cannot be classified as another type. */
|
||||
};
|
||||
Q_ENUM(Type);
|
||||
@@ -104,4 +109,22 @@ public:
|
||||
|
||||
return MessageComponentType::Other;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Return MessageComponentType for the given html tag.
|
||||
*
|
||||
* @param tag the tag name to return a type for.
|
||||
*
|
||||
* @sa Type
|
||||
*/
|
||||
static Type typeForTag(const QString &tag)
|
||||
{
|
||||
if (tag == QLatin1String("pre") || tag == QLatin1String("pre")) {
|
||||
return Code;
|
||||
}
|
||||
if (tag == QLatin1String("blockquote")) {
|
||||
return Quote;
|
||||
}
|
||||
return Text;
|
||||
}
|
||||
};
|
||||
|
||||
98
src/enums/neochatroomtype.h
Normal file
98
src/enums/neochatroomtype.h
Normal file
@@ -0,0 +1,98 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "neochatroom.h"
|
||||
#include <Quotient/quotient_common.h>
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
class NeoChatRoomType : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("")
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the room list categories a room can be assigned.
|
||||
*/
|
||||
enum Types {
|
||||
Search = 0, /**< So we can show a search delegate if needed, e.g. collapsed mode. */
|
||||
Invited, /**< The user has been invited to the room. */
|
||||
Favorite, /**< The room is set as a favourite. */
|
||||
Direct, /**< The room is a direct chat. */
|
||||
Normal, /**< The default category for a joined room. */
|
||||
Deprioritized, /**< The room is set as low priority. */
|
||||
Space, /**< The room is a space. */
|
||||
AddDirect, /**< So we can show the add friend delegate. */
|
||||
TypesCount, /**< Number of different types (this should always be last). */
|
||||
};
|
||||
Q_ENUM(Types);
|
||||
|
||||
static NeoChatRoomType::Types typeForRoom(const NeoChatRoom *room)
|
||||
{
|
||||
if (room->isSpace()) {
|
||||
return NeoChatRoomType::Space;
|
||||
}
|
||||
if (room->joinState() == Quotient::JoinState::Invite) {
|
||||
return NeoChatRoomType::Invited;
|
||||
}
|
||||
if (room->isFavourite()) {
|
||||
return NeoChatRoomType::Favorite;
|
||||
}
|
||||
if (room->isLowPriority()) {
|
||||
return NeoChatRoomType::Deprioritized;
|
||||
}
|
||||
if (room->isDirectChat()) {
|
||||
return NeoChatRoomType::Direct;
|
||||
}
|
||||
return NeoChatRoomType::Normal;
|
||||
}
|
||||
|
||||
static QString typeName(int category)
|
||||
{
|
||||
switch (category) {
|
||||
case NeoChatRoomType::Invited:
|
||||
return i18n("Invited");
|
||||
case NeoChatRoomType::Favorite:
|
||||
return i18n("Favorite");
|
||||
case NeoChatRoomType::Direct:
|
||||
return i18n("Friends");
|
||||
case NeoChatRoomType::Normal:
|
||||
return i18n("Normal");
|
||||
case NeoChatRoomType::Deprioritized:
|
||||
return i18n("Low priority");
|
||||
case NeoChatRoomType::Space:
|
||||
return i18n("Spaces");
|
||||
case NeoChatRoomType::Search:
|
||||
return i18n("Search");
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
static QString typeIconName(int category)
|
||||
{
|
||||
switch (category) {
|
||||
case NeoChatRoomType::Invited:
|
||||
return QStringLiteral("user-invisible");
|
||||
case NeoChatRoomType::Favorite:
|
||||
return QStringLiteral("favorite");
|
||||
case NeoChatRoomType::Direct:
|
||||
return QStringLiteral("dialog-messages");
|
||||
case NeoChatRoomType::Normal:
|
||||
return QStringLiteral("group");
|
||||
case NeoChatRoomType::Deprioritized:
|
||||
return QStringLiteral("object-order-lower");
|
||||
case NeoChatRoomType::Space:
|
||||
return QStringLiteral("group");
|
||||
case NeoChatRoomType::Search:
|
||||
return QStringLiteral("search");
|
||||
default:
|
||||
return QStringLiteral("tools-report-bug");
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -232,6 +232,36 @@ bool EventHandler::isHidden()
|
||||
return false;
|
||||
}
|
||||
|
||||
Qt::TextFormat EventHandler::messageBodyInputFormat(const Quotient::RoomMessageEvent &event)
|
||||
{
|
||||
if (event.mimeType().name() == "text/plain"_ls) {
|
||||
return Qt::PlainText;
|
||||
} else {
|
||||
return Qt::RichText;
|
||||
}
|
||||
}
|
||||
|
||||
QString EventHandler::rawMessageBody(const Quotient::RoomMessageEvent &event)
|
||||
{
|
||||
if (event.hasFileContent()) {
|
||||
auto fileCaption = event.content()->fileInfo()->originalName;
|
||||
if (fileCaption.isEmpty()) {
|
||||
fileCaption = event.plainBody();
|
||||
} else if (event.content()->fileInfo()->originalName != event.plainBody()) {
|
||||
fileCaption = event.plainBody() + " | "_ls + fileCaption;
|
||||
}
|
||||
return fileCaption;
|
||||
}
|
||||
|
||||
QString body;
|
||||
if (event.hasTextContent() && event.content()) {
|
||||
body = static_cast<const MessageEventContent::TextContent *>(event.content())->body;
|
||||
} else {
|
||||
body = event.plainBody();
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
QString EventHandler::getRichBody(bool stripNewlines) const
|
||||
{
|
||||
if (m_event == nullptr) {
|
||||
@@ -250,6 +280,22 @@ QString EventHandler::getPlainBody(bool stripNewlines) const
|
||||
return getBody(m_event, Qt::PlainText, stripNewlines);
|
||||
}
|
||||
|
||||
QString EventHandler::getMarkdownBody() const
|
||||
{
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getMarkdownBody called with m_event set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!m_event->is<RoomMessageEvent>()) {
|
||||
qCWarning(EventHandling) << "getMarkdownBody called when m_event isn't a RoomMessageEvent.";
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto roomMessageEvent = eventCast<const RoomMessageEvent>(m_event);
|
||||
return roomMessageEvent->plainBody();
|
||||
}
|
||||
|
||||
QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat format, bool stripNewlines) const
|
||||
{
|
||||
if (event->isRedacted()) {
|
||||
@@ -445,7 +491,7 @@ QString EventHandler::getMessageBody(const RoomMessageEvent &event, Qt::TextForm
|
||||
}
|
||||
|
||||
if (format == Qt::RichText) {
|
||||
return textHandler.handleRecieveRichText(inputFormat, m_room, &event, stripNewlines);
|
||||
return textHandler.handleRecieveRichText(inputFormat, m_room, &event, stripNewlines, event.isReplaced());
|
||||
} else {
|
||||
return textHandler.handleRecievePlainText(inputFormat, stripNewlines);
|
||||
}
|
||||
@@ -610,6 +656,7 @@ QVariantMap EventHandler::getMediaInfoForEvent(const Quotient::RoomEvent *event)
|
||||
|
||||
// Get the file info for the event.
|
||||
const EventContent::FileInfo *fileInfo;
|
||||
bool isSticker = false;
|
||||
if (event->is<RoomMessageEvent>()) {
|
||||
auto roomMessageEvent = eventCast<const RoomMessageEvent>(event);
|
||||
if (!roomMessageEvent->hasFileContent()) {
|
||||
@@ -619,14 +666,15 @@ QVariantMap EventHandler::getMediaInfoForEvent(const Quotient::RoomEvent *event)
|
||||
} else if (event->is<StickerEvent>()) {
|
||||
auto stickerEvent = eventCast<const StickerEvent>(event);
|
||||
fileInfo = &stickerEvent->image();
|
||||
isSticker = true;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
|
||||
return getMediaInfoFromFileInfo(fileInfo, eventId);
|
||||
return getMediaInfoFromFileInfo(fileInfo, eventId, false, isSticker);
|
||||
}
|
||||
|
||||
QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail) const
|
||||
QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail, bool isSticker) const
|
||||
{
|
||||
QVariantMap mediaInfo;
|
||||
|
||||
@@ -653,6 +701,8 @@ QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo
|
||||
// Add media size if available.
|
||||
mediaInfo["size"_ls] = fileInfo->payloadSize;
|
||||
|
||||
mediaInfo["isSticker"_ls] = isSticker;
|
||||
|
||||
// Add parameter depending on media type.
|
||||
if (mimeType.name().contains(QStringLiteral("image"))) {
|
||||
if (auto castInfo = static_cast<const EventContent::ImageContent *>(fileInfo)) {
|
||||
|
||||
@@ -137,6 +137,22 @@ public:
|
||||
*/
|
||||
bool isHidden();
|
||||
|
||||
/**
|
||||
* @brief The input format of the body in the message.
|
||||
*
|
||||
* I.e. if the message has only a body the format will be Qt::PlainText, if it
|
||||
* has a formatted body it will be Qt::RichText.
|
||||
*/
|
||||
static Qt::TextFormat messageBodyInputFormat(const Quotient::RoomMessageEvent &event);
|
||||
|
||||
/**
|
||||
* @brief Output a string for the room message content without any formatting.
|
||||
*
|
||||
* This is the content of the formatted_body key if present or the body key if
|
||||
* not.
|
||||
*/
|
||||
static QString rawMessageBody(const Quotient::RoomMessageEvent &event);
|
||||
|
||||
/**
|
||||
* @brief Output a string for the message content ready for display in a rich text field.
|
||||
*
|
||||
@@ -169,6 +185,13 @@ public:
|
||||
*/
|
||||
QString getPlainBody(bool stripNewlines = false) const;
|
||||
|
||||
/**
|
||||
* @brief Output the original body for the message content, useful for editing the original message.
|
||||
*
|
||||
* The event type must be a room message event.
|
||||
*/
|
||||
QString getMarkdownBody() const;
|
||||
|
||||
/**
|
||||
* @brief Output a generic string for the message content ready for display.
|
||||
*
|
||||
@@ -206,6 +229,7 @@ public:
|
||||
* - width - The width in pixels of the audio media.
|
||||
* - height - The height in pixels of the audio media.
|
||||
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
|
||||
* - isSticker - Whether the image is a sticker or not
|
||||
*/
|
||||
QVariantMap getMediaInfo() const;
|
||||
|
||||
@@ -297,6 +321,7 @@ public:
|
||||
* - width - The width in pixels of the audio media.
|
||||
* - height - The height in pixels of the audio media.
|
||||
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
|
||||
* - isSticker - Whether the image is a sticker or not
|
||||
*/
|
||||
QVariantMap getReplyMediaInfo() const;
|
||||
|
||||
@@ -382,5 +407,6 @@ private:
|
||||
QString getMessageBody(const Quotient::RoomMessageEvent &event, Qt::TextFormat format, bool stripNewlines) const;
|
||||
|
||||
QVariantMap getMediaInfoForEvent(const Quotient::RoomEvent *event) const;
|
||||
QVariantMap getMediaInfoFromFileInfo(const Quotient::EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail = false) const;
|
||||
QVariantMap
|
||||
getMediaInfoFromFileInfo(const Quotient::EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail = false, bool isSticker = false) const;
|
||||
};
|
||||
|
||||
36
src/fakerunner.cpp
Normal file
36
src/fakerunner.cpp
Normal file
@@ -0,0 +1,36 @@
|
||||
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include "fakerunner.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDBusMetaType>
|
||||
|
||||
Q_SCRIPTABLE RemoteActions FakeRunner::Actions()
|
||||
{
|
||||
QCoreApplication::quit();
|
||||
return {};
|
||||
}
|
||||
|
||||
Q_SCRIPTABLE RemoteMatches FakeRunner::Match(const QString &searchTerm)
|
||||
{
|
||||
QCoreApplication::quit();
|
||||
return {};
|
||||
}
|
||||
|
||||
Q_SCRIPTABLE void FakeRunner::Run(const QString &id, const QString &actionId)
|
||||
{
|
||||
QCoreApplication::quit();
|
||||
}
|
||||
|
||||
FakeRunner::FakeRunner()
|
||||
: QObject()
|
||||
{
|
||||
qDBusRegisterMetaType<RemoteMatch>();
|
||||
qDBusRegisterMetaType<RemoteMatches>();
|
||||
qDBusRegisterMetaType<RemoteAction>();
|
||||
qDBusRegisterMetaType<RemoteActions>();
|
||||
qDBusRegisterMetaType<RemoteImage>();
|
||||
}
|
||||
|
||||
#include "moc_fakerunner.cpp"
|
||||
31
src/fakerunner.h
Normal file
31
src/fakerunner.h
Normal file
@@ -0,0 +1,31 @@
|
||||
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QDBusContext>
|
||||
|
||||
#include "runner.h"
|
||||
|
||||
/**
|
||||
* This is a close-to-identical copy of the regular Runner interface,
|
||||
* only used when activated for push notifications. This stubs it out so
|
||||
* Plasma Search and Kickoff doesn't accidentally activate the push notification
|
||||
* service.
|
||||
*
|
||||
* @sa Runner
|
||||
*/
|
||||
class FakeRunner : public QObject, protected QDBusContext
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_CLASSINFO("D-Bus Interface", "org.kde.krunner1")
|
||||
|
||||
public:
|
||||
Q_SCRIPTABLE RemoteActions Actions();
|
||||
|
||||
Q_SCRIPTABLE RemoteMatches Match(const QString &searchTerm);
|
||||
|
||||
Q_SCRIPTABLE void Run(const QString &id, const QString &actionId);
|
||||
|
||||
FakeRunner();
|
||||
};
|
||||
@@ -41,6 +41,12 @@ FileType::~FileType() noexcept
|
||||
{
|
||||
}
|
||||
|
||||
FileType &FileType::instance()
|
||||
{
|
||||
static FileType _instance;
|
||||
return _instance;
|
||||
}
|
||||
|
||||
QMimeType FileType::mimeTypeForName(const QString &nameOrAlias) const
|
||||
{
|
||||
Q_D(const FileType);
|
||||
@@ -113,4 +119,10 @@ QStringList FileType::supportedAnimatedImageFormats() const
|
||||
return d->supportedAnimatedImageFormats;
|
||||
}
|
||||
|
||||
bool FileType::fileHasImage(const QUrl &file) const
|
||||
{
|
||||
const auto mimeType = mimeTypeForFile(file.toString());
|
||||
return mimeType.isValid() && supportedImageFormats().contains(mimeType.preferredSuffix());
|
||||
}
|
||||
|
||||
#include "moc_filetype.cpp"
|
||||
|
||||
@@ -41,8 +41,13 @@ class FileType : public QObject
|
||||
Q_PROPERTY(QStringList supportedAnimatedImageFormats READ supportedAnimatedImageFormats CONSTANT FINAL)
|
||||
|
||||
public:
|
||||
explicit FileType(QObject *parent = nullptr);
|
||||
~FileType();
|
||||
static FileType &instance();
|
||||
static FileType *create(QQmlEngine *engine, QJSEngine *)
|
||||
{
|
||||
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
|
||||
return &instance();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns a MIME type for nameOrAlias or an invalid one if none found.
|
||||
@@ -120,7 +125,11 @@ public:
|
||||
QStringList supportedImageFormats() const;
|
||||
QStringList supportedAnimatedImageFormats() const;
|
||||
|
||||
bool fileHasImage(const QUrl &file) const;
|
||||
|
||||
private:
|
||||
explicit FileType(QObject *parent = nullptr);
|
||||
|
||||
const QScopedPointer<FileTypePrivate> d_ptr;
|
||||
Q_DECLARE_PRIVATE(FileType)
|
||||
Q_DISABLE_COPY(FileType)
|
||||
|
||||
80
src/main.cpp
80
src/main.cpp
@@ -11,6 +11,7 @@
|
||||
#include <QQmlNetworkAccessManagerFactory>
|
||||
#include <QQuickStyle>
|
||||
#include <QQuickWindow>
|
||||
#include <QtQml/QQmlExtensionPlugin>
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include <QGuiApplication>
|
||||
@@ -34,6 +35,11 @@
|
||||
|
||||
#include "neochat-version.h"
|
||||
|
||||
#include <Quotient/accountregistry.h>
|
||||
#if __has_include("Quotient/e2ee/sssshandler.h")
|
||||
#include <Quotient/e2ee/sssshandler.h>
|
||||
#endif
|
||||
#include <Quotient/keyverificationsession.h>
|
||||
#include <Quotient/networkaccessmanager.h>
|
||||
|
||||
#include "blurhashimageprovider.h"
|
||||
@@ -44,10 +50,16 @@
|
||||
#include "neochatconfig.h"
|
||||
#include "roommanager.h"
|
||||
#include "windowcontroller.h"
|
||||
#include "sharehandler.h"
|
||||
|
||||
#ifdef HAVE_RUNNER
|
||||
#include "runner.h"
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusMetaType>
|
||||
#endif
|
||||
|
||||
#if defined(HAVE_RUNNER) && defined(HAVE_KUNIFIEDPUSH)
|
||||
#include "fakerunner.h"
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_WINDOWS
|
||||
@@ -139,7 +151,7 @@ int main(int argc, char *argv[])
|
||||
about.addAuthor(i18n("Tobias Fella"), i18n("Maintainer"), QStringLiteral("tobias.fella@kde.org"), QStringLiteral("https://tobiasfella.de"));
|
||||
about.addAuthor(i18n("James Graham"), i18n("Maintainer"), QStringLiteral("james.h.graham@protonmail.com"));
|
||||
about.addCredit(i18n("Black Hat"), i18n("Original author of Spectral"), QStringLiteral("bhat@encom.eu.org"));
|
||||
about.addCredit(i18n("Alexey Rusakov"), i18n("Maintainer of Quotient"), QStringLiteral("Kitsune-Ral@users.sf.net"));
|
||||
about.addCredit(i18n("Alexey Rusakov"), i18n("Maintainer of libQuotient"), QStringLiteral("Kitsune-Ral@users.sf.net"));
|
||||
about.setTranslator(i18nc("NAME OF TRANSLATORS", "Your names"), i18nc("EMAIL OF TRANSLATORS", "Your emails"));
|
||||
about.setOrganizationDomain("kde.org");
|
||||
|
||||
@@ -186,6 +198,9 @@ int main(int argc, char *argv[])
|
||||
parser.addOption(dbusActivatedOption);
|
||||
#endif
|
||||
|
||||
QCommandLineOption shareOption(QStringLiteral("share"), i18n("Share a URL to Matrix"), QStringLiteral("text"));
|
||||
parser.addOption(shareOption);
|
||||
|
||||
about.setupCommandLine(&parser);
|
||||
parser.process(app);
|
||||
about.processCommandLine(&parser);
|
||||
@@ -196,6 +211,14 @@ int main(int argc, char *argv[])
|
||||
// We want to be replaceable by the main client
|
||||
KDBusService service(KDBusService::Replace);
|
||||
|
||||
#ifdef HAVE_RUNNER
|
||||
// If we are built with KRunner and KUnifiedPush support, we need to do something special.
|
||||
// Because KRunner may call us on the D-Bus (under the same service name org.kde.neochat) then it may
|
||||
// accidentally activate us for push notifications instead. If this happens, then immediately quit if the fake
|
||||
// runner is called.
|
||||
QDBusConnection::sessionBus().registerObject("/RoomRunner"_ls, new FakeRunner(), QDBusConnection::ExportScriptableContents);
|
||||
#endif
|
||||
|
||||
Controller::listenForNotifications();
|
||||
return QCoreApplication::exec();
|
||||
}
|
||||
@@ -205,36 +228,49 @@ int main(int argc, char *argv[])
|
||||
KDBusService service(KDBusService::Unique);
|
||||
#endif
|
||||
|
||||
Q_IMPORT_QML_PLUGIN(org_kde_neochat_settingsPlugin)
|
||||
Q_IMPORT_QML_PLUGIN(org_kde_neochat_timelinePlugin)
|
||||
Q_IMPORT_QML_PLUGIN(org_kde_neochat_devtoolsPlugin)
|
||||
|
||||
qml_register_types_org_kde_neochat();
|
||||
qmlRegisterSingletonInstance("org.kde.neochat.config", 1, 0, "Config", NeoChatConfig::self());
|
||||
qmlRegisterSingletonInstance("org.kde.neochat.accounts", 1, 0, "AccountRegistry", &Controller::instance().accounts());
|
||||
|
||||
qmlRegisterUncreatableType<KeyVerificationSession>("com.github.quotient_im.libquotient", 1, 0, "KeyVerificationSession", {});
|
||||
#if __has_include("Quotient/e2ee/sssshandler.h")
|
||||
qmlRegisterType<SSSSHandler>("com.github.quotient_im.libquotient", 1, 0, "SSSSHandler");
|
||||
#endif
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
|
||||
#ifdef HAVE_KDBUSADDONS
|
||||
service.connect(&service,
|
||||
&KDBusService::activateRequested,
|
||||
&RoomManager::instance(),
|
||||
[&engine](const QStringList &arguments, const QString &workingDirectory) {
|
||||
Q_UNUSED(workingDirectory);
|
||||
&KDBusService::activateRequested,
|
||||
&RoomManager::instance(),
|
||||
[&engine](const QStringList &arguments, const QString &workingDirectory) {
|
||||
Q_UNUSED(workingDirectory);
|
||||
|
||||
QWindow *window = windowFromEngine(&engine);
|
||||
KWindowSystem::updateStartupId(window);
|
||||
QWindow *window = windowFromEngine(&engine);
|
||||
KWindowSystem::updateStartupId(window);
|
||||
|
||||
WindowController::instance().showAndRaiseWindow(QString());
|
||||
WindowController::instance().showAndRaiseWindow(QString());
|
||||
|
||||
// Open matrix uri
|
||||
if (arguments.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
auto args = arguments;
|
||||
args.removeFirst();
|
||||
for (const auto &arg : args) {
|
||||
RoomManager::instance().resolveResource(arg);
|
||||
}
|
||||
});
|
||||
// Open matrix uri
|
||||
if (arguments.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto args = arguments;
|
||||
args.removeFirst();
|
||||
if (args.length() == 2 && args[0] == "--share"_ls) {
|
||||
ShareHandler::instance().setText(args[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto &arg : args) {
|
||||
RoomManager::instance().resolveResource(arg);
|
||||
}
|
||||
});
|
||||
#endif
|
||||
|
||||
engine.rootContext()->setContextObject(new KLocalizedContext(&engine));
|
||||
@@ -247,15 +283,19 @@ int main(int argc, char *argv[])
|
||||
});
|
||||
}
|
||||
|
||||
if (parser.isSet("share"_ls)) {
|
||||
ShareHandler::instance().setText(parser.value(shareOption));
|
||||
}
|
||||
|
||||
engine.addImageProvider(QLatin1String("mxc"), MatrixImageProvider::create(&engine, &engine));
|
||||
engine.addImageProvider(QLatin1String("blurhash"), new BlurhashImageProvider);
|
||||
|
||||
engine.load(QUrl(QStringLiteral("qrc:/org/kde/neochat/qml/main.qml")));
|
||||
engine.load(QUrl(QStringLiteral("qrc:/qt/qml/org/kde/neochat/qml/main.qml")));
|
||||
if (engine.rootObjects().isEmpty()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!parser.positionalArguments().isEmpty()) {
|
||||
if (!parser.positionalArguments().isEmpty() && !parser.isSet("share"_ls)) {
|
||||
RoomManager::instance().setUrlArgument(parser.positionalArguments()[0]);
|
||||
}
|
||||
|
||||
|
||||
9
src/mediamanager.cpp
Normal file
9
src/mediamanager.cpp
Normal file
@@ -0,0 +1,9 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "mediamanager.h"
|
||||
|
||||
void MediaManager::startPlayback()
|
||||
{
|
||||
Q_EMIT playbackStarted();
|
||||
}
|
||||
42
src/mediamanager.h
Normal file
42
src/mediamanager.h
Normal file
@@ -0,0 +1,42 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QQmlEngine>
|
||||
|
||||
/**
|
||||
* @class MediaManager
|
||||
*
|
||||
* Manages media playback, like voice/audio messages, videos, etc.
|
||||
*/
|
||||
class MediaManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
|
||||
public:
|
||||
static MediaManager &instance()
|
||||
{
|
||||
static MediaManager _instance;
|
||||
return _instance;
|
||||
}
|
||||
static MediaManager *create(QQmlEngine *, QJSEngine *)
|
||||
{
|
||||
QQmlEngine::setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
|
||||
return &instance();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Notify other objects that media playback has started.
|
||||
*/
|
||||
Q_INVOKABLE void startPlayback();
|
||||
|
||||
Q_SIGNALS:
|
||||
/**
|
||||
* @brief Emitted when any media player starts playing. Other objects should stop / pause playback.
|
||||
*/
|
||||
void playbackStarted();
|
||||
};
|
||||
@@ -354,17 +354,12 @@ QList<ActionsModel::Action> actions{
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
|
||||
return QString();
|
||||
}
|
||||
auto user = room->connection()->users()[text];
|
||||
if (room->connection()->ignoredUsers().contains(user->id())) {
|
||||
if (room->connection()->ignoredUsers().contains(text)) {
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<username> is already ignored.", "%1 is already ignored.", text));
|
||||
return QString();
|
||||
}
|
||||
if (user) {
|
||||
room->connection()->addToIgnoredUsers(user);
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> is now ignored", "%1 is now ignored.", text));
|
||||
} else {
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("<username> is not a known user", "%1 is not a known user.", text));
|
||||
}
|
||||
room->connection()->addToIgnoredUsers(room->connection()->user(text));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> is now ignored", "%1 is now ignored.", text));
|
||||
return QString();
|
||||
},
|
||||
false,
|
||||
@@ -382,17 +377,12 @@ QList<ActionsModel::Action> actions{
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
|
||||
return QString();
|
||||
}
|
||||
auto user = room->connection()->users()[text];
|
||||
if (user) {
|
||||
if (!room->connection()->ignoredUsers().contains(user->id())) {
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<username> is not ignored.", "%1 is not ignored.", text));
|
||||
return QString();
|
||||
}
|
||||
room->connection()->removeFromIgnoredUsers(user);
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> is no longer ignored.", "%1 is no longer ignored.", text));
|
||||
} else {
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("<username> is not a known user", "%1 is not a known user.", text));
|
||||
if (!room->connection()->ignoredUsers().contains(text)) {
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<username> is not ignored.", "%1 is not ignored.", text));
|
||||
return QString();
|
||||
}
|
||||
room->connection()->removeFromIgnoredUsers(room->connection()->user(text));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> is no longer ignored.", "%1 is no longer ignored.", text));
|
||||
return QString();
|
||||
},
|
||||
false,
|
||||
|
||||
@@ -91,7 +91,9 @@ QVariant CompletionModel::data(const QModelIndex &index, int role) const
|
||||
if (mediaId.isEmpty()) {
|
||||
return QVariant();
|
||||
}
|
||||
return m_room->connection()->makeMediaUrl(QUrl(QStringLiteral("mxc://%1").arg(mediaId)));
|
||||
if (m_room) {
|
||||
return m_room->connection()->makeMediaUrl(QUrl(QStringLiteral("mxc://%1").arg(mediaId)));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (m_autoCompletionType == Emoji) {
|
||||
|
||||
@@ -118,7 +118,7 @@ private:
|
||||
QString m_text;
|
||||
QString m_fullText;
|
||||
CompletionProxyModel *m_filterModel;
|
||||
NeoChatRoom *m_room = nullptr;
|
||||
QPointer<NeoChatRoom> m_room;
|
||||
AutoCompletionType m_autoCompletionType = None;
|
||||
|
||||
void updateCompletion();
|
||||
|
||||
@@ -161,7 +161,7 @@ QVariant CustomEmojiModel::data(const QModelIndex &idx, int role) const
|
||||
case Roles::ImageURL:
|
||||
return QUrl(QStringLiteral("image://mxc/") + data.url.mid(6));
|
||||
case Roles::MxcUrl:
|
||||
return data.url.mid(6);
|
||||
return m_connection->makeMediaUrl(QUrl(data.url));
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -19,6 +19,15 @@ using namespace Quotient;
|
||||
DevicesModel::DevicesModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
connect(m_connection, &Connection::sessionVerified, this, [this](const QString &, const QString &deviceId) {
|
||||
const auto it = std::find_if(m_devices.begin(), m_devices.end(), [deviceId](const Quotient::Device &device) {
|
||||
return device.deviceId == deviceId;
|
||||
});
|
||||
if (it != m_devices.end()) {
|
||||
const auto index = this->index(it - m_devices.begin());
|
||||
Q_EMIT dataChanged(index, index, {Type});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void DevicesModel::fetchDevices()
|
||||
|
||||
@@ -64,11 +64,13 @@ void ImagePacksModel::setRoom(NeoChatRoom *room)
|
||||
}
|
||||
m_room = room;
|
||||
|
||||
connect(m_room->connection(), &Connection::accountDataChanged, this, [this](const QString &type) {
|
||||
if (type == "im.ponies.user_emotes"_ls) {
|
||||
reloadImages();
|
||||
}
|
||||
});
|
||||
if (m_room) {
|
||||
connect(m_room->connection(), &Connection::accountDataChanged, this, [this](const QString &type) {
|
||||
if (type == "im.ponies.user_emotes"_ls) {
|
||||
reloadImages();
|
||||
}
|
||||
});
|
||||
}
|
||||
// TODO listen to packs changing
|
||||
reloadImages();
|
||||
Q_EMIT roomChanged();
|
||||
@@ -76,6 +78,9 @@ void ImagePacksModel::setRoom(NeoChatRoom *room)
|
||||
|
||||
void ImagePacksModel::reloadImages()
|
||||
{
|
||||
if (!m_room) {
|
||||
return;
|
||||
}
|
||||
beginResetModel();
|
||||
m_events.clear();
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "itinerarymodel.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QProcess>
|
||||
|
||||
#include "config-neochat.h"
|
||||
@@ -16,20 +17,6 @@ ItineraryModel::ItineraryModel(QObject *parent)
|
||||
{
|
||||
}
|
||||
|
||||
void ItineraryModel::setConnection(NeoChatConnection *connection)
|
||||
{
|
||||
if (m_connection == connection) {
|
||||
return;
|
||||
}
|
||||
m_connection = connection;
|
||||
Q_EMIT connectionChanged();
|
||||
}
|
||||
|
||||
NeoChatConnection *ItineraryModel::connection() const
|
||||
{
|
||||
return m_connection;
|
||||
}
|
||||
|
||||
QVariant ItineraryModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid()) {
|
||||
@@ -39,20 +26,68 @@ QVariant ItineraryModel::data(const QModelIndex &index, int role) const
|
||||
auto data = m_data[row];
|
||||
if (role == NameRole) {
|
||||
if (data[QStringLiteral("@type")] == QStringLiteral("TrainReservation")) {
|
||||
return data[QStringLiteral("reservationFor")][QStringLiteral("trainNumber")];
|
||||
auto trainName = QStringLiteral("%1 %2").arg(data[QStringLiteral("reservationFor")][QStringLiteral("trainName")].toString(),
|
||||
data[QStringLiteral("reservationFor")][QStringLiteral("trainNumber")].toString());
|
||||
if (trainName.trimmed().isEmpty()) {
|
||||
return QStringLiteral("%1 to %2")
|
||||
.arg(data[QStringLiteral("reservationFor")][QStringLiteral("departureStation")][QStringLiteral("name")].toString(),
|
||||
data[QStringLiteral("reservationFor")][QStringLiteral("arrivalStation")][QStringLiteral("name")].toString());
|
||||
;
|
||||
}
|
||||
return trainName;
|
||||
}
|
||||
if (data[QStringLiteral("@type")] == QStringLiteral("LodgingReservation")) {
|
||||
return data[QStringLiteral("reservationFor")][QStringLiteral("name")];
|
||||
}
|
||||
if (data[QStringLiteral("@type")] == QStringLiteral("FoodEstablishmentReservation")) {
|
||||
return data[QStringLiteral("reservationFor")][QStringLiteral("name")];
|
||||
}
|
||||
if (data[QStringLiteral("@type")] == QStringLiteral("FlightReservation")) {
|
||||
return QStringLiteral("%1 %2 %3 → %4")
|
||||
.arg(data[QStringLiteral("reservationFor")][QStringLiteral("airline")][QStringLiteral("iataCode")].toString(),
|
||||
data[QStringLiteral("reservationFor")][QStringLiteral("flightNumber")].toString(),
|
||||
data[QStringLiteral("reservationFor")][QStringLiteral("departureAirport")][QStringLiteral("iataCode")].toString(),
|
||||
data[QStringLiteral("reservationFor")][QStringLiteral("arrivalAirport")][QStringLiteral("iataCode")].toString());
|
||||
}
|
||||
}
|
||||
if (role == TypeRole) {
|
||||
return data[QStringLiteral("@type")];
|
||||
}
|
||||
if (role == DepartureStationRole) {
|
||||
return data[QStringLiteral("reservationFor")][QStringLiteral("departureStation")][QStringLiteral("name")];
|
||||
if (role == DepartureLocationRole) {
|
||||
if (data[QStringLiteral("@type")] == QStringLiteral("TrainReservation")) {
|
||||
return data[QStringLiteral("reservationFor")][QStringLiteral("departureStation")][QStringLiteral("name")];
|
||||
}
|
||||
if (data[QStringLiteral("@type")] == QStringLiteral("FlightReservation")) {
|
||||
return data[QStringLiteral("reservationFor")][QStringLiteral("departureAirport")][QStringLiteral("iataCode")];
|
||||
}
|
||||
}
|
||||
if (role == ArrivalStationRole) {
|
||||
return data[QStringLiteral("reservationFor")][QStringLiteral("arrivalStation")][QStringLiteral("name")];
|
||||
if (role == DepartureAddressRole) {
|
||||
if (data[QStringLiteral("@type")] == QStringLiteral("TrainReservation")) {
|
||||
return data[QStringLiteral("reservationFor")][QStringLiteral("departureStation")][QStringLiteral("address")][QStringLiteral("addressCountry")]
|
||||
.toString();
|
||||
}
|
||||
if (data[QStringLiteral("@type")] == QStringLiteral("FlightReservation")) {
|
||||
return data[QStringLiteral("reservationFor")][QStringLiteral("departureAirport")][QStringLiteral("address")][QStringLiteral("addressCountry")]
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
if (role == ArrivalLocationRole) {
|
||||
if (data[QStringLiteral("@type")] == QStringLiteral("TrainReservation")) {
|
||||
return data[QStringLiteral("reservationFor")][QStringLiteral("arrivalStation")][QStringLiteral("name")];
|
||||
}
|
||||
if (data[QStringLiteral("@type")] == QStringLiteral("FlightReservation")) {
|
||||
return data[QStringLiteral("reservationFor")][QStringLiteral("arrivalAirport")][QStringLiteral("iataCode")];
|
||||
}
|
||||
}
|
||||
if (role == ArrivalAddressRole) {
|
||||
if (data[QStringLiteral("@type")] == QStringLiteral("TrainReservation")) {
|
||||
return data[QStringLiteral("reservationFor")][QStringLiteral("arrivalStation")][QStringLiteral("address")][QStringLiteral("addressCountry")]
|
||||
.toString();
|
||||
}
|
||||
if (data[QStringLiteral("@type")] == QStringLiteral("FlightReservation")) {
|
||||
return data[QStringLiteral("reservationFor")][QStringLiteral("arrivalAirport")][QStringLiteral("address")][QStringLiteral("addressCountry")]
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
if (role == DepartureTimeRole) {
|
||||
const auto &time = data[QStringLiteral("reservationFor")][QStringLiteral("departureTime")];
|
||||
@@ -79,7 +114,16 @@ QVariant ItineraryModel::data(const QModelIndex &index, int role) const
|
||||
addressData[QStringLiteral("addressCountry")].toString());
|
||||
}
|
||||
if (role == StartTimeRole) {
|
||||
auto dateTime = data[QStringLiteral("checkinTime")][QStringLiteral("@value")].toVariant().toDateTime();
|
||||
QDateTime dateTime;
|
||||
if (data[QStringLiteral("@type")] == QStringLiteral("LodgingReservation")) {
|
||||
dateTime = data[QStringLiteral("checkinTime")][QStringLiteral("@value")].toVariant().toDateTime();
|
||||
}
|
||||
if (data[QStringLiteral("@type")] == QStringLiteral("FoodEstablishmentReservation")) {
|
||||
dateTime = data[QStringLiteral("startTime")][QStringLiteral("@value")].toVariant().toDateTime();
|
||||
}
|
||||
if (data[QStringLiteral("@type")] == QStringLiteral("FlightReservation")) {
|
||||
dateTime = data[QStringLiteral("reservationFor")][QStringLiteral("boardingTime")][QStringLiteral("@value")].toVariant().toDateTime();
|
||||
}
|
||||
return dateTime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat));
|
||||
}
|
||||
if (role == EndTimeRole) {
|
||||
@@ -112,8 +156,10 @@ QHash<int, QByteArray> ItineraryModel::roleNames() const
|
||||
return {
|
||||
{NameRole, "name"},
|
||||
{TypeRole, "type"},
|
||||
{DepartureStationRole, "departureStation"},
|
||||
{ArrivalStationRole, "arrivalStation"},
|
||||
{DepartureLocationRole, "departureLocation"},
|
||||
{DepartureAddressRole, "departureAddress"},
|
||||
{ArrivalLocationRole, "arrivalLocation"},
|
||||
{ArrivalAddressRole, "arrivalAddress"},
|
||||
{DepartureTimeRole, "departureTime"},
|
||||
{ArrivalTimeRole, "arrivalTime"},
|
||||
{AddressRole, "address"},
|
||||
@@ -133,11 +179,7 @@ QString ItineraryModel::path() const
|
||||
|
||||
void ItineraryModel::setPath(const QString &path)
|
||||
{
|
||||
if (path == m_path) {
|
||||
return;
|
||||
}
|
||||
m_path = path;
|
||||
Q_EMIT pathChanged();
|
||||
loadData();
|
||||
}
|
||||
|
||||
@@ -150,6 +192,11 @@ void ItineraryModel::loadData()
|
||||
beginResetModel();
|
||||
m_data = QJsonDocument::fromJson(data).array();
|
||||
endResetModel();
|
||||
|
||||
Q_EMIT loaded();
|
||||
});
|
||||
connect(process, &QProcess::errorOccurred, this, [this]() {
|
||||
Q_EMIT loadErrorOccurred();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,28 +4,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QJsonArray>
|
||||
#include <QPointer>
|
||||
#include <QQmlEngine>
|
||||
#include <QString>
|
||||
|
||||
#include "neochatconnection.h"
|
||||
|
||||
class ItineraryModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
|
||||
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged)
|
||||
QML_UNCREATABLE("")
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
NameRole = Qt::DisplayRole,
|
||||
TypeRole,
|
||||
DepartureStationRole,
|
||||
ArrivalStationRole,
|
||||
DepartureLocationRole,
|
||||
ArrivalLocationRole,
|
||||
DepartureTimeRole,
|
||||
DepartureAddressRole,
|
||||
ArrivalTimeRole,
|
||||
ArrivalAddressRole,
|
||||
AddressRole,
|
||||
StartTimeRole,
|
||||
EndTimeRole,
|
||||
@@ -37,9 +36,6 @@ public:
|
||||
Q_ENUM(Roles)
|
||||
explicit ItineraryModel(QObject *parent = nullptr);
|
||||
|
||||
void setConnection(NeoChatConnection *connection);
|
||||
NeoChatConnection *connection() const;
|
||||
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
int rowCount(const QModelIndex &parent = {}) const override;
|
||||
|
||||
@@ -51,11 +47,10 @@ public:
|
||||
Q_INVOKABLE void sendToItinerary();
|
||||
|
||||
Q_SIGNALS:
|
||||
void connectionChanged();
|
||||
void pathChanged();
|
||||
void loaded();
|
||||
void loadErrorOccurred();
|
||||
|
||||
private:
|
||||
QPointer<NeoChatConnection> m_connection;
|
||||
QJsonArray m_data;
|
||||
QString m_path;
|
||||
void loadData();
|
||||
|
||||
@@ -3,16 +3,28 @@
|
||||
|
||||
#include "messagecontentmodel.h"
|
||||
|
||||
#include <QImageReader>
|
||||
|
||||
#include <Quotient/events/redactionevent.h>
|
||||
#include <Quotient/events/roommessageevent.h>
|
||||
#include <Quotient/events/stickerevent.h>
|
||||
#include <Quotient/room.h>
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
#include <KSyntaxHighlighting/Definition>
|
||||
#include <KSyntaxHighlighting/Repository>
|
||||
#endif
|
||||
|
||||
#include "chatbarcache.h"
|
||||
#include "enums/messagecomponenttype.h"
|
||||
#include "eventhandler.h"
|
||||
#include "filetype.h"
|
||||
#include "itinerarymodel.h"
|
||||
#include "linkpreviewer.h"
|
||||
#include "neochatroom.h"
|
||||
#include "texthandler.h"
|
||||
|
||||
MessageContentModel::MessageContentModel(const Quotient::RoomEvent *event, NeoChatRoom *room)
|
||||
: QAbstractListModel(nullptr)
|
||||
@@ -40,33 +52,35 @@ MessageContentModel::MessageContentModel(const Quotient::RoomEvent *event, NeoCh
|
||||
});
|
||||
connect(m_room, &NeoChatRoom::replyLoaded, this, [this](const QString &eventId, const QString &replyId) {
|
||||
Q_UNUSED(eventId)
|
||||
if (m_event != nullptr && m_event != nullptr) {
|
||||
if (m_event != nullptr && m_room != nullptr) {
|
||||
const auto eventHandler = EventHandler(m_room, m_event);
|
||||
if (replyId == eventHandler.getReplyId()) {
|
||||
// HACK: Because DelegateChooser can't switch the delegate on dataChanged it has to think there is a new delegate.
|
||||
beginResetModel();
|
||||
m_components[0] = MessageComponentType::Reply;
|
||||
m_components[0].type = MessageComponentType::Reply;
|
||||
endResetModel();
|
||||
}
|
||||
}
|
||||
});
|
||||
connect(m_room, &NeoChatRoom::newFileTransfer, this, [this](const QString &eventId) {
|
||||
if (m_event != nullptr && m_event != nullptr && eventId == m_event->id()) {
|
||||
if (m_event != nullptr && eventId == m_event->id()) {
|
||||
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
|
||||
}
|
||||
});
|
||||
connect(m_room, &NeoChatRoom::fileTransferProgress, this, [this](const QString &eventId) {
|
||||
if (m_event != nullptr && m_event != nullptr && eventId == m_event->id()) {
|
||||
if (m_event != nullptr && eventId == m_event->id()) {
|
||||
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
|
||||
}
|
||||
});
|
||||
connect(m_room, &NeoChatRoom::fileTransferCompleted, this, [this](const QString &eventId) {
|
||||
if (m_event != nullptr && m_event != nullptr && eventId == m_event->id()) {
|
||||
if (m_event != nullptr && eventId == m_event->id()) {
|
||||
updateComponents();
|
||||
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
|
||||
}
|
||||
});
|
||||
connect(m_room, &NeoChatRoom::fileTransferFailed, this, [this](const QString &eventId) {
|
||||
if (m_event != nullptr && m_event != nullptr && eventId == m_event->id()) {
|
||||
if (m_event != nullptr && eventId == m_event->id()) {
|
||||
updateComponents();
|
||||
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
|
||||
}
|
||||
});
|
||||
@@ -74,6 +88,7 @@ MessageContentModel::MessageContentModel(const Quotient::RoomEvent *event, NeoCh
|
||||
if (m_event != nullptr && (oldEventId == m_event->id() || newEventId == m_event->id())) {
|
||||
// HACK: Because DelegateChooser can't switch the delegate on dataChanged it has to think there is a new delegate.
|
||||
beginResetModel();
|
||||
updateComponents(newEventId == m_event->id());
|
||||
endResetModel();
|
||||
}
|
||||
});
|
||||
@@ -87,7 +102,7 @@ MessageContentModel::MessageContentModel(const Quotient::RoomEvent *event, NeoCh
|
||||
if (m_linkPreviewer->loaded()) {
|
||||
// HACK: Because DelegateChooser can't switch the delegate on dataChanged it has to think there is a new delegate.
|
||||
beginResetModel();
|
||||
m_components[m_components.size() - 1] = MessageComponentType::LinkPreview;
|
||||
m_components[m_components.size() - 1].type = MessageComponentType::LinkPreview;
|
||||
endResetModel();
|
||||
}
|
||||
});
|
||||
@@ -111,6 +126,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
|
||||
EventHandler eventHandler(m_room, m_event);
|
||||
const auto component = m_components[index.row()];
|
||||
|
||||
if (role == DisplayRole) {
|
||||
if (m_event->isRedacted()) {
|
||||
@@ -118,14 +134,16 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
|
||||
return (reason.isEmpty()) ? i18n("<i>[This message was deleted]</i>")
|
||||
: i18n("<i>[This message was deleted: %1]</i>", m_event->redactedBecause()->reason());
|
||||
}
|
||||
if (!component.content.isEmpty()) {
|
||||
return component.content;
|
||||
}
|
||||
return eventHandler.getRichBody();
|
||||
}
|
||||
if (role == ComponentTypeRole) {
|
||||
const auto component = m_components[index.row()];
|
||||
if (component == MessageComponentType::Text && !m_event->id().isEmpty() && m_room->editCache()->editId() == m_event->id()) {
|
||||
return MessageComponentType::Edit;
|
||||
}
|
||||
return component;
|
||||
return component.type;
|
||||
}
|
||||
if (role == ComponentAttributesRole) {
|
||||
return component.attributes;
|
||||
}
|
||||
if (role == EventIdRole) {
|
||||
return eventHandler.getId();
|
||||
@@ -146,6 +164,9 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
|
||||
return QVariant::fromValue(m_room->fileTransferInfo(event->id()));
|
||||
}
|
||||
}
|
||||
if (role == ItineraryModelRole) {
|
||||
return QVariant::fromValue<ItineraryModel *>(m_itineraryModel);
|
||||
}
|
||||
if (role == LatitudeRole) {
|
||||
return eventHandler.getLatitude();
|
||||
}
|
||||
@@ -198,10 +219,12 @@ QHash<int, QByteArray> MessageContentModel::roleNames() const
|
||||
QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
|
||||
roles[DisplayRole] = "display";
|
||||
roles[ComponentTypeRole] = "componentType";
|
||||
roles[ComponentAttributesRole] = "componentAttributes";
|
||||
roles[EventIdRole] = "eventId";
|
||||
roles[AuthorRole] = "author";
|
||||
roles[MediaInfoRole] = "mediaInfo";
|
||||
roles[FileTransferInfoRole] = "fileTransferInfo";
|
||||
roles[ItineraryModelRole] = "itineraryModel";
|
||||
roles[LatitudeRole] = "latitude";
|
||||
roles[LongitudeRole] = "longitude";
|
||||
roles[AssetRole] = "asset";
|
||||
@@ -216,30 +239,118 @@ QHash<int, QByteArray> MessageContentModel::roleNames() const
|
||||
return roles;
|
||||
}
|
||||
|
||||
void MessageContentModel::updateComponents()
|
||||
void MessageContentModel::updateComponents(bool isEditing)
|
||||
{
|
||||
beginResetModel();
|
||||
m_components.clear();
|
||||
|
||||
EventHandler eventHandler(m_room, m_event);
|
||||
if (eventHandler.hasReply()) {
|
||||
if (m_room->findInTimeline(eventHandler.getReplyId()) == m_room->historyEdge()) {
|
||||
m_components += MessageComponentType::ReplyLoad;
|
||||
m_room->loadReply(m_event->id(), eventHandler.getReplyId());
|
||||
} else {
|
||||
m_components += MessageComponentType::Reply;
|
||||
if (eventCast<const Quotient::RoomMessageEvent>(m_event)
|
||||
&& eventCast<const Quotient::RoomMessageEvent>(m_event)->rawMsgtype() == QStringLiteral("m.key.verification.request")) {
|
||||
m_components += MessageComponent{MessageComponentType::Verification, QString(), {}};
|
||||
} else {
|
||||
EventHandler eventHandler(m_room, m_event);
|
||||
if (eventHandler.hasReply()) {
|
||||
if (m_room->findInTimeline(eventHandler.getReplyId()) == m_room->historyEdge()) {
|
||||
m_components += MessageComponent{MessageComponentType::ReplyLoad, QString(), {}};
|
||||
m_room->loadReply(m_event->id(), eventHandler.getReplyId());
|
||||
} else {
|
||||
m_components += MessageComponent{MessageComponentType::Reply, QString(), {}};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_components += eventHandler.messageComponentType();
|
||||
|
||||
if (m_linkPreviewer != nullptr) {
|
||||
if (m_linkPreviewer->loaded()) {
|
||||
m_components += MessageComponentType::LinkPreview;
|
||||
if (isEditing) {
|
||||
m_components += MessageComponent{MessageComponentType::Edit, QString(), {}};
|
||||
} else if (m_event->isRedacted()) {
|
||||
m_components += MessageComponent{MessageComponentType::Text, QString(), {}};
|
||||
} else {
|
||||
m_components += MessageComponentType::LinkPreviewLoad;
|
||||
if (eventHandler.messageComponentType() == MessageComponentType::Text) {
|
||||
const auto event = eventCast<const Quotient::RoomMessageEvent>(m_event);
|
||||
auto body = EventHandler::rawMessageBody(*event);
|
||||
m_components.append(TextHandler().textComponents(body, EventHandler::messageBodyInputFormat(*event), m_room, event, event->isReplaced()));
|
||||
} else if (eventHandler.messageComponentType() == MessageComponentType::File) {
|
||||
m_components += MessageComponent{MessageComponentType::File, QString(), {}};
|
||||
if (m_emptyItinerary) {
|
||||
Quotient::FileTransferInfo fileTransferInfo;
|
||||
if (auto event = eventCast<const Quotient::RoomMessageEvent>(m_event)) {
|
||||
if (event->hasFileContent()) {
|
||||
fileTransferInfo = m_room->fileTransferInfo(event->id());
|
||||
}
|
||||
}
|
||||
if (auto event = eventCast<const Quotient::StickerEvent>(m_event)) {
|
||||
fileTransferInfo = m_room->fileTransferInfo(event->id());
|
||||
}
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
KSyntaxHighlighting::Repository repository;
|
||||
const auto definitionForFile = repository.definitionForFileName(fileTransferInfo.localPath.toString());
|
||||
if (definitionForFile.isValid() || QFileInfo(fileTransferInfo.localPath.path()).suffix() == QStringLiteral("txt")) {
|
||||
QFile file(fileTransferInfo.localPath.path());
|
||||
file.open(QIODevice::ReadOnly);
|
||||
m_components += MessageComponent{MessageComponentType::Code,
|
||||
QString::fromStdString(file.readAll().toStdString()),
|
||||
{{QStringLiteral("class"), definitionForFile.name()}}};
|
||||
}
|
||||
#endif
|
||||
|
||||
if (FileType::instance().fileHasImage(fileTransferInfo.localPath)) {
|
||||
QImageReader reader(fileTransferInfo.localPath.path());
|
||||
m_components += MessageComponent{MessageComponentType::Pdf, QString(), {{QStringLiteral("size"), reader.size()}}};
|
||||
}
|
||||
} else {
|
||||
updateItineraryModel();
|
||||
if (m_itineraryModel != nullptr) {
|
||||
m_components += MessageComponent{MessageComponentType::Itinerary, QString(), {}};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
m_components += MessageComponent{eventHandler.messageComponentType(), QString(), {}};
|
||||
}
|
||||
}
|
||||
|
||||
if (m_linkPreviewer != nullptr) {
|
||||
if (m_linkPreviewer->loaded()) {
|
||||
m_components += MessageComponent{MessageComponentType::LinkPreview, QString(), {}};
|
||||
} else {
|
||||
m_components += MessageComponent{MessageComponentType::LinkPreviewLoad, QString(), {}};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void MessageContentModel::updateItineraryModel()
|
||||
{
|
||||
if (m_room == nullptr || m_event == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto event = eventCast<const Quotient::RoomMessageEvent>(m_event)) {
|
||||
if (event->hasFileContent()) {
|
||||
auto filePath = m_room->fileTransferInfo(event->id()).localPath;
|
||||
if (filePath.isEmpty() && m_itineraryModel != nullptr) {
|
||||
delete m_itineraryModel;
|
||||
m_itineraryModel = nullptr;
|
||||
} else if (!filePath.isEmpty()) {
|
||||
if (m_itineraryModel == nullptr) {
|
||||
m_itineraryModel = new ItineraryModel(this);
|
||||
connect(m_itineraryModel, &ItineraryModel::loaded, this, [this]() {
|
||||
if (m_itineraryModel->rowCount() == 0) {
|
||||
m_itineraryModel->deleteLater();
|
||||
m_itineraryModel = nullptr;
|
||||
m_emptyItinerary = true;
|
||||
updateComponents();
|
||||
}
|
||||
});
|
||||
connect(m_itineraryModel, &ItineraryModel::loadErrorOccurred, this, [this]() {
|
||||
m_itineraryModel->deleteLater();
|
||||
m_itineraryModel = nullptr;
|
||||
m_emptyItinerary = true;
|
||||
updateComponents();
|
||||
});
|
||||
}
|
||||
m_itineraryModel->setPath(filePath.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,23 @@
|
||||
#include <QAbstractListModel>
|
||||
#include <QQmlEngine>
|
||||
|
||||
#include "enums/messagecomponenttype.h"
|
||||
#include "eventhandler.h"
|
||||
#include "itinerarymodel.h"
|
||||
#include "linkpreviewer.h"
|
||||
#include "messagecomponenttype.h"
|
||||
#include "neochatroom.h"
|
||||
|
||||
struct MessageComponent {
|
||||
MessageComponentType::Type type;
|
||||
QString content;
|
||||
QVariantMap attributes;
|
||||
|
||||
int operator==(const MessageComponent &right) const
|
||||
{
|
||||
return type == right.type && content == right.content && attributes == right.attributes;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @class MessageContentModel
|
||||
*
|
||||
@@ -29,10 +41,12 @@ public:
|
||||
enum Roles {
|
||||
DisplayRole = Qt::DisplayRole, /**< The display text for the message. */
|
||||
ComponentTypeRole, /**< The type of component to visualise the message. */
|
||||
ComponentAttributesRole, /**< The attributes of the component. */
|
||||
EventIdRole, /**< The matrix event ID of the event. */
|
||||
AuthorRole, /**< The author of the event. */
|
||||
MediaInfoRole, /**< The media info for the event. */
|
||||
FileTransferInfoRole, /**< FileTransferInfo for any downloading files. */
|
||||
ItineraryModelRole, /**< The itinerary model for a file. */
|
||||
LatitudeRole, /**< Latitude for a location event. */
|
||||
LongitudeRole, /**< Longitude for a location event. */
|
||||
AssetRole, /**< Type of location event, e.g. self pin of the user location. */
|
||||
@@ -73,11 +87,15 @@ public:
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
private:
|
||||
NeoChatRoom *m_room = nullptr;
|
||||
QPointer<NeoChatRoom> m_room;
|
||||
const Quotient::RoomEvent *m_event = nullptr;
|
||||
|
||||
QVector<MessageComponentType::Type> m_components;
|
||||
void updateComponents();
|
||||
QList<MessageComponent> m_components;
|
||||
void updateComponents(bool isEditing = false);
|
||||
|
||||
LinkPreviewer *m_linkPreviewer = nullptr;
|
||||
ItineraryModel *m_itineraryModel = nullptr;
|
||||
|
||||
void updateItineraryModel();
|
||||
bool m_emptyItinerary = false;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "messageeventmodel.h"
|
||||
#include "messagecomponenttype.h"
|
||||
#include "messageeventmodel_logging.h"
|
||||
|
||||
#include "neochatconfig.h"
|
||||
@@ -24,6 +25,7 @@
|
||||
#include "events/pollevent.h"
|
||||
#include "linkpreviewer.h"
|
||||
#include "messagecontentmodel.h"
|
||||
#include "models/messagefiltermodel.h"
|
||||
#include "models/reactionmodel.h"
|
||||
#include "texthandler.h"
|
||||
|
||||
@@ -43,7 +45,6 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
||||
roles[ProgressInfoRole] = "progressInfo";
|
||||
roles[IsThreadedRole] = "isThreaded";
|
||||
roles[ThreadRootRole] = "threadRoot";
|
||||
roles[ShowAuthorRole] = "showAuthor";
|
||||
roles[ShowSectionRole] = "showSection";
|
||||
roles[ReadMarkersRole] = "readMarkers";
|
||||
roles[ExcessReadMarkersRole] = "excessReadMarkers";
|
||||
@@ -58,6 +59,7 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
||||
roles[IsPendingRole] = "isPending";
|
||||
roles[ContentModelRole] = "contentModel";
|
||||
roles[MediaInfoRole] = "mediaInfo";
|
||||
roles[IsEditableRole] = "isEditable";
|
||||
return roles;
|
||||
}
|
||||
|
||||
@@ -176,7 +178,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
}
|
||||
if (biggest < m_currentRoom->maxTimelineIndex()) {
|
||||
auto rowBelowInserted = m_currentRoom->maxTimelineIndex() - biggest + timelineBaseIndex() - 1;
|
||||
refreshEventRoles(rowBelowInserted, {ShowAuthorRole});
|
||||
refreshEventRoles(rowBelowInserted, {MessageFilterModel::ShowAuthorRole});
|
||||
}
|
||||
for (auto i = m_currentRoom->maxTimelineIndex() - biggest; i <= m_currentRoom->maxTimelineIndex() - lowest; ++i) {
|
||||
refreshLastUserEvents(i);
|
||||
@@ -206,7 +208,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
refreshRow(timelineBaseIndex()); // Refresh the looks
|
||||
refreshLastUserEvents(0);
|
||||
if (timelineBaseIndex() > 0) { // Refresh below, see #312
|
||||
refreshEventRoles(timelineBaseIndex() - 1, {ShowAuthorRole});
|
||||
refreshEventRoles(timelineBaseIndex() - 1, {MessageFilterModel::ShowAuthorRole});
|
||||
}
|
||||
});
|
||||
connect(m_currentRoom, &Room::pendingEventChanged, this, &MessageEventModel::refreshRow);
|
||||
@@ -549,25 +551,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
return eventHandler.threadRoot();
|
||||
}
|
||||
|
||||
if (role == ShowAuthorRole) {
|
||||
for (auto r = row + 1; r < rowCount(); ++r) {
|
||||
auto i = index(r);
|
||||
// Note !itemData(i).empty() is a check for instances where rows have been removed, e.g. when the read marker is moved.
|
||||
// While the row is removed the subsequent row indexes are not changed so we need to skip over the removed index.
|
||||
// See - https://doc.qt.io/qt-5/qabstractitemmodel.html#beginRemoveRows
|
||||
if (data(i, SpecialMarksRole) != EventStatus::Hidden && !itemData(i).empty()) {
|
||||
return data(i, AuthorRole) != data(idx, AuthorRole) || data(i, DelegateTypeRole) == DelegateType::State
|
||||
|| data(i, TimeRole).toDateTime().msecsTo(data(idx, TimeRole).toDateTime()) > 600000
|
||||
|| data(i, TimeRole).toDateTime().toLocalTime().date().day() != data(idx, TimeRole).toDateTime().toLocalTime().date().day()
|
||||
// FIXME: This should not be necessary; the proper fix is to calculate this role in MessageFilterModel with the knowledge about the filtered
|
||||
// events.
|
||||
|| data(i, IsRedactedRole).toBool();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (role == ShowSectionRole) {
|
||||
for (auto r = row + 1; r < rowCount(); ++r) {
|
||||
auto i = index(r);
|
||||
@@ -637,11 +620,19 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
return eventHandler.getMediaInfo();
|
||||
}
|
||||
|
||||
if (role == IsEditableRole) {
|
||||
return eventHandler.messageComponentType() == MessageComponentType::Text && evt.senderId() == m_currentRoom->localUser()->id();
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
int MessageEventModel::eventIdToRow(const QString &eventID) const
|
||||
{
|
||||
if (m_currentRoom == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const auto it = m_currentRoom->findInTimeline(eventID);
|
||||
if (it == m_currentRoom->historyEdge()) {
|
||||
// qWarning() << "Trying to find inexistent event:" << eventID;
|
||||
|
||||
@@ -56,7 +56,6 @@ public:
|
||||
IsThreadedRole,
|
||||
ThreadRootRole,
|
||||
|
||||
ShowAuthorRole, /**< Whether the author's name should be shown. */
|
||||
ShowSectionRole, /**< Whether the section header should be shown. */
|
||||
|
||||
ReadMarkersRole, /**< The first 5 other users at the event for read marker tracking. */
|
||||
@@ -70,6 +69,7 @@ public:
|
||||
AuthorDisplayNameRole, /**< The displayname for the event's sender; for name change events, the old displayname. */
|
||||
IsRedactedRole, /**< Whether an event has been deleted. */
|
||||
IsPendingRole, /**< Whether an event is waiting to be accepted by the server. */
|
||||
IsEditableRole, /**< Whether the event can be edited by the user. */
|
||||
LastRole, // Keep this last
|
||||
};
|
||||
Q_ENUM(EventRoles)
|
||||
@@ -113,7 +113,7 @@ private Q_SLOTS:
|
||||
void refreshRow(int row);
|
||||
|
||||
private:
|
||||
NeoChatRoom *m_currentRoom = nullptr;
|
||||
QPointer<NeoChatRoom> m_currentRoom = nullptr;
|
||||
QString lastReadEventId;
|
||||
QPersistentModelIndex m_lastReadEventIndex;
|
||||
int rowBelowInserted = -1;
|
||||
|
||||
@@ -80,8 +80,24 @@ QVariant MessageFilterModel::data(const QModelIndex &index, int role) const
|
||||
return authorList(mapToSource(index).row());
|
||||
} else if (role == ExcessAuthorsRole) {
|
||||
return excessAuthors(mapToSource(index).row());
|
||||
} else if (role == ShowAuthorRole) {
|
||||
for (auto r = index.row() + 1; r < rowCount(); ++r) {
|
||||
auto i = this->index(r, 0);
|
||||
// Note !itemData(i).empty() is a check for instances where rows have been removed, e.g. when the read marker is moved.
|
||||
// While the row is removed the subsequent row indexes are not changed so we need to skip over the removed index.
|
||||
// See - https://doc.qt.io/qt-5/qabstractitemmodel.html#beginRemoveRows
|
||||
if (data(i, MessageEventModel::SpecialMarksRole) != EventStatus::Hidden && !itemData(i).empty()) {
|
||||
return data(i, MessageEventModel::AuthorRole) != data(index, MessageEventModel::AuthorRole)
|
||||
|| data(i, MessageEventModel::DelegateTypeRole) == DelegateType::State
|
||||
|| data(i, MessageEventModel::TimeRole).toDateTime().msecsTo(data(index, MessageEventModel::TimeRole).toDateTime()) > 600000
|
||||
|| data(i, MessageEventModel::TimeRole).toDateTime().toLocalTime().date().day()
|
||||
!= data(index, MessageEventModel::TimeRole).toDateTime().toLocalTime().date().day();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return sourceModel()->data(mapToSource(index), role);
|
||||
return QSortFilterProxyModel::data(index, role);
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> MessageFilterModel::roleNames() const
|
||||
@@ -91,6 +107,7 @@ QHash<int, QByteArray> MessageFilterModel::roleNames() const
|
||||
roles[StateEventsRole] = "stateEvents";
|
||||
roles[AuthorListRole] = "authorList";
|
||||
roles[ExcessAuthorsRole] = "excessAuthors";
|
||||
roles[ShowAuthorRole] = "showAuthor";
|
||||
return roles;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ public:
|
||||
StateEventsRole, /**< List of state events in the aggregated state. */
|
||||
AuthorListRole, /**< List of the first 5 unique authors of the aggregated state event. */
|
||||
ExcessAuthorsRole, /**< The number of unique authors beyond the first 5. */
|
||||
ShowAuthorRole, /**< Whether the author (name and avatar) should be shown at this message. */
|
||||
LastRole, // Keep this last
|
||||
};
|
||||
|
||||
|
||||
@@ -124,6 +124,15 @@ void PublicRoomListModel::setShowOnlySpaces(bool showOnlySpaces)
|
||||
}
|
||||
m_showOnlySpaces = showOnlySpaces;
|
||||
Q_EMIT showOnlySpacesChanged();
|
||||
|
||||
nextBatch = QString();
|
||||
attempted = false;
|
||||
|
||||
if (job) {
|
||||
job->abandon();
|
||||
job = nullptr;
|
||||
Q_EMIT searchingChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void PublicRoomListModel::search(int limit)
|
||||
@@ -243,6 +252,9 @@ QVariant PublicRoomListModel::data(const QModelIndex &index, int role) const
|
||||
|
||||
return m_connection->room(room.roomId, JoinState::Join) != nullptr;
|
||||
}
|
||||
if (role == IsSpaceRole) {
|
||||
return room.roomType == QLatin1String("m.space");
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
@@ -259,6 +271,7 @@ QHash<int, QByteArray> PublicRoomListModel::roleNames() const
|
||||
roles[AllowGuestsRole] = "allowGuests";
|
||||
roles[WorldReadableRole] = "worldReadable";
|
||||
roles[IsJoinedRole] = "isJoined";
|
||||
roles[IsSpaceRole] = "isSpace";
|
||||
roles[AliasRole] = "alias";
|
||||
|
||||
return roles;
|
||||
|
||||
@@ -69,6 +69,7 @@ public:
|
||||
AllowGuestsRole, /**< Whether the room allows guest users. */
|
||||
WorldReadableRole, /**< Whether the room events can be seen by non-members. */
|
||||
IsJoinedRole, /**< Whether the local user has joined the room. */
|
||||
IsSpaceRole, /**< Whether the room is a space. */
|
||||
};
|
||||
|
||||
explicit PublicRoomListModel(QObject *parent = nullptr);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
#include <Quotient/user.h>
|
||||
|
||||
ReactionModel::ReactionModel(const Quotient::RoomMessageEvent *event, const NeoChatRoom *room)
|
||||
ReactionModel::ReactionModel(const Quotient::RoomMessageEvent *event, NeoChatRoom *room)
|
||||
: QAbstractListModel(nullptr)
|
||||
, m_room(room)
|
||||
, m_event(event)
|
||||
@@ -162,6 +162,30 @@ QHash<int, QByteArray> ReactionModel::roleNames() const
|
||||
};
|
||||
}
|
||||
|
||||
bool isEmoji(const QString &text)
|
||||
{
|
||||
#ifdef HAVE_ICU
|
||||
QTextBoundaryFinder finder(QTextBoundaryFinder::Grapheme, text);
|
||||
int from = 0;
|
||||
while (finder.toNextBoundary() != -1) {
|
||||
auto to = finder.position();
|
||||
if (text[from].isSpace()) {
|
||||
from = to;
|
||||
continue;
|
||||
}
|
||||
|
||||
auto first = text.mid(from, to - from).toUcs4()[0];
|
||||
if (!u_hasBinaryProperty(first, UCHAR_EMOJI)) {
|
||||
return false;
|
||||
}
|
||||
from = to;
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
QString ReactionModel::reactionText(QString text) const
|
||||
{
|
||||
text = text.toHtmlEscaped();
|
||||
@@ -174,28 +198,6 @@ QString ReactionModel::reactionText(QString text) const
|
||||
return QStringLiteral("<img src=\"%1\" width=\"%2\" height=\"%2\">")
|
||||
.arg(m_room->connection()->makeMediaUrl(QUrl(text)).toString(), QString::number(size));
|
||||
}
|
||||
const auto isEmoji = [](const QString &text) {
|
||||
#ifdef HAVE_ICU
|
||||
QTextBoundaryFinder finder(QTextBoundaryFinder::Grapheme, text);
|
||||
int from = 0;
|
||||
while (finder.toNextBoundary() != -1) {
|
||||
auto to = finder.position();
|
||||
if (text[from].isSpace()) {
|
||||
from = to;
|
||||
continue;
|
||||
}
|
||||
|
||||
auto first = text.mid(from, to - from).toUcs4()[0];
|
||||
if (!u_hasBinaryProperty(first, UCHAR_EMOJI_PRESENTATION)) {
|
||||
return false;
|
||||
}
|
||||
from = to;
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
};
|
||||
|
||||
return isEmoji(text) ? QStringLiteral("<span style=\"font-family: 'emoji';\">") + text + QStringLiteral("</span>") : text;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ public:
|
||||
HasLocalUser, /**< Whether the local user is in the list of authors. */
|
||||
};
|
||||
|
||||
explicit ReactionModel(const Quotient::RoomMessageEvent *event, const NeoChatRoom *room);
|
||||
explicit ReactionModel(const Quotient::RoomMessageEvent *event, NeoChatRoom *room);
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
@@ -68,7 +68,7 @@ public:
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
private:
|
||||
const NeoChatRoom *m_room;
|
||||
QPointer<NeoChatRoom> m_room;
|
||||
const Quotient::RoomMessageEvent *m_event;
|
||||
QList<Reaction> m_reactions;
|
||||
QMap<QString, QString> m_shortcodes;
|
||||
|
||||
@@ -9,17 +9,7 @@
|
||||
#include "roommanager.h"
|
||||
#include "spacehierarchycache.h"
|
||||
|
||||
#include <QDebug>
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
|
||||
#ifndef Q_OS_ANDROID
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusInterface>
|
||||
#include <QDBusMessage>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#include <KLocalizedString>
|
||||
#include <QGuiApplication>
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
@@ -28,37 +18,6 @@ Q_DECLARE_METATYPE(Quotient::JoinState)
|
||||
RoomListModel::RoomListModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
const auto collapsedSections = NeoChatConfig::collapsedSections();
|
||||
for (auto collapsedSection : collapsedSections) {
|
||||
m_categoryVisibility[collapsedSection] = false;
|
||||
}
|
||||
|
||||
connect(this, &RoomListModel::highlightCountChanged, this, [this]() {
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
|
||||
#ifndef Q_OS_ANDROID
|
||||
// copied from Telegram desktop
|
||||
const auto launcherUrl = "application://org.kde.neochat.desktop"_ls;
|
||||
// Gnome requires that count is a 64bit integer
|
||||
const qint64 counterSlice = std::min(m_highlightCount, 9999);
|
||||
QVariantMap dbusUnityProperties;
|
||||
|
||||
if (counterSlice > 0) {
|
||||
dbusUnityProperties["count"_ls] = counterSlice;
|
||||
dbusUnityProperties["count-visible"_ls] = true;
|
||||
} else {
|
||||
dbusUnityProperties["count-visible"_ls] = false;
|
||||
}
|
||||
|
||||
auto signal = QDBusMessage::createSignal("/com/canonical/unity/launcherentry/neochat"_ls, "com.canonical.Unity.LauncherEntry"_ls, "Update"_ls);
|
||||
|
||||
signal.setArguments({launcherUrl, dbusUnityProperties});
|
||||
|
||||
QDBusConnection::sessionBus().send(signal);
|
||||
#endif // Q_OS_ANDROID
|
||||
#else
|
||||
qGuiApp->setBadgeNumber(m_highlightCount);
|
||||
#endif // QT_VERSION_CHECK(6, 6, 0)
|
||||
});
|
||||
connect(&SpaceHierarchyCache::instance(), &SpaceHierarchyCache::spaceHierarchyChanged, this, [this]() {
|
||||
Q_EMIT dataChanged(index(0, 0), index(rowCount(), 0), {IsChildSpaceRole});
|
||||
});
|
||||
@@ -127,7 +86,6 @@ void RoomListModel::doResetModel()
|
||||
doAddRoom(room);
|
||||
}
|
||||
endResetModel();
|
||||
refreshNotificationCount();
|
||||
}
|
||||
|
||||
NeoChatRoom *RoomListModel::roomAt(int row) const
|
||||
@@ -153,7 +111,7 @@ void RoomListModel::connectRoomSignals(NeoChatRoom *room)
|
||||
refresh(room, {DisplayNameRole});
|
||||
});
|
||||
connect(room, &Room::unreadStatsChanged, this, [this, room] {
|
||||
refresh(room, {NotificationCountRole, HighlightCountRole});
|
||||
refresh(room, {ContextNotificationCountRole, HasHighlightNotificationsRole});
|
||||
});
|
||||
connect(room, &Room::notificationCountChanged, this, [this, room] {
|
||||
refresh(room);
|
||||
@@ -176,44 +134,6 @@ void RoomListModel::connectRoomSignals(NeoChatRoom *room)
|
||||
connect(room, &Room::pendingEventMerged, this, [this, room] {
|
||||
refresh(room, {SubtitleTextRole});
|
||||
});
|
||||
connect(room, &Room::unreadStatsChanged, this, &RoomListModel::refreshNotificationCount);
|
||||
connect(room, &Room::highlightCountChanged, this, &RoomListModel::refreshHighlightCount);
|
||||
}
|
||||
|
||||
int RoomListModel::notificationCount() const
|
||||
{
|
||||
return m_notificationCount;
|
||||
}
|
||||
|
||||
int RoomListModel::highlightCount() const
|
||||
{
|
||||
return m_highlightCount;
|
||||
}
|
||||
|
||||
void RoomListModel::refreshNotificationCount()
|
||||
{
|
||||
int count = 0;
|
||||
for (auto room : std::as_const(m_rooms)) {
|
||||
count += room->notificationCount();
|
||||
}
|
||||
if (m_notificationCount == count) {
|
||||
return;
|
||||
}
|
||||
m_notificationCount = count;
|
||||
Q_EMIT notificationCountChanged();
|
||||
}
|
||||
|
||||
void RoomListModel::refreshHighlightCount()
|
||||
{
|
||||
int count = 0;
|
||||
for (auto room : std::as_const(m_rooms)) {
|
||||
count += room->highlightCount();
|
||||
}
|
||||
if (m_highlightCount == count) {
|
||||
return;
|
||||
}
|
||||
m_highlightCount = count;
|
||||
Q_EMIT highlightCountChanged();
|
||||
}
|
||||
|
||||
void RoomListModel::updateRoom(Room *room, Room *prev)
|
||||
@@ -288,6 +208,9 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
|
||||
if (role == DisplayNameRole) {
|
||||
return room->displayName();
|
||||
}
|
||||
if (role == EscapedDisplayNameRole) {
|
||||
return room->displayName().toHtmlEscaped();
|
||||
}
|
||||
if (role == AvatarRole) {
|
||||
return room->avatarMediaId();
|
||||
}
|
||||
@@ -298,13 +221,13 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
|
||||
return room->topic();
|
||||
}
|
||||
if (role == CategoryRole) {
|
||||
return category(room);
|
||||
return NeoChatRoomType::typeForRoom(room);
|
||||
}
|
||||
if (role == NotificationCountRole) {
|
||||
return room->notificationCount();
|
||||
if (role == ContextNotificationCountRole) {
|
||||
return room->contextAwareNotificationCount();
|
||||
}
|
||||
if (role == HighlightCountRole) {
|
||||
return room->highlightCount();
|
||||
if (role == HasHighlightNotificationsRole) {
|
||||
return room->highlightCount() > 0 && room->contextAwareNotificationCount() > 0;
|
||||
}
|
||||
if (role == LastActiveTimeRole) {
|
||||
return room->lastActiveTime();
|
||||
@@ -318,9 +241,6 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
|
||||
if (role == CurrentRoomRole) {
|
||||
return QVariant::fromValue(room);
|
||||
}
|
||||
if (role == CategoryVisibleRole) {
|
||||
return m_categoryVisibility.value(data(index, CategoryRole).toInt(), true);
|
||||
}
|
||||
if (role == SubtitleTextRole) {
|
||||
if (room->lastEvent() == nullptr || room->lastEventIsSpoiler()) {
|
||||
return QString();
|
||||
@@ -365,16 +285,16 @@ QHash<int, QByteArray> RoomListModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[DisplayNameRole] = "displayName";
|
||||
roles[EscapedDisplayNameRole] = "escapedDisplayName";
|
||||
roles[AvatarRole] = "avatar";
|
||||
roles[CanonicalAliasRole] = "canonicalAlias";
|
||||
roles[TopicRole] = "topic";
|
||||
roles[CategoryRole] = "category";
|
||||
roles[NotificationCountRole] = "notificationCount";
|
||||
roles[HighlightCountRole] = "highlightCount";
|
||||
roles[ContextNotificationCountRole] = "contextNotificationCount";
|
||||
roles[HasHighlightNotificationsRole] = "hasHighlightNotifications";
|
||||
roles[LastActiveTimeRole] = "lastActiveTime";
|
||||
roles[JoinStateRole] = "joinState";
|
||||
roles[CurrentRoomRole] = "currentRoom";
|
||||
roles[CategoryVisibleRole] = "categoryVisible";
|
||||
roles[SubtitleTextRole] = "subtitleText";
|
||||
roles[IsSpaceRole] = "isSpace";
|
||||
roles[RoomIdRole] = "roomId";
|
||||
@@ -383,87 +303,6 @@ QHash<int, QByteArray> RoomListModel::roleNames() const
|
||||
return roles;
|
||||
}
|
||||
|
||||
NeoChatRoomType::Types RoomListModel::category(NeoChatRoom *room)
|
||||
{
|
||||
if (room->isSpace()) {
|
||||
return NeoChatRoomType::Space;
|
||||
}
|
||||
if (room->joinState() == JoinState::Invite) {
|
||||
return NeoChatRoomType::Invited;
|
||||
}
|
||||
if (room->isFavourite()) {
|
||||
return NeoChatRoomType::Favorite;
|
||||
}
|
||||
if (room->isLowPriority()) {
|
||||
return NeoChatRoomType::Deprioritized;
|
||||
}
|
||||
if (room->isDirectChat()) {
|
||||
return NeoChatRoomType::Direct;
|
||||
}
|
||||
return NeoChatRoomType::Normal;
|
||||
}
|
||||
|
||||
QString RoomListModel::categoryName(int category)
|
||||
{
|
||||
switch (category) {
|
||||
case NeoChatRoomType::Invited:
|
||||
return i18n("Invited");
|
||||
case NeoChatRoomType::Favorite:
|
||||
return i18n("Favorite");
|
||||
case NeoChatRoomType::Direct:
|
||||
return i18n("Friends");
|
||||
case NeoChatRoomType::Normal:
|
||||
return i18n("Normal");
|
||||
case NeoChatRoomType::Deprioritized:
|
||||
return i18n("Low priority");
|
||||
case NeoChatRoomType::Space:
|
||||
return i18n("Spaces");
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
QString RoomListModel::categoryIconName(int category)
|
||||
{
|
||||
switch (category) {
|
||||
case NeoChatRoomType::Invited:
|
||||
return QStringLiteral("user-invisible");
|
||||
case NeoChatRoomType::Favorite:
|
||||
return QStringLiteral("favorite");
|
||||
case NeoChatRoomType::Direct:
|
||||
return QStringLiteral("dialog-messages");
|
||||
case NeoChatRoomType::Normal:
|
||||
return QStringLiteral("group");
|
||||
case NeoChatRoomType::Deprioritized:
|
||||
return QStringLiteral("object-order-lower");
|
||||
case NeoChatRoomType::Space:
|
||||
return QStringLiteral("group");
|
||||
default:
|
||||
return QStringLiteral("tools-report-bug");
|
||||
}
|
||||
}
|
||||
|
||||
void RoomListModel::setCategoryVisible(int category, bool visible)
|
||||
{
|
||||
beginResetModel();
|
||||
auto collapsedSections = NeoChatConfig::collapsedSections();
|
||||
if (visible) {
|
||||
collapsedSections.removeAll(category);
|
||||
} else {
|
||||
collapsedSections.push_back(category);
|
||||
}
|
||||
NeoChatConfig::setCollapsedSections(collapsedSections);
|
||||
NeoChatConfig::self()->save();
|
||||
|
||||
m_categoryVisibility[category] = visible;
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
bool RoomListModel::categoryVisible(int category) const
|
||||
{
|
||||
return m_categoryVisibility.value(category, true);
|
||||
}
|
||||
|
||||
NeoChatRoom *RoomListModel::roomByAliasOrId(const QString &aliasOrId)
|
||||
{
|
||||
for (const auto &room : std::as_const(m_rooms)) {
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include <QAbstractListModel>
|
||||
#include <QQmlEngine>
|
||||
|
||||
#include "enums/neochatroomtype.h"
|
||||
|
||||
class NeoChatRoom;
|
||||
|
||||
namespace Quotient
|
||||
@@ -14,27 +16,6 @@ class Connection;
|
||||
class Room;
|
||||
}
|
||||
|
||||
class NeoChatRoomType : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("")
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the room list categories a room can be assigned.
|
||||
*/
|
||||
enum Types {
|
||||
Invited = 1, /**< The user has been invited to the room. */
|
||||
Favorite, /**< The room is set as a favourite. */
|
||||
Direct, /**< The room is a direct chat. */
|
||||
Normal, /**< The default category for a joined room. */
|
||||
Deprioritized, /**< The room is set as low priority. */
|
||||
Space, /**< The room is a space. */
|
||||
};
|
||||
Q_ENUM(Types)
|
||||
};
|
||||
|
||||
/**
|
||||
* @class RoomListModel
|
||||
*
|
||||
@@ -50,27 +31,22 @@ class RoomListModel : public QAbstractListModel
|
||||
*/
|
||||
Q_PROPERTY(Quotient::Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
|
||||
|
||||
/**
|
||||
* @brief The total number of notifications for all the rooms.
|
||||
*/
|
||||
Q_PROPERTY(int notificationCount READ notificationCount NOTIFY notificationCountChanged)
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum EventRoles {
|
||||
DisplayNameRole = Qt::DisplayRole, /**< The display name of the room. */
|
||||
EscapedDisplayNameRole, /**< HTML-Escaped display name of the room. */
|
||||
AvatarRole, /**< The source URL for the room's avatar. */
|
||||
CanonicalAliasRole, /**< The room canonical alias. */
|
||||
TopicRole, /**< The room topic. */
|
||||
CategoryRole, /**< The room category, e.g favourite. */
|
||||
NotificationCountRole, /**< The number of notifications in the room. */
|
||||
HighlightCountRole, /**< The number of highlighted messages in the room. */
|
||||
ContextNotificationCountRole, /**< The context aware notification count for the room. */
|
||||
HasHighlightNotificationsRole, /**< Whether there are any highlight notifications. */
|
||||
LastActiveTimeRole, /**< The timestamp of the last event sent in the room. */
|
||||
JoinStateRole, /**< The local user's join state in the room. */
|
||||
CurrentRoomRole, /**< The room object for the room. */
|
||||
CategoryVisibleRole, /**< If the room's category is visible. */
|
||||
SubtitleTextRole, /**< The text to show as the room subtitle. */
|
||||
AvatarImageRole, /**< The room avatar as an image. */
|
||||
RoomIdRole, /**< The room matrix ID. */
|
||||
@@ -87,9 +63,6 @@ public:
|
||||
[[nodiscard]] Quotient::Connection *connection() const;
|
||||
void setConnection(Quotient::Connection *connection);
|
||||
|
||||
[[nodiscard]] int notificationCount() const;
|
||||
[[nodiscard]] int highlightCount() const;
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
@@ -116,35 +89,6 @@ public:
|
||||
*/
|
||||
Q_INVOKABLE [[nodiscard]] NeoChatRoom *roomAt(int row) const;
|
||||
|
||||
/**
|
||||
* @brief The category for the given room.
|
||||
*/
|
||||
static NeoChatRoomType::Types category(NeoChatRoom *room);
|
||||
|
||||
/**
|
||||
* @brief Return a string to represent the given room category.
|
||||
*/
|
||||
Q_INVOKABLE [[nodiscard]] static QString categoryName(int category);
|
||||
|
||||
/**
|
||||
* @brief Return a string with the name of the given room category icon.
|
||||
*/
|
||||
Q_INVOKABLE [[nodiscard]] static QString categoryIconName(int category);
|
||||
|
||||
/**
|
||||
* @brief Set whether a given category should be visible or not.
|
||||
*
|
||||
* @param category the NeoChatRoomType::Types value for the category (it's an
|
||||
* int due to the pain of Q_INVOKABLES and cpp enums).
|
||||
* @param visible true if the category should be visible, false if not.
|
||||
*/
|
||||
Q_INVOKABLE void setCategoryVisible(int category, bool visible);
|
||||
|
||||
/**
|
||||
* @brief Return whether a room category is set to be visible.
|
||||
*/
|
||||
Q_INVOKABLE [[nodiscard]] bool categoryVisible(int category) const;
|
||||
|
||||
/**
|
||||
* @brief Return the model row for the given room.
|
||||
*/
|
||||
@@ -163,25 +107,16 @@ private Q_SLOTS:
|
||||
void updateRoom(Quotient::Room *room, Quotient::Room *prev);
|
||||
void deleteRoom(Quotient::Room *room);
|
||||
void refresh(NeoChatRoom *room, const QList<int> &roles = {});
|
||||
void refreshNotificationCount();
|
||||
void refreshHighlightCount();
|
||||
|
||||
private:
|
||||
Quotient::Connection *m_connection = nullptr;
|
||||
QList<NeoChatRoom *> m_rooms;
|
||||
|
||||
QMap<int, bool> m_categoryVisibility;
|
||||
|
||||
int m_notificationCount = 0;
|
||||
int m_highlightCount = 0;
|
||||
QString m_activeSpaceId;
|
||||
|
||||
void connectRoomSignals(NeoChatRoom *room);
|
||||
|
||||
Q_SIGNALS:
|
||||
void connectionChanged();
|
||||
void notificationCountChanged();
|
||||
void highlightCountChanged();
|
||||
|
||||
void roomAdded(NeoChatRoom *_t1);
|
||||
};
|
||||
|
||||
100
src/models/roomtreeitem.cpp
Normal file
100
src/models/roomtreeitem.cpp
Normal file
@@ -0,0 +1,100 @@
|
||||
// SPDX-FileCopyrightText: 2024 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-FileCopyrightText: 2024 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 "roomtreeitem.h"
|
||||
|
||||
RoomTreeItem::RoomTreeItem(TreeData data, RoomTreeItem *parent)
|
||||
: m_parentItem(parent)
|
||||
, m_data(data)
|
||||
{
|
||||
}
|
||||
|
||||
bool RoomTreeItem::operator==(const RoomTreeItem &other) const
|
||||
{
|
||||
if (std::holds_alternative<NeoChatRoomType::Types>(m_data) && std::holds_alternative<NeoChatRoomType::Types>(other.data())) {
|
||||
return std::get<NeoChatRoomType::Types>(m_data) == std::get<NeoChatRoomType::Types>(m_data);
|
||||
}
|
||||
if (std::holds_alternative<NeoChatRoom *>(m_data) && std::holds_alternative<NeoChatRoom *>(other.data())) {
|
||||
return std::get<NeoChatRoom *>(m_data)->id() == std::get<NeoChatRoom *>(m_data)->id();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
RoomTreeItem *RoomTreeItem::child(int row)
|
||||
{
|
||||
return row >= 0 && row < childCount() ? m_children.at(row).get() : nullptr;
|
||||
}
|
||||
|
||||
int RoomTreeItem::childCount() const
|
||||
{
|
||||
return int(m_children.size());
|
||||
}
|
||||
|
||||
bool RoomTreeItem::insertChild(std::unique_ptr<RoomTreeItem> newChild)
|
||||
{
|
||||
if (newChild == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (auto it = m_children.begin(), end = m_children.end(); it != end; ++it) {
|
||||
if (*it == newChild) {
|
||||
*it = std::move(newChild);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
m_children.push_back(std::move(newChild));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RoomTreeItem::removeChild(int row)
|
||||
{
|
||||
if (row < 0 || row >= childCount()) {
|
||||
return false;
|
||||
}
|
||||
m_children.erase(m_children.begin() + row);
|
||||
return true;
|
||||
}
|
||||
|
||||
int RoomTreeItem::row() const
|
||||
{
|
||||
if (m_parentItem == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto it = std::find_if(m_parentItem->m_children.cbegin(), m_parentItem->m_children.cend(), [this](const std::unique_ptr<RoomTreeItem> &treeItem) {
|
||||
return treeItem.get() == this;
|
||||
});
|
||||
|
||||
if (it != m_parentItem->m_children.cend()) {
|
||||
return std::distance(m_parentItem->m_children.cbegin(), it);
|
||||
}
|
||||
Q_ASSERT(false); // should not happen
|
||||
return -1;
|
||||
}
|
||||
|
||||
RoomTreeItem *RoomTreeItem::parentItem() const
|
||||
{
|
||||
return m_parentItem;
|
||||
}
|
||||
|
||||
RoomTreeItem::TreeData RoomTreeItem::data() const
|
||||
{
|
||||
return m_data;
|
||||
}
|
||||
|
||||
std::optional<int> RoomTreeItem::rowForRoom(Quotient::Room *room) const
|
||||
{
|
||||
Q_ASSERT_X(std::holds_alternative<NeoChatRoomType::Types>(m_data), __FUNCTION__, "rowForRoom only works items for rooms not categories");
|
||||
|
||||
int i = 0;
|
||||
for (const auto &child : m_children) {
|
||||
if (std::get<NeoChatRoom *>(child->data()) == room) {
|
||||
return i;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
78
src/models/roomtreeitem.h
Normal file
78
src/models/roomtreeitem.h
Normal file
@@ -0,0 +1,78 @@
|
||||
// SPDX-FileCopyrightText: 2024 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-FileCopyrightText: 2024 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 "enums/neochatroomtype.h"
|
||||
|
||||
class NeoChatRoom;
|
||||
|
||||
/**
|
||||
* @class RoomTreeItem
|
||||
*
|
||||
* This class defines an item in the space tree hierarchy model.
|
||||
*
|
||||
* @note This is separate from Quotient::Room and NeoChatRoom because we don't have
|
||||
* full room information for any room/space the user hasn't joined and we
|
||||
* don't want to create one for ever possible child in a space as that would
|
||||
* be expensive.
|
||||
*
|
||||
* @sa Quotient::Room, NeoChatRoom
|
||||
*/
|
||||
class RoomTreeItem
|
||||
{
|
||||
public:
|
||||
using TreeData = std::variant<NeoChatRoom *, NeoChatRoomType::Types>;
|
||||
|
||||
explicit RoomTreeItem(TreeData data, RoomTreeItem *parent = nullptr);
|
||||
|
||||
bool operator==(const RoomTreeItem &other) const;
|
||||
|
||||
/**
|
||||
* @brief Return the child at the given row number.
|
||||
*
|
||||
* Nullptr is returned if there is no child at the given row number.
|
||||
*/
|
||||
RoomTreeItem *child(int row);
|
||||
|
||||
/**
|
||||
* @brief The number of children this item has.
|
||||
*/
|
||||
int childCount() const;
|
||||
|
||||
/**
|
||||
* @brief Insert the given child.
|
||||
*/
|
||||
bool insertChild(std::unique_ptr<RoomTreeItem> newChild);
|
||||
|
||||
/**
|
||||
* @brief Remove the child at the given row number.
|
||||
*
|
||||
* @return True if a child was removed, false if the given row isn't valid.
|
||||
*/
|
||||
bool removeChild(int row);
|
||||
|
||||
/**
|
||||
* @brief Return this item's parent.
|
||||
*/
|
||||
RoomTreeItem *parentItem() const;
|
||||
|
||||
/**
|
||||
* @brief Return the row number for this child relative to the parent.
|
||||
*
|
||||
* @return The row value if the child has a parent, 0 otherwise.
|
||||
*/
|
||||
int row() const;
|
||||
|
||||
/**
|
||||
* @brief Return this item's data.
|
||||
*/
|
||||
TreeData data() const;
|
||||
|
||||
std::optional<int> rowForRoom(Quotient::Room *room) const;
|
||||
|
||||
private:
|
||||
std::vector<std::unique_ptr<RoomTreeItem>> m_children;
|
||||
RoomTreeItem *m_parentItem;
|
||||
|
||||
TreeData m_data;
|
||||
};
|
||||
419
src/models/roomtreemodel.cpp
Normal file
419
src/models/roomtreemodel.cpp
Normal file
@@ -0,0 +1,419 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "roomtreemodel.h"
|
||||
|
||||
#include <Quotient/connection.h>
|
||||
#include <Quotient/room.h>
|
||||
|
||||
#include "eventhandler.h"
|
||||
#include "neochatconnection.h"
|
||||
#include "neochatroomtype.h"
|
||||
#include "spacehierarchycache.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
RoomTreeModel::RoomTreeModel(QObject *parent)
|
||||
: QAbstractItemModel(parent)
|
||||
, m_rootItem(new RoomTreeItem(nullptr))
|
||||
{
|
||||
}
|
||||
|
||||
RoomTreeItem *RoomTreeModel::getItem(const QModelIndex &index) const
|
||||
{
|
||||
if (index.isValid()) {
|
||||
RoomTreeItem *item = static_cast<RoomTreeItem *>(index.internalPointer());
|
||||
if (item) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return m_rootItem.get();
|
||||
}
|
||||
|
||||
void RoomTreeModel::resetModel()
|
||||
{
|
||||
if (m_connection == nullptr) {
|
||||
beginResetModel();
|
||||
m_rootItem.reset();
|
||||
endResetModel();
|
||||
return;
|
||||
}
|
||||
|
||||
beginResetModel();
|
||||
m_rootItem.reset(new RoomTreeItem(nullptr));
|
||||
|
||||
for (int i = 0; i < NeoChatRoomType::TypesCount; i++) {
|
||||
m_rootItem->insertChild(std::make_unique<RoomTreeItem>(NeoChatRoomType::Types(i), m_rootItem.get()));
|
||||
}
|
||||
|
||||
for (const auto &r : m_connection->allRooms()) {
|
||||
const auto room = dynamic_cast<NeoChatRoom *>(r);
|
||||
const auto type = NeoChatRoomType::typeForRoom(room);
|
||||
const auto categoryItem = m_rootItem->child(type);
|
||||
if (categoryItem->insertChild(std::make_unique<RoomTreeItem>(room, categoryItem))) {
|
||||
connectRoomSignals(room);
|
||||
}
|
||||
}
|
||||
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void RoomTreeModel::setConnection(NeoChatConnection *connection)
|
||||
{
|
||||
if (m_connection == connection) {
|
||||
return;
|
||||
}
|
||||
if (m_connection) {
|
||||
disconnect(m_connection.get(), nullptr, this, nullptr);
|
||||
}
|
||||
m_connection = connection;
|
||||
|
||||
resetModel();
|
||||
|
||||
connect(connection, &Connection::newRoom, this, &RoomTreeModel::newRoom);
|
||||
connect(connection, &Connection::leftRoom, this, &RoomTreeModel::leftRoom);
|
||||
connect(connection, &Connection::aboutToDeleteRoom, this, &RoomTreeModel::leftRoom);
|
||||
|
||||
Q_EMIT connectionChanged();
|
||||
}
|
||||
|
||||
void RoomTreeModel::newRoom(Room *r)
|
||||
{
|
||||
const auto room = dynamic_cast<NeoChatRoom *>(r);
|
||||
const auto type = NeoChatRoomType::typeForRoom(room);
|
||||
// Check if the room is already in the model.
|
||||
const auto checkRoomIndex = indexForRoom(room);
|
||||
if (checkRoomIndex.isValid()) {
|
||||
// If the room is in the wrong type category for whatever reason, move it.
|
||||
if (checkRoomIndex.parent().row() != type) {
|
||||
moveRoom(room);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const auto parentItem = m_rootItem->child(type);
|
||||
beginInsertRows(index(parentItem->row(), 0), parentItem->childCount(), parentItem->childCount());
|
||||
parentItem->insertChild(std::make_unique<RoomTreeItem>(room, parentItem));
|
||||
connectRoomSignals(room);
|
||||
endInsertRows();
|
||||
qWarning() << "adding room" << type << "new count" << parentItem->childCount();
|
||||
}
|
||||
|
||||
void RoomTreeModel::leftRoom(Room *r)
|
||||
{
|
||||
const auto room = dynamic_cast<NeoChatRoom *>(r);
|
||||
auto index = indexForRoom(room);
|
||||
if (!index.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto parentItem = getItem(index.parent());
|
||||
Q_ASSERT(parentItem);
|
||||
|
||||
beginRemoveRows(index.parent(), index.row(), index.row());
|
||||
parentItem->removeChild(index.row());
|
||||
room->disconnect(this);
|
||||
endRemoveRows();
|
||||
}
|
||||
|
||||
void RoomTreeModel::moveRoom(Quotient::Room *room)
|
||||
{
|
||||
// We can't assume the type as it has changed so currently the return of
|
||||
// NeoChatRoomType::typeForRoom doesn't match it's current location. So find the room.
|
||||
NeoChatRoomType::Types oldType;
|
||||
int oldRow = -1;
|
||||
for (int i = 0; i < NeoChatRoomType::TypesCount; i++) {
|
||||
const auto categoryItem = m_rootItem->child(i);
|
||||
const auto row = categoryItem->rowForRoom(room);
|
||||
if (row) {
|
||||
oldType = static_cast<NeoChatRoomType::Types>(i);
|
||||
oldRow = *row;
|
||||
}
|
||||
}
|
||||
|
||||
if (oldRow == -1) {
|
||||
return;
|
||||
}
|
||||
auto neochatRoom = dynamic_cast<NeoChatRoom *>(room);
|
||||
const auto newType = NeoChatRoomType::typeForRoom(neochatRoom);
|
||||
if (newType == oldType) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto oldParent = index(oldType, 0, {});
|
||||
auto oldParentItem = getItem(oldParent);
|
||||
Q_ASSERT(oldParentItem);
|
||||
|
||||
const auto newParent = index(newType, 0, {});
|
||||
auto newParentItem = getItem(newParent);
|
||||
Q_ASSERT(newParentItem);
|
||||
|
||||
// HACK: We're doing this as a remove then insert because moving doesn't work
|
||||
// properly with DelegateChooser for whatever reason.
|
||||
Q_ASSERT(checkIndex(index(oldRow, 0, oldParent), QAbstractItemModel::CheckIndexOption::IndexIsValid));
|
||||
beginRemoveRows(oldParent, oldRow, oldRow);
|
||||
const bool success = oldParentItem->removeChild(oldRow);
|
||||
Q_ASSERT(success);
|
||||
endRemoveRows();
|
||||
beginInsertRows(newParent, newParentItem->childCount(), newParentItem->childCount());
|
||||
newParentItem->insertChild(std::make_unique<RoomTreeItem>(neochatRoom, newParentItem));
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
void RoomTreeModel::connectRoomSignals(NeoChatRoom *room)
|
||||
{
|
||||
connect(room, &Room::displaynameChanged, this, [this, room] {
|
||||
refreshRoomRoles(room, {DisplayNameRole});
|
||||
});
|
||||
connect(room, &Room::unreadStatsChanged, this, [this, room] {
|
||||
refreshRoomRoles(room, {ContextNotificationCountRole, HasHighlightNotificationsRole});
|
||||
});
|
||||
connect(room, &Room::avatarChanged, this, [this, room] {
|
||||
refreshRoomRoles(room, {AvatarRole});
|
||||
});
|
||||
connect(room, &Room::tagsChanged, this, [this, room] {
|
||||
moveRoom(room);
|
||||
});
|
||||
connect(room, &Room::joinStateChanged, this, [this, room] {
|
||||
refreshRoomRoles(room);
|
||||
});
|
||||
connect(room, &Room::addedMessages, this, [this, room] {
|
||||
refreshRoomRoles(room, {SubtitleTextRole, LastActiveTimeRole});
|
||||
});
|
||||
connect(room, &Room::pendingEventMerged, this, [this, room] {
|
||||
refreshRoomRoles(room, {SubtitleTextRole});
|
||||
});
|
||||
connect(room, &NeoChatRoom::pushNotificationStateChanged, this, [this, room] {
|
||||
refreshRoomRoles(room, {ContextNotificationCountRole, HasHighlightNotificationsRole});
|
||||
});
|
||||
}
|
||||
|
||||
void RoomTreeModel::refreshRoomRoles(NeoChatRoom *room, const QList<int> &roles)
|
||||
{
|
||||
const auto index = indexForRoom(room);
|
||||
if (!index.isValid()) {
|
||||
qCritical() << "Room" << room->id() << "not found in the room list";
|
||||
return;
|
||||
}
|
||||
Q_EMIT dataChanged(index, index, roles);
|
||||
}
|
||||
|
||||
NeoChatConnection *RoomTreeModel::connection() const
|
||||
{
|
||||
return m_connection;
|
||||
}
|
||||
|
||||
int RoomTreeModel::columnCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent)
|
||||
return 1;
|
||||
}
|
||||
|
||||
int RoomTreeModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
RoomTreeItem *parentItem;
|
||||
if (parent.column() > 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!parent.isValid()) {
|
||||
parentItem = m_rootItem.get();
|
||||
} else {
|
||||
parentItem = static_cast<RoomTreeItem *>(parent.internalPointer());
|
||||
}
|
||||
|
||||
return parentItem->childCount();
|
||||
}
|
||||
|
||||
QModelIndex RoomTreeModel::parent(const QModelIndex &index) const
|
||||
{
|
||||
if (!index.isValid()) {
|
||||
return QModelIndex();
|
||||
}
|
||||
|
||||
RoomTreeItem *childItem = static_cast<RoomTreeItem *>(index.internalPointer());
|
||||
if (!childItem) {
|
||||
return QModelIndex();
|
||||
}
|
||||
RoomTreeItem *parentItem = childItem->parentItem();
|
||||
|
||||
if (parentItem == m_rootItem.get()) {
|
||||
return QModelIndex();
|
||||
}
|
||||
|
||||
return createIndex(parentItem->row(), 0, parentItem);
|
||||
}
|
||||
|
||||
QModelIndex RoomTreeModel::index(int row, int column, const QModelIndex &parent) const
|
||||
{
|
||||
if (!hasIndex(row, column, parent)) {
|
||||
return QModelIndex();
|
||||
}
|
||||
|
||||
RoomTreeItem *parentItem = getItem(parent);
|
||||
if (!parentItem) {
|
||||
return QModelIndex();
|
||||
}
|
||||
|
||||
RoomTreeItem *childItem = parentItem->child(row);
|
||||
if (childItem) {
|
||||
return createIndex(row, column, childItem);
|
||||
}
|
||||
return QModelIndex();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> RoomTreeModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[DisplayNameRole] = "displayName";
|
||||
roles[AvatarRole] = "avatar";
|
||||
roles[CanonicalAliasRole] = "canonicalAlias";
|
||||
roles[TopicRole] = "topic";
|
||||
roles[CategoryRole] = "category";
|
||||
roles[ContextNotificationCountRole] = "contextNotificationCount";
|
||||
roles[HasHighlightNotificationsRole] = "hasHighlightNotifications";
|
||||
roles[LastActiveTimeRole] = "lastActiveTime";
|
||||
roles[JoinStateRole] = "joinState";
|
||||
roles[CurrentRoomRole] = "currentRoom";
|
||||
roles[SubtitleTextRole] = "subtitleText";
|
||||
roles[IsSpaceRole] = "isSpace";
|
||||
roles[RoomIdRole] = "roomId";
|
||||
roles[IsChildSpaceRole] = "isChildSpace";
|
||||
roles[IsDirectChat] = "isDirectChat";
|
||||
roles[DelegateTypeRole] = "delegateType";
|
||||
roles[IconRole] = "icon";
|
||||
roles[AttentionRole] = "attention";
|
||||
roles[FavouriteRole] = "favourite";
|
||||
return roles;
|
||||
}
|
||||
|
||||
// TODO room type changes
|
||||
QVariant RoomTreeModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid)) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
RoomTreeItem *child = getItem(index);
|
||||
if (std::holds_alternative<NeoChatRoomType::Types>(child->data())) {
|
||||
if (role == DisplayNameRole) {
|
||||
return NeoChatRoomType::typeName(index.row());
|
||||
}
|
||||
if (role == DelegateTypeRole) {
|
||||
if (index.row() == NeoChatRoomType::Search) {
|
||||
return QStringLiteral("search");
|
||||
}
|
||||
if (index.row() == NeoChatRoomType::AddDirect) {
|
||||
return QStringLiteral("addDirect");
|
||||
}
|
||||
return QStringLiteral("section");
|
||||
}
|
||||
if (role == IconRole) {
|
||||
return NeoChatRoomType::typeIconName(index.row());
|
||||
}
|
||||
if (role == CategoryRole) {
|
||||
return index.row();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto room = std::get<NeoChatRoom *>(child->data());
|
||||
Q_ASSERT(room);
|
||||
|
||||
if (role == DisplayNameRole) {
|
||||
return room->displayName();
|
||||
}
|
||||
if (role == AvatarRole) {
|
||||
return room->avatarMediaId();
|
||||
}
|
||||
if (role == CanonicalAliasRole) {
|
||||
return room->canonicalAlias();
|
||||
}
|
||||
if (role == TopicRole) {
|
||||
return room->topic();
|
||||
}
|
||||
if (role == CategoryRole) {
|
||||
return NeoChatRoomType::typeForRoom(room);
|
||||
}
|
||||
if (role == ContextNotificationCountRole) {
|
||||
return int(room->contextAwareNotificationCount());
|
||||
}
|
||||
if (role == HasHighlightNotificationsRole) {
|
||||
return room->highlightCount() > 0 && room->contextAwareNotificationCount() > 0;
|
||||
}
|
||||
if (role == LastActiveTimeRole) {
|
||||
return room->lastActiveTime();
|
||||
}
|
||||
if (role == JoinStateRole) {
|
||||
if (!room->successorId().isEmpty()) {
|
||||
return QStringLiteral("upgraded");
|
||||
}
|
||||
return QVariant::fromValue(room->joinState());
|
||||
}
|
||||
if (role == CurrentRoomRole) {
|
||||
return QVariant::fromValue(room);
|
||||
}
|
||||
if (role == SubtitleTextRole) {
|
||||
if (room->lastEvent() == nullptr || room->lastEventIsSpoiler()) {
|
||||
return QString();
|
||||
}
|
||||
EventHandler eventHandler(room, room->lastEvent());
|
||||
return eventHandler.subtitleText();
|
||||
}
|
||||
if (role == AvatarImageRole) {
|
||||
return room->avatar(128);
|
||||
}
|
||||
if (role == RoomIdRole) {
|
||||
return room->id();
|
||||
}
|
||||
if (role == IsSpaceRole) {
|
||||
return room->isSpace();
|
||||
}
|
||||
if (role == IsChildSpaceRole) {
|
||||
return SpaceHierarchyCache::instance().isChild(room->id());
|
||||
}
|
||||
if (role == ReplacementIdRole) {
|
||||
return room->successorId();
|
||||
}
|
||||
if (role == IsDirectChat) {
|
||||
return room->isDirectChat();
|
||||
}
|
||||
if (role == DelegateTypeRole) {
|
||||
return QStringLiteral("normal");
|
||||
}
|
||||
if (role == AttentionRole) {
|
||||
return room->notificationCount() + room->highlightCount() > 0;
|
||||
}
|
||||
if (role == FavouriteRole) {
|
||||
return room->isFavourite();
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
QModelIndex RoomTreeModel::indexForRoom(NeoChatRoom *room) const
|
||||
{
|
||||
if (room == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Try and find by checking type.
|
||||
const auto type = NeoChatRoomType::typeForRoom(room);
|
||||
const auto parentItem = m_rootItem->child(type);
|
||||
const auto row = parentItem->rowForRoom(room);
|
||||
if (row) {
|
||||
return index(*row, 0, index(type, 0));
|
||||
}
|
||||
// Double check that the room isn't in the wrong category.
|
||||
for (int i = 0; i < NeoChatRoomType::TypesCount; i++) {
|
||||
const auto parentItem = m_rootItem->child(i);
|
||||
const auto row = parentItem->rowForRoom(room);
|
||||
if (row) {
|
||||
return index(*row, 0, index(i, 0));
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
#include "moc_roomtreemodel.cpp"
|
||||
99
src/models/roomtreemodel.h
Normal file
99
src/models/roomtreemodel.h
Normal file
@@ -0,0 +1,99 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include <QPointer>
|
||||
|
||||
#include "enums/neochatroomtype.h"
|
||||
#include "roomtreeitem.h"
|
||||
|
||||
namespace Quotient
|
||||
{
|
||||
class Room;
|
||||
}
|
||||
|
||||
class NeoChatConnection;
|
||||
class NeoChatRoom;
|
||||
|
||||
class RoomTreeModel : public QAbstractItemModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum EventRoles {
|
||||
DisplayNameRole = Qt::DisplayRole, /**< The display name of the room. */
|
||||
AvatarRole, /**< The source URL for the room's avatar. */
|
||||
CanonicalAliasRole, /**< The room canonical alias. */
|
||||
TopicRole, /**< The room topic. */
|
||||
CategoryRole, /**< The room category, e.g favourite. */
|
||||
ContextNotificationCountRole, /**< The context aware notification count for the room. */
|
||||
HasHighlightNotificationsRole, /**< Whether there are any highlight notifications. */
|
||||
LastActiveTimeRole, /**< The timestamp of the last event sent in the room. */
|
||||
JoinStateRole, /**< The local user's join state in the room. */
|
||||
CurrentRoomRole, /**< The room object for the room. */
|
||||
SubtitleTextRole, /**< The text to show as the room subtitle. */
|
||||
AvatarImageRole, /**< The room avatar as an image. */
|
||||
RoomIdRole, /**< The room matrix ID. */
|
||||
IsSpaceRole, /**< Whether the room is a space. */
|
||||
IsChildSpaceRole, /**< Whether this space is a child of a different space. */
|
||||
ReplacementIdRole, /**< The room id of the room replacing this one, if any. */
|
||||
IsDirectChat, /**< Whether this room is a direct chat. */
|
||||
DelegateTypeRole,
|
||||
IconRole,
|
||||
AttentionRole, /**< Whether there are any notifications. */
|
||||
FavouriteRole, /**< Whether the room is favourited. */
|
||||
};
|
||||
Q_ENUM(EventRoles)
|
||||
explicit RoomTreeModel(QObject *parent = nullptr);
|
||||
|
||||
void setConnection(NeoChatConnection *connection);
|
||||
NeoChatConnection *connection() const;
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa EventRoles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
QModelIndex parent(const QModelIndex &index) const override;
|
||||
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
Q_INVOKABLE QModelIndex indexForRoom(NeoChatRoom *room) const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void connectionChanged();
|
||||
|
||||
private:
|
||||
QPointer<NeoChatConnection> m_connection;
|
||||
std::unique_ptr<RoomTreeItem> m_rootItem;
|
||||
|
||||
RoomTreeItem *getItem(const QModelIndex &index) const;
|
||||
|
||||
void resetModel();
|
||||
void connectRoomSignals(NeoChatRoom *room);
|
||||
|
||||
void newRoom(Quotient::Room *room);
|
||||
void leftRoom(Quotient::Room *room);
|
||||
void moveRoom(Quotient::Room *room);
|
||||
|
||||
void refreshRoomRoles(NeoChatRoom *room, const QList<int> &roles = {});
|
||||
};
|
||||
@@ -121,7 +121,7 @@ private:
|
||||
void setSearching(bool searching);
|
||||
|
||||
QString m_searchText;
|
||||
NeoChatRoom *m_room = nullptr;
|
||||
QPointer<NeoChatRoom> m_room;
|
||||
Quotient::Omittable<Quotient::SearchJob::ResultRoomEvents> m_result = Quotient::none;
|
||||
Quotient::SearchJob *m_job = nullptr;
|
||||
bool m_searching = false;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user