Compare commits
155 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b901ea6e2a | ||
|
|
8cfd515db2 | ||
|
|
3e5181d64e | ||
|
|
ae53bf5df2 | ||
|
|
d2ed304672 | ||
|
|
4924bd05a8 | ||
|
|
9aa7553a1f | ||
|
|
1c910165c1 | ||
|
|
05a84da722 | ||
|
|
1ab8b85f06 | ||
|
|
05883bcb71 | ||
|
|
20cb6dc864 | ||
|
|
b6cf60acdb | ||
|
|
d4a6a41981 | ||
|
|
8c2682c943 | ||
|
|
9c56561853 | ||
|
|
ab9410cc03 | ||
|
|
43fae7af04 | ||
|
|
0cf19d21f2 | ||
|
|
ce448bd027 | ||
|
|
056e91df9f | ||
|
|
258815ca10 | ||
|
|
174373fb15 | ||
|
|
aa0790d7fd | ||
|
|
e6c589c6ac | ||
|
|
4b1805bdaa | ||
|
|
6055460bff | ||
|
|
0642685874 | ||
|
|
e2b7e6778e | ||
|
|
22bf9b8a59 | ||
|
|
85fc1a1f46 | ||
|
|
624123407c | ||
|
|
7bad41739f | ||
|
|
ee16504aa0 | ||
|
|
cf308bcdce | ||
|
|
6fbfa48c77 | ||
|
|
67d71cb590 | ||
|
|
c5817df2c9 | ||
|
|
b94fcd6858 | ||
|
|
8a8874fcb6 | ||
|
|
b593f7321b | ||
|
|
5002258e34 | ||
|
|
f75c194e7c | ||
|
|
ecf93de006 | ||
|
|
7feb02c7d8 | ||
|
|
0764fe0dd9 | ||
|
|
50a7138633 | ||
|
|
96a477f91c | ||
|
|
5a6b0f756d | ||
|
|
dbbf975f7a | ||
|
|
e06c2d2f93 | ||
|
|
5dea17b616 | ||
|
|
63b6d7ebe0 | ||
|
|
652106e6a1 | ||
|
|
3e158b3e60 | ||
|
|
b9ec33dd94 | ||
|
|
69bd1202ba | ||
|
|
f75c09e130 | ||
|
|
683d216f44 | ||
|
|
c10bcf1764 | ||
|
|
9e2bf0da26 | ||
|
|
51f7de117d | ||
|
|
f361f4e2d8 | ||
|
|
acfb5ab834 | ||
|
|
8bc4500ce1 | ||
|
|
b96d3dde46 | ||
|
|
fad381c36f | ||
|
|
ad083f64b1 | ||
|
|
5c78b23cc2 | ||
|
|
2202063641 | ||
|
|
5be15ffa6a | ||
|
|
a0bafe53a0 | ||
|
|
1a39ce9585 | ||
|
|
4a809d57f7 | ||
|
|
c01e42c972 | ||
|
|
6c56f7f4ef | ||
|
|
6d8d5a82c2 | ||
|
|
daa27b0333 | ||
|
|
f6edf0e4cc | ||
|
|
356e8eefe0 | ||
|
|
7ad362225f | ||
|
|
d623a8c826 | ||
|
|
b3f0d110d9 | ||
|
|
612b5d7f47 | ||
|
|
7e9f206348 | ||
|
|
d5f4a3dd64 | ||
|
|
e807ad9908 | ||
|
|
2d8ad834a7 | ||
|
|
d82dfc7a5b | ||
|
|
1e1e54d4bd | ||
|
|
9043b1c7a1 | ||
|
|
0739f4c661 | ||
|
|
29321aeaf3 | ||
|
|
9fe44515a7 | ||
|
|
4c3d7ab011 | ||
|
|
d02eee6daa | ||
|
|
a613baa148 | ||
|
|
4e141e05f0 | ||
|
|
134f1d2ae5 | ||
|
|
9ec3e5b2cc | ||
|
|
15d066b3ed | ||
|
|
7694f6c464 | ||
|
|
46da4f9777 | ||
|
|
44e0ef43ac | ||
|
|
5b79371c0a | ||
|
|
3caa5ad2ed | ||
|
|
a74d78439c | ||
|
|
0b82aeb5cc | ||
|
|
bc7aa4e272 | ||
|
|
d1f421994d | ||
|
|
b043cbedc9 | ||
|
|
5a469ed126 | ||
|
|
8d209a5424 | ||
|
|
a0937bdcac | ||
|
|
df4eba0191 | ||
|
|
9f83cf1c52 | ||
|
|
ddc0bfe786 | ||
|
|
09796b6e0b | ||
|
|
73c34c68b7 | ||
|
|
76ab96ef3e | ||
|
|
5a580dd16c | ||
|
|
918887525b | ||
|
|
500ebbecbf | ||
|
|
8062dface6 | ||
|
|
8d717b78ac | ||
|
|
4751ac6acc | ||
|
|
1d8ffb22ee | ||
|
|
a035d6542d | ||
|
|
d7230022f6 | ||
|
|
d25340bc31 | ||
|
|
c396024a26 | ||
|
|
65b94890ab | ||
|
|
682004d9a2 | ||
|
|
decf797180 | ||
|
|
552b6dd913 | ||
|
|
e6358eca16 | ||
|
|
f3bacad460 | ||
|
|
862f4363a9 | ||
|
|
9b3a3dc562 | ||
|
|
c2641a5fc2 | ||
|
|
1213ba5916 | ||
|
|
0521ff2c4d | ||
|
|
65c892787e | ||
|
|
7541c192bf | ||
|
|
c692d7e679 | ||
|
|
44a9a491af | ||
|
|
f8cae7c143 | ||
|
|
4f2b559126 | ||
|
|
46fd288a95 | ||
|
|
3a1cac6c99 | ||
|
|
225092a193 | ||
|
|
2fcb17621c | ||
|
|
69e3ee862f | ||
|
|
47c12cb582 | ||
|
|
94cf75af68 |
@@ -2,9 +2,6 @@
|
||||
; SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
[BlueprintSettings]
|
||||
kde/frameworks/extra-cmake-modules.version=master
|
||||
kde/unreleased/kirigami-addons.version=master
|
||||
kde/frameworks.version=master
|
||||
kde/libs.version=master
|
||||
kde/plasma.version=master
|
||||
kde/unreleased.version=master
|
||||
libs/qt.qtMajorVersion=6
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"id": "org.kde.neochat",
|
||||
"branch": "master",
|
||||
"runtime": "org.kde.Platform",
|
||||
"runtime-version": "5.15-22.08",
|
||||
"runtime-version": "6.6-kf6preview",
|
||||
"sdk": "org.kde.Sdk",
|
||||
"command": "neochat",
|
||||
"tags": [
|
||||
@@ -12,7 +12,7 @@
|
||||
"finish-args": [
|
||||
"--share=network",
|
||||
"--share=ipc",
|
||||
"--socket=x11",
|
||||
"--socket=fallback-x11",
|
||||
"--socket=wayland",
|
||||
"--device=dri",
|
||||
"--filesystem=xdg-download",
|
||||
@@ -31,6 +31,7 @@
|
||||
},
|
||||
{
|
||||
"name": "kquickimageeditor",
|
||||
"config-opts": [ "-DBUILD_WITH_QT6=ON" ],
|
||||
"buildsystem": "cmake-ninja",
|
||||
"sources": [
|
||||
{
|
||||
@@ -85,8 +86,8 @@
|
||||
"sources": [
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/frankosterfeld/qtkeychain/archive/v0.13.2.tar.gz",
|
||||
"sha256": "20beeb32de7c4eb0af9039b21e18370faf847ac8697ab3045906076afbc4caa5",
|
||||
"url": "https://github.com/frankosterfeld/qtkeychain/archive/0.14.2.tar.gz",
|
||||
"sha256": "cf2e972b783ba66334a79a30f6b3a1ea794a1dc574d6c3bebae5ffd2f0399571",
|
||||
"x-checker-data": {
|
||||
"type": "anitya",
|
||||
"project-id": 4138,
|
||||
@@ -96,6 +97,7 @@
|
||||
}
|
||||
],
|
||||
"config-opts": [
|
||||
"-DBUILD_WITH_QT6=ON",
|
||||
"-DCMAKE_INSTALL_LIBDIR=/app/lib",
|
||||
"-DLIB_INSTALL_DIR=/app/lib",
|
||||
"-DBUILD_TRANSLATIONS=NO"
|
||||
@@ -113,6 +115,7 @@
|
||||
}
|
||||
],
|
||||
"config-opts": [
|
||||
"-DBUILD_WITH_QT6=ON",
|
||||
"-DQuotient_ENABLE_E2EE=ON",
|
||||
"-DBUILD_TESTING=OFF"
|
||||
]
|
||||
|
||||
@@ -9,5 +9,7 @@ include:
|
||||
- /gitlab-templates/linux-qt6.yml
|
||||
- /gitlab-templates/windows-qt6.yml
|
||||
- /gitlab-templates/freebsd-qt6.yml
|
||||
# - /gitlab-templates/flatpak.yml
|
||||
- /gitlab-templates/flatpak.yml
|
||||
- /gitlab-templates/craft-android-qt6-apks.yml
|
||||
- /gitlab-templates/craft-appimage-qt6.yml
|
||||
- /gitlab-templates/craft-windows-x86-64-qt6.yml
|
||||
|
||||
@@ -8,8 +8,8 @@ cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
# KDE Applications version, managed by release script.
|
||||
set(RELEASE_SERVICE_VERSION_MAJOR "24")
|
||||
set(RELEASE_SERVICE_VERSION_MINOR "01")
|
||||
set(RELEASE_SERVICE_VERSION_MICRO "80")
|
||||
set(RELEASE_SERVICE_VERSION_MINOR "02")
|
||||
set(RELEASE_SERVICE_VERSION_MICRO "0")
|
||||
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
|
||||
|
||||
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
|
||||
|
||||
@@ -92,6 +92,7 @@ android {
|
||||
exclude 'lib/*/*_imageformats_qtga_*'
|
||||
exclude 'lib/*/*_imageformats_qtiff_*'
|
||||
exclude 'lib/*/*_qmltooling_*'
|
||||
exclude 'lib/*/*_multimedia_ffmpeg*' // temporary qt6 android fix
|
||||
}
|
||||
|
||||
aaptOptions {
|
||||
|
||||
@@ -46,3 +46,39 @@ ecm_add_test(
|
||||
LINK_LIBRARIES neochat Qt::Test
|
||||
TEST_NAME chatdocumenthandlertest
|
||||
)
|
||||
|
||||
ecm_add_test(
|
||||
messageeventmodeltest.cpp
|
||||
LINK_LIBRARIES neochat Qt::Test
|
||||
TEST_NAME messageeventmodeltest
|
||||
)
|
||||
|
||||
ecm_add_test(
|
||||
actionshandlertest.cpp
|
||||
LINK_LIBRARIES neochat Qt::Test
|
||||
TEST_NAME actionshandlertest
|
||||
)
|
||||
|
||||
ecm_add_test(
|
||||
windowcontrollertest.cpp
|
||||
LINK_LIBRARIES neochat Qt::Test
|
||||
TEST_NAME windowcontrollertest
|
||||
)
|
||||
|
||||
ecm_add_test(
|
||||
pollhandlertest.cpp
|
||||
LINK_LIBRARIES neochat Qt::Test
|
||||
TEST_NAME pollhandlertest
|
||||
)
|
||||
|
||||
ecm_add_test(
|
||||
reactionmodeltest.cpp
|
||||
LINK_LIBRARIES neochat Qt::Test
|
||||
TEST_NAME reactionmodeltest
|
||||
)
|
||||
|
||||
ecm_add_test(
|
||||
linkpreviewertest.cpp
|
||||
LINK_LIBRARIES neochat Qt::Test
|
||||
TEST_NAME linkpreviewertest
|
||||
)
|
||||
|
||||
43
autotests/actionshandlertest.cpp
Normal file
43
autotests/actionshandlertest.cpp
Normal file
@@ -0,0 +1,43 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include <QTest>
|
||||
|
||||
#include "actionshandler.h"
|
||||
#include "chatbarcache.h"
|
||||
|
||||
#include "testutils.h"
|
||||
|
||||
class ActionsHandlerTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
Quotient::Connection *connection = Quotient::Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
|
||||
ActionsHandler *actionsHandler = new ActionsHandler(this);
|
||||
|
||||
private Q_SLOTS:
|
||||
void nullObject();
|
||||
};
|
||||
|
||||
void ActionsHandlerTest::nullObject()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "ActionsHandler::handleMessageEvent - called with m_room and/or chatBarCache set to nullptr.");
|
||||
actionsHandler->handleMessageEvent(nullptr);
|
||||
|
||||
auto chatBarCache = new ChatBarCache(this);
|
||||
QTest::ignoreMessage(QtWarningMsg, "ActionsHandler::handleMessageEvent - called with m_room and/or chatBarCache set to nullptr.");
|
||||
actionsHandler->handleMessageEvent(chatBarCache);
|
||||
|
||||
auto room = new TestUtils::TestRoom(connection, QStringLiteral("#myroom:kde.org"));
|
||||
actionsHandler->setRoom(room);
|
||||
QTest::ignoreMessage(QtWarningMsg, "ActionsHandler::handleMessageEvent - called with m_room and/or chatBarCache set to nullptr.");
|
||||
actionsHandler->handleMessageEvent(nullptr);
|
||||
|
||||
// The final one should throw no warning so we make sure.
|
||||
QTest::failOnWarning("ActionsHandler::handleMessageEvent - called with m_room and/or chatBarCache set to nullptr.");
|
||||
actionsHandler->handleMessageEvent(chatBarCache);
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(ActionsHandlerTest)
|
||||
#include "actionshandlertest.moc"
|
||||
@@ -12,26 +12,17 @@
|
||||
#include "chatbarcache.h"
|
||||
#include "neochatroom.h"
|
||||
|
||||
#include "testutils.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
class TestRoom : public NeoChatRoom
|
||||
{
|
||||
public:
|
||||
using NeoChatRoom::NeoChatRoom;
|
||||
|
||||
void update(SyncRoomData &&data, bool fromCache = false)
|
||||
{
|
||||
Room::updateData(std::move(data), fromCache);
|
||||
}
|
||||
};
|
||||
|
||||
class ChatBarCacheTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
Connection *connection = nullptr;
|
||||
TestRoom *room = nullptr;
|
||||
TestUtils::TestRoom *room = nullptr;
|
||||
|
||||
private Q_SLOTS:
|
||||
void initTestCase();
|
||||
@@ -47,14 +38,7 @@ private Q_SLOTS:
|
||||
void ChatBarCacheTest::initTestCase()
|
||||
{
|
||||
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
|
||||
room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join);
|
||||
|
||||
QFile testMinSyncFile;
|
||||
testMinSyncFile.setFileName(QLatin1String(DATA_DIR) + u'/' + QLatin1String("test-min-sync.json"));
|
||||
testMinSyncFile.open(QIODevice::ReadOnly);
|
||||
const auto testMinSyncJson = QJsonDocument::fromJson(testMinSyncFile.readAll());
|
||||
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, testMinSyncJson.object());
|
||||
room->update(std::move(roomData));
|
||||
room = new TestUtils::TestRoom(connection, QStringLiteral("#myroom:kde.org"), QLatin1String("test-min-sync.json"));
|
||||
}
|
||||
|
||||
void ChatBarCacheTest::empty()
|
||||
|
||||
14
autotests/data/test-invalidmatrixtolink-event.json
Normal file
14
autotests/data/test-invalidmatrixtolink-event.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"content": {
|
||||
"body": "https://matrix.to/#/@alice:example.org",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$validlink1:example.org",
|
||||
"origin_server_ts": 1432735824654,
|
||||
"room_id": "!test:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
}
|
||||
14
autotests/data/test-invalidmxclink-event.json
Normal file
14
autotests/data/test-invalidmxclink-event.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"content": {
|
||||
"body": "mxc://example.org/SEsfnsuifSDFSSEF",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$validlink1:example.org",
|
||||
"origin_server_ts": 1432735824654,
|
||||
"room_id": "!test:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
}
|
||||
14
autotests/data/test-invalidnospacelink-event.json
Normal file
14
autotests/data/test-invalidnospacelink-event.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"content": {
|
||||
"body": "testhttps://kde.org",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$validlink1:example.org",
|
||||
"origin_server_ts": 1432735824654,
|
||||
"room_id": "!test:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
}
|
||||
24
autotests/data/test-linkpreviewerintial-sync.json
Normal file
24
autotests/data/test-linkpreviewerintial-sync.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"timeline": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"body": "https://kde.org",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "https://kde.org",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"origin_server_ts": 1704648567967,
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 112
|
||||
},
|
||||
"event_id": "$validlink:example.org",
|
||||
"room_id": "!test:example.org"
|
||||
}
|
||||
],
|
||||
"limited": true,
|
||||
"prev_batch": "t34-23535_0_0"
|
||||
}
|
||||
}
|
||||
35
autotests/data/test-linkpreviewerreplace-sync.json
Normal file
35
autotests/data/test-linkpreviewerreplace-sync.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"timeline": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"body": "* ",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "no link",
|
||||
"m.new_content": {
|
||||
"body": "",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "no link",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"m.relates_to": {
|
||||
"event_id": "$validlink:example.org",
|
||||
"rel_type": "m.replace"
|
||||
},
|
||||
"msgtype": "m.text",
|
||||
"type": "m.room.message"
|
||||
},
|
||||
"origin_server_ts": 1704648614969,
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 65
|
||||
},
|
||||
"event_id": "$nolink:example.org",
|
||||
"room_id": "!test:example.org"
|
||||
}
|
||||
],
|
||||
"limited": true,
|
||||
"prev_batch": "t34-23535_0_0"
|
||||
}
|
||||
}
|
||||
105
autotests/data/test-messageventmodel-sync.json
Normal file
105
autotests/data/test-messageventmodel-sync.json
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"account_data": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"tags": {
|
||||
"u.work": {
|
||||
"order": 0.9
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.tag"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"custom_config_key": "custom_config_value"
|
||||
},
|
||||
"type": "org.example.custom.room.config"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ephemeral": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"user_ids": [
|
||||
"@alice:matrix.org",
|
||||
"@bob:example.com"
|
||||
]
|
||||
},
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"type": "m.typing"
|
||||
}
|
||||
]
|
||||
},
|
||||
"state": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"displayname": "Example",
|
||||
"membership": "join"
|
||||
},
|
||||
"event_id": "$exampleMember:example.org",
|
||||
"origin_server_ts": 1432735824653,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"state_key": "@example:example.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"summary": {
|
||||
"m.heroes": [
|
||||
"@example:example.org"
|
||||
],
|
||||
"m.invited_member_count": 0,
|
||||
"m.joined_member_count": 2
|
||||
},
|
||||
"timeline": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"body": "This is an example\ntext message",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "<b>This is an example<br>text message</b>",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$153456789:example.org",
|
||||
"origin_server_ts": 1,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1232
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"displayname": "Example Changed",
|
||||
"membership": "join"
|
||||
},
|
||||
"event_id": "$exampleMemberChnage:example.org",
|
||||
"origin_server_ts": 2,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"state_key": "@example:example.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"replaces_state": "$exampleMember:example.org",
|
||||
"prev_content": {
|
||||
"displayname": "Example",
|
||||
"membership": "join"
|
||||
},
|
||||
"prev_sender": "@example:example.org",
|
||||
"age": 1234
|
||||
}
|
||||
}
|
||||
],
|
||||
"limited": true,
|
||||
"prev_batch": "t34-23535_0_0"
|
||||
}
|
||||
}
|
||||
14
autotests/data/test-multiplelink-event.json
Normal file
14
autotests/data/test-multiplelink-event.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"content": {
|
||||
"body": "www.example.org https://kde.org",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$validlink1:example.org",
|
||||
"origin_server_ts": 1432735824654,
|
||||
"room_id": "!test:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
}
|
||||
22
autotests/data/test-pending-sync.json
Normal file
22
autotests/data/test-pending-sync.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"timeline": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"body": "New plain message",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$pendingmerge:example.org",
|
||||
"origin_server_ts": 123456,
|
||||
"room_id":"#myroom:kde.org",
|
||||
"sender":"@bob:kde.org",
|
||||
"type":"m.room.message",
|
||||
"unsigned": {
|
||||
"transaction_id": "17017181543521"
|
||||
}
|
||||
}
|
||||
],
|
||||
"limited": true,
|
||||
"prev_batch": "t34-23535_0_0"
|
||||
}
|
||||
}
|
||||
38
autotests/data/test-pollhandlerstart-sync.json
Normal file
38
autotests/data/test-pollhandlerstart-sync.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"timeline": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"org.matrix.msc1767.text": "test\n1. option1\n2. option 2",
|
||||
"org.matrix.msc3381.poll.start": {
|
||||
"answers": [
|
||||
{
|
||||
"id": "option1",
|
||||
"org.matrix.msc1767.text": "option1"
|
||||
},
|
||||
{
|
||||
"id": "option2",
|
||||
"org.matrix.msc1767.text": "option2"
|
||||
}
|
||||
],
|
||||
"kind": "org.matrix.msc3381.poll.disclosed",
|
||||
"max_selections": 1,
|
||||
"question": {
|
||||
"body": "test",
|
||||
"msgtype": "m.text",
|
||||
"org.matrix.msc1767.text": "test"
|
||||
}
|
||||
}
|
||||
},
|
||||
"event_id": "$153456789:example.org",
|
||||
"origin_server_ts": 1432735824654,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "org.matrix.msc3381.poll.start",
|
||||
"unsigned": {
|
||||
"age": 1232
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
44
autotests/data/test-reactionmodel-extra-sync.json
Normal file
44
autotests/data/test-reactionmodel-extra-sync.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"timeline": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"m.relates_to": {
|
||||
"event_id": "$153456789:example.org",
|
||||
"key": "👍",
|
||||
"rel_type": "m.annotation"
|
||||
}
|
||||
},
|
||||
"origin_server_ts": 1690322545183,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@bob:example.org",
|
||||
"type": "m.reaction",
|
||||
"unsigned": {
|
||||
"age": 390159121
|
||||
},
|
||||
"event_id": "$163456790:example.org",
|
||||
"age": 390159121
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"m.relates_to": {
|
||||
"event_id": "$153456789:example.org",
|
||||
"key": "😆",
|
||||
"rel_type": "m.annotation"
|
||||
}
|
||||
},
|
||||
"origin_server_ts": 1690322545184,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@bob:example.org",
|
||||
"type": "m.reaction",
|
||||
"unsigned": {
|
||||
"age": 390159122
|
||||
},
|
||||
"event_id": "$163456791:example.org",
|
||||
"age": 390159122
|
||||
}
|
||||
],
|
||||
"limited": true,
|
||||
"prev_batch": "t34-23535_0_0"
|
||||
}
|
||||
}
|
||||
204
autotests/data/test-reactionmodel-sync.json
Normal file
204
autotests/data/test-reactionmodel-sync.json
Normal file
@@ -0,0 +1,204 @@
|
||||
{
|
||||
"account_data": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"tags": {
|
||||
"u.work": {
|
||||
"order": 0.9
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.tag"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"custom_config_key": "custom_config_value"
|
||||
},
|
||||
"type": "org.example.custom.room.config"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ephemeral": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"user_ids": [
|
||||
"@alice:matrix.org",
|
||||
"@bob:example.com"
|
||||
]
|
||||
},
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"type": "m.typing"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"$153456789:example.org": {
|
||||
"m.read": {
|
||||
"@alice:matrix.org": {
|
||||
"ts": 1436451550453
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.receipt"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"$1532735824654:example.org": {
|
||||
"m.read": {
|
||||
"@bob:example.com": {
|
||||
"ts": 1436451550453
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.receipt"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"$1532735824654:example.org": {
|
||||
"m.read": {
|
||||
"@tim:example.com": {
|
||||
"ts": 1436451550454
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.receipt"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"$1532735824654:example.org": {
|
||||
"m.read": {
|
||||
"@jeff:example.com": {
|
||||
"ts": 1436451550455
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.receipt"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"$1532735824654:example.org": {
|
||||
"m.read": {
|
||||
"@tina:example.com": {
|
||||
"ts": 1436451550456
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.receipt"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"$1532735824654:example.org": {
|
||||
"m.read": {
|
||||
"@sally:example.com": {
|
||||
"ts": 1436451550457
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.receipt"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"$1532735824654:example.org": {
|
||||
"m.read": {
|
||||
"@fred:example.com": {
|
||||
"ts": 1436451550458
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.receipt"
|
||||
}
|
||||
]
|
||||
},
|
||||
"state": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
|
||||
"displayname": "Alice Margatroid",
|
||||
"membership": "join",
|
||||
"reason": "Looking for support"
|
||||
},
|
||||
"event_id": "$143273582443PhrSn:example.org",
|
||||
"origin_server_ts": 1432735824653,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"state_key": "@alice:example.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"displayname": "Look\nat\nme\nI\nput\nnewlines\nin\nmy\ndisplay name",
|
||||
"membership": "join"
|
||||
},
|
||||
"event_id": "$143273582443PhrSh:example.org",
|
||||
"origin_server_ts": 1432735824659,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@newline:example.org",
|
||||
"state_key": "@newline:example.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"age": 12345
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"summary": {
|
||||
"m.heroes": [
|
||||
"@alice:example.com",
|
||||
"@bob:example.com"
|
||||
],
|
||||
"m.invited_member_count": 0,
|
||||
"m.joined_member_count": 2
|
||||
},
|
||||
"timeline": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"body": "This is an example\ntext message",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "<b>This is an example<br>text message</b>",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$153456789:example.org",
|
||||
"origin_server_ts": 1432735824654,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1232
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"m.relates_to": {
|
||||
"event_id": "$153456789:example.org",
|
||||
"key": "👍",
|
||||
"rel_type": "m.annotation"
|
||||
}
|
||||
},
|
||||
"origin_server_ts": 1690322545182,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@alice:matrix.org",
|
||||
"type": "m.reaction",
|
||||
"unsigned": {
|
||||
"age": 390159120
|
||||
},
|
||||
"event_id": "$163456789:example.org",
|
||||
"age": 390159120
|
||||
}
|
||||
],
|
||||
"limited": true,
|
||||
"prev_batch": "t34-23535_0_0"
|
||||
}
|
||||
}
|
||||
14
autotests/data/test-validplainlink-event.json
Normal file
14
autotests/data/test-validplainlink-event.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"content": {
|
||||
"body": "https://kde.org",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$validlink1:example.org",
|
||||
"origin_server_ts": 1432735824654,
|
||||
"room_id": "!test:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
}
|
||||
14
autotests/data/test-validplainwwwlink-event.json
Normal file
14
autotests/data/test-validplainwwwlink-event.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"content": {
|
||||
"body": "www.example.org",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$validlink1:example.org",
|
||||
"origin_server_ts": 1432735824654,
|
||||
"room_id": "!test:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
}
|
||||
16
autotests/data/test-validrichlink-event.json
Normal file
16
autotests/data/test-validrichlink-event.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"content": {
|
||||
"body": "[Rich Link](https://kde.org)",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "<a href=\"https://kde.org\">Rich Link</a>",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$validlink1:example.org",
|
||||
"origin_server_ts": 1432735824654,
|
||||
"room_id": "!test:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
}
|
||||
@@ -18,26 +18,17 @@
|
||||
#include "neochatroom.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include "testutils.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
class TestRoom : public NeoChatRoom
|
||||
{
|
||||
public:
|
||||
using NeoChatRoom::NeoChatRoom;
|
||||
|
||||
void update(SyncRoomData &&data, bool fromCache = false)
|
||||
{
|
||||
Room::updateData(std::move(data), fromCache);
|
||||
}
|
||||
};
|
||||
|
||||
class EventHandlerTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
Connection *connection = nullptr;
|
||||
TestRoom *room = nullptr;
|
||||
TestUtils::TestRoom *room = nullptr;
|
||||
EventHandler eventHandler;
|
||||
EventHandler emptyHandler;
|
||||
EventHandler noEventHandler;
|
||||
@@ -70,12 +61,10 @@ private Q_SLOTS:
|
||||
void genericBody_data();
|
||||
void genericBody();
|
||||
void nullGenericBody();
|
||||
void subtitle();
|
||||
void nullSubtitle();
|
||||
void mediaInfo();
|
||||
void nullMediaInfo();
|
||||
void linkPreviewer();
|
||||
void nullLinkPreviewer();
|
||||
void reactions();
|
||||
void nullReactions();
|
||||
void hasReply();
|
||||
void nullHasReply();
|
||||
void replyId();
|
||||
@@ -101,14 +90,7 @@ private Q_SLOTS:
|
||||
void EventHandlerTest::initTestCase()
|
||||
{
|
||||
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
|
||||
room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join);
|
||||
|
||||
QFile testEventHandlerSyncFile;
|
||||
testEventHandlerSyncFile.setFileName(QLatin1String(DATA_DIR) + u'/' + QLatin1String("test-eventhandler-sync.json"));
|
||||
testEventHandlerSyncFile.open(QIODevice::ReadOnly);
|
||||
const auto testEventHandlerSyncJson = QJsonDocument::fromJson(testEventHandlerSyncFile.readAll());
|
||||
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, testEventHandlerSyncJson.object());
|
||||
room->update(std::move(roomData));
|
||||
room = new TestUtils::TestRoom(connection, QStringLiteral("#myroom:kde.org"), QLatin1String("test-eventhandler-sync.json"));
|
||||
|
||||
eventHandler.setRoom(room);
|
||||
noEventHandler.setRoom(room);
|
||||
@@ -364,6 +346,25 @@ void EventHandlerTest::nullGenericBody()
|
||||
QCOMPARE(noEventHandler.getGenericBody(), QString());
|
||||
}
|
||||
|
||||
void EventHandlerTest::subtitle()
|
||||
{
|
||||
auto event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.subtitleText(), QStringLiteral("after: This is an example text message"));
|
||||
|
||||
event = room->messageEvents().at(2).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.subtitleText(), QStringLiteral("after: This is a highlight @bob:kde.org and this is a link https://kde.org"));
|
||||
}
|
||||
|
||||
void EventHandlerTest::nullSubtitle()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "subtitleText called with m_event set to nullptr.");
|
||||
QCOMPARE(noEventHandler.subtitleText(), QString());
|
||||
}
|
||||
|
||||
void EventHandlerTest::mediaInfo()
|
||||
{
|
||||
auto event = room->messageEvents().at(4).get();
|
||||
@@ -396,45 +397,6 @@ void EventHandlerTest::nullMediaInfo()
|
||||
QCOMPARE(noEventHandler.getMediaInfo(), QVariantMap());
|
||||
}
|
||||
|
||||
void EventHandlerTest::linkPreviewer()
|
||||
{
|
||||
auto event = room->messageEvents().at(2).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getLinkPreviewer()->url(), QUrl("https://kde.org"_ls));
|
||||
|
||||
event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getLinkPreviewer(), nullptr);
|
||||
}
|
||||
|
||||
void EventHandlerTest::nullLinkPreviewer()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "getLinkPreviewer called with m_room set to nullptr.");
|
||||
QCOMPARE(emptyHandler.getLinkPreviewer(), nullptr);
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "getLinkPreviewer called with m_event set to nullptr.");
|
||||
QCOMPARE(noEventHandler.getLinkPreviewer(), nullptr);
|
||||
}
|
||||
|
||||
void EventHandlerTest::reactions()
|
||||
{
|
||||
auto event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getReactions()->rowCount(), 1);
|
||||
}
|
||||
|
||||
void EventHandlerTest::nullReactions()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "getReactions called with m_room set to nullptr.");
|
||||
QCOMPARE(emptyHandler.getReactions(), nullptr);
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "getReactions called with m_event set to nullptr.");
|
||||
QCOMPARE(noEventHandler.getReactions(), nullptr);
|
||||
}
|
||||
|
||||
void EventHandlerTest::hasReply()
|
||||
{
|
||||
auto event = room->messageEvents().at(5).get();
|
||||
|
||||
104
autotests/linkpreviewertest.cpp
Normal file
104
autotests/linkpreviewertest.cpp
Normal file
@@ -0,0 +1,104 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include <QObject>
|
||||
#include <QTest>
|
||||
|
||||
#include "linkpreviewer.h"
|
||||
|
||||
#include <Quotient/events/roommessageevent.h>
|
||||
#include <Quotient/quotient_common.h>
|
||||
#include <Quotient/syncdata.h>
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
#include "testutils.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
class LinkPreviewerTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
Connection *connection = nullptr;
|
||||
TestUtils::TestRoom *room = nullptr;
|
||||
|
||||
private Q_SLOTS:
|
||||
void initTestCase();
|
||||
|
||||
void linkPreviewsMatch_data();
|
||||
void linkPreviewsMatch();
|
||||
|
||||
void linkPreviewsReject_data();
|
||||
void linkPreviewsReject();
|
||||
|
||||
void editedLink();
|
||||
};
|
||||
|
||||
void LinkPreviewerTest::initTestCase()
|
||||
{
|
||||
connection = Connection::makeMockConnection(QStringLiteral("@bob:example.org"));
|
||||
room = new TestUtils::TestRoom(connection, QStringLiteral("!test:example.org"));
|
||||
}
|
||||
|
||||
void LinkPreviewerTest::linkPreviewsMatch_data()
|
||||
{
|
||||
QTest::addColumn<QString>("eventSource");
|
||||
QTest::addColumn<QUrl>("testOutputLink");
|
||||
|
||||
QTest::newRow("plainHttps") << QStringLiteral("test-validplainlink-event.json") << QUrl("https://kde.org"_ls);
|
||||
QTest::newRow("richHttps") << QStringLiteral("test-validrichlink-event.json") << QUrl("https://kde.org"_ls);
|
||||
QTest::newRow("plainWww") << QStringLiteral("test-validplainwwwlink-event.json") << QUrl("www.example.org"_ls);
|
||||
QTest::newRow("multipleHttps") << QStringLiteral("test-multiplelink-event.json") << QUrl("www.example.org"_ls);
|
||||
}
|
||||
|
||||
void LinkPreviewerTest::linkPreviewsMatch()
|
||||
{
|
||||
QFETCH(QString, eventSource);
|
||||
QFETCH(QUrl, testOutputLink);
|
||||
|
||||
auto event = TestUtils::loadEventFromFile<RoomMessageEvent>(eventSource);
|
||||
auto linkPreviewer = LinkPreviewer(room, event.get());
|
||||
|
||||
QCOMPARE(linkPreviewer.empty(), false);
|
||||
QCOMPARE(linkPreviewer.url(), testOutputLink);
|
||||
}
|
||||
|
||||
void LinkPreviewerTest::linkPreviewsReject_data()
|
||||
{
|
||||
QTest::addColumn<QString>("eventSource");
|
||||
|
||||
QTest::newRow("mxc") << QStringLiteral("test-invalidmxclink-event.json");
|
||||
QTest::newRow("matrixTo") << QStringLiteral("test-invalidmatrixtolink-event.json");
|
||||
QTest::newRow("noSpace") << QStringLiteral("test-invalidnospacelink-event.json");
|
||||
}
|
||||
|
||||
void LinkPreviewerTest::linkPreviewsReject()
|
||||
{
|
||||
QFETCH(QString, eventSource);
|
||||
|
||||
auto event = TestUtils::loadEventFromFile<RoomMessageEvent>(eventSource);
|
||||
auto linkPreviewer = LinkPreviewer(room, event.get());
|
||||
|
||||
QCOMPARE(linkPreviewer.empty(), true);
|
||||
QCOMPARE(linkPreviewer.url(), QUrl());
|
||||
}
|
||||
|
||||
void LinkPreviewerTest::editedLink()
|
||||
{
|
||||
room->syncNewEvents(QStringLiteral("test-linkpreviewerintial-sync.json"));
|
||||
auto event = eventCast<const RoomMessageEvent>(room->messageEvents().at(0).get());
|
||||
auto linkPreviewer = LinkPreviewer(room, event);
|
||||
|
||||
QCOMPARE(linkPreviewer.empty(), false);
|
||||
QCOMPARE(linkPreviewer.url(), QUrl("https://kde.org"_ls));
|
||||
|
||||
room->syncNewEvents(QStringLiteral("test-linkpreviewerreplace-sync.json"));
|
||||
|
||||
QCOMPARE(linkPreviewer.empty(), true);
|
||||
QCOMPARE(linkPreviewer.url(), QUrl());
|
||||
}
|
||||
|
||||
QTEST_MAIN(LinkPreviewerTest)
|
||||
#include "linkpreviewertest.moc"
|
||||
215
autotests/messageeventmodeltest.cpp
Normal file
215
autotests/messageeventmodeltest.cpp
Normal file
@@ -0,0 +1,215 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include <QObject>
|
||||
#include <QSignalSpy>
|
||||
#include <QTest>
|
||||
|
||||
#include <Quotient/connection.h>
|
||||
#include <Quotient/quotient_common.h>
|
||||
#include <Quotient/syncdata.h>
|
||||
|
||||
#include "enums/delegatetype.h"
|
||||
#include "models/messageeventmodel.h"
|
||||
#include "neochatroom.h"
|
||||
|
||||
#include "testutils.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
class MessageEventModelTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
Connection *connection = nullptr;
|
||||
MessageEventModel *model = nullptr;
|
||||
|
||||
private Q_SLOTS:
|
||||
void initTestCase();
|
||||
void init();
|
||||
|
||||
void switchEmptyRoom();
|
||||
void switchSyncedRoom();
|
||||
void simpleTimeline();
|
||||
void syncNewEvents();
|
||||
void pendingEvent();
|
||||
void disconnect();
|
||||
void idToRow();
|
||||
|
||||
void cleanup();
|
||||
};
|
||||
|
||||
void MessageEventModelTest::initTestCase()
|
||||
{
|
||||
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
|
||||
}
|
||||
|
||||
void MessageEventModelTest::init()
|
||||
{
|
||||
QCOMPARE(model, nullptr);
|
||||
model = new MessageEventModel;
|
||||
}
|
||||
|
||||
// Make sure that basic empty rooms can be switched without crashing.
|
||||
void MessageEventModelTest::switchEmptyRoom()
|
||||
{
|
||||
auto firstRoom = new TestUtils::TestRoom(connection, QStringLiteral("#firstRoom:kde.org"));
|
||||
auto secondRoom = new TestUtils::TestRoom(connection, QStringLiteral("#secondRoom:kde.org"));
|
||||
|
||||
QSignalSpy spy(model, SIGNAL(roomChanged()));
|
||||
|
||||
QCOMPARE(model->room(), nullptr);
|
||||
model->setRoom(firstRoom);
|
||||
QCOMPARE(spy.count(), 1);
|
||||
QCOMPARE(model->room()->id(), QStringLiteral("#firstRoom:kde.org"));
|
||||
model->setRoom(secondRoom);
|
||||
QCOMPARE(spy.count(), 2);
|
||||
QCOMPARE(model->room()->id(), QStringLiteral("#secondRoom:kde.org"));
|
||||
model->setRoom(nullptr);
|
||||
QCOMPARE(spy.count(), 3);
|
||||
QCOMPARE(model->room(), nullptr);
|
||||
}
|
||||
|
||||
// Make sure that rooms with some events can be switched without crashing
|
||||
void MessageEventModelTest::switchSyncedRoom()
|
||||
{
|
||||
auto firstRoom = new TestUtils::TestRoom(connection, QStringLiteral("#firstRoom:kde.org"), QLatin1String("test-messageventmodel-sync.json"));
|
||||
auto secondRoom = new TestUtils::TestRoom(connection, QStringLiteral("#secondRoom:kde.org"), QLatin1String("test-messageventmodel-sync.json"));
|
||||
|
||||
QSignalSpy spy(model, SIGNAL(roomChanged()));
|
||||
|
||||
QCOMPARE(model->room(), nullptr);
|
||||
model->setRoom(firstRoom);
|
||||
QCOMPARE(spy.count(), 1);
|
||||
QCOMPARE(model->room()->id(), QStringLiteral("#firstRoom:kde.org"));
|
||||
model->setRoom(secondRoom);
|
||||
QCOMPARE(spy.count(), 2);
|
||||
QCOMPARE(model->room()->id(), QStringLiteral("#secondRoom:kde.org"));
|
||||
model->setRoom(nullptr);
|
||||
QCOMPARE(spy.count(), 3);
|
||||
QCOMPARE(model->room(), nullptr);
|
||||
}
|
||||
|
||||
void MessageEventModelTest::simpleTimeline()
|
||||
{
|
||||
auto room = new TestUtils::TestRoom(connection, QStringLiteral("#myroom:kde.org"), QLatin1String("test-messageventmodel-sync.json"));
|
||||
|
||||
model->setRoom(room);
|
||||
QCOMPARE(model->rowCount(), 2);
|
||||
|
||||
QCOMPARE(model->data(model->index(0), MessageEventModel::DelegateTypeRole), DelegateType::State);
|
||||
QCOMPARE(model->data(model->index(0)), QStringLiteral("changed their display name to Example Changed"));
|
||||
|
||||
QCOMPARE(model->data(model->index(1)), QStringLiteral("<b>This is an example<br>text message</b>"));
|
||||
QCOMPARE(model->data(model->index(1), MessageEventModel::DelegateTypeRole), DelegateType::Message);
|
||||
QCOMPARE(model->data(model->index(1), MessageEventModel::PlainText), QStringLiteral("This is an example\ntext message"));
|
||||
QCOMPARE(model->data(model->index(1), MessageEventModel::EventIdRole), QStringLiteral("$153456789:example.org"));
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "Index QModelIndex(-1,-1,0x0,QObject(0x0)) is not valid (expected valid)");
|
||||
QCOMPARE(model->data(model->index(-1)), QVariant());
|
||||
QTest::ignoreMessage(QtWarningMsg, "Index QModelIndex(-1,-1,0x0,QObject(0x0)) is not valid (expected valid)");
|
||||
QCOMPARE(model->data(model->index(model->rowCount())), QVariant());
|
||||
}
|
||||
|
||||
// Sync some events into the MessageEventModel's current room and don't crash.
|
||||
void MessageEventModelTest::syncNewEvents()
|
||||
{
|
||||
auto room = new TestUtils::TestRoom(connection, QStringLiteral("#myroom:kde.org"));
|
||||
QSignalSpy spy(room, SIGNAL(aboutToAddNewMessages(Quotient::RoomEventsRange)));
|
||||
|
||||
model->setRoom(room);
|
||||
QCOMPARE(model->rowCount(), 0);
|
||||
|
||||
room->syncNewEvents(QLatin1String("test-messageventmodel-sync.json"));
|
||||
|
||||
QCOMPARE(model->rowCount(), 2);
|
||||
QCOMPARE(spy.count(), 1);
|
||||
}
|
||||
|
||||
// Check the adding of pending events to the room doesn't cause any issues in the model.
|
||||
void MessageEventModelTest::pendingEvent()
|
||||
{
|
||||
QSignalSpy spyInsert(model, SIGNAL(rowsInserted(const QModelIndex &, int, int)));
|
||||
QSignalSpy spyRemove(model, SIGNAL(rowsRemoved(const QModelIndex &, int, int)));
|
||||
QSignalSpy spyChanged(model, SIGNAL(dataChanged(const QModelIndex, const QModelIndex, const QList<int> &)));
|
||||
|
||||
auto room = new TestUtils::TestRoom(connection, QStringLiteral("#myroom:kde.org"));
|
||||
model->setRoom(room);
|
||||
QCOMPARE(model->rowCount(), 0);
|
||||
|
||||
auto txnId = room->postPlainText("New plain message"_ls);
|
||||
QCOMPARE(model->rowCount(), 1);
|
||||
QCOMPARE(spyInsert.count(), 1);
|
||||
|
||||
room->discardMessage(txnId);
|
||||
QCOMPARE(model->rowCount(), 0);
|
||||
QCOMPARE(spyRemove.count(), 1);
|
||||
|
||||
txnId = room->postPlainText("New plain message"_ls);
|
||||
QCOMPARE(model->rowCount(), 1);
|
||||
QCOMPARE(spyInsert.count(), 2);
|
||||
|
||||
// We need to manually set the transaction ID of the new message as it will be
|
||||
// different every time.
|
||||
QFile testSyncFile;
|
||||
testSyncFile.setFileName(QLatin1String(DATA_DIR) + u'/' + QLatin1String("test-pending-sync.json"));
|
||||
testSyncFile.open(QIODevice::ReadOnly);
|
||||
auto testSyncJson = QJsonDocument::fromJson(testSyncFile.readAll());
|
||||
auto root = testSyncJson.object();
|
||||
auto timeline = root["timeline"_ls].toObject();
|
||||
auto events = timeline["events"_ls].toArray();
|
||||
auto firstEvent = events[0].toObject();
|
||||
firstEvent.insert(QLatin1String("unsigned"), QJsonObject{{QLatin1String("transaction_id"), txnId}});
|
||||
events[0] = firstEvent;
|
||||
timeline.insert("events"_ls, events);
|
||||
root.insert("timeline"_ls, timeline);
|
||||
testSyncJson.setObject(root);
|
||||
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, testSyncJson.object());
|
||||
room->update(std::move(roomData));
|
||||
|
||||
QCOMPARE(model->rowCount(), 1);
|
||||
// The model will throw multiple data changed signals we need the one that refreshes
|
||||
// the IsPendingRole.
|
||||
QCOMPARE(spyChanged.count() > 0, true);
|
||||
auto isPendingChanged = false;
|
||||
for (auto signal : spyChanged) {
|
||||
auto roles = signal.at(2).toList();
|
||||
if (roles.contains(MessageEventModel::IsPendingRole)) {
|
||||
isPendingChanged = true;
|
||||
}
|
||||
}
|
||||
QCOMPARE(isPendingChanged, true);
|
||||
}
|
||||
|
||||
// Make sure that the signals are disconnecting correctly when a room is switched.
|
||||
void MessageEventModelTest::disconnect()
|
||||
{
|
||||
auto room = new TestUtils::TestRoom(connection, QStringLiteral("#myroom:kde.org"));
|
||||
model->setRoom(room);
|
||||
|
||||
QSignalSpy spy(model, SIGNAL(rowsInserted(const QModelIndex &, int, int)));
|
||||
|
||||
model->setRoom(nullptr);
|
||||
room->syncNewEvents(QLatin1String("test-messageventmodel-sync.json"));
|
||||
|
||||
QCOMPARE(spy.count(), 0);
|
||||
}
|
||||
|
||||
void MessageEventModelTest::idToRow()
|
||||
{
|
||||
auto room = new TestUtils::TestRoom(connection, QStringLiteral("#myroom:kde.org"), QLatin1String("test-min-sync.json"));
|
||||
model->setRoom(room);
|
||||
|
||||
QCOMPARE(model->eventIdToRow(QStringLiteral("$153456789:example.org")), 0);
|
||||
}
|
||||
|
||||
void MessageEventModelTest::cleanup()
|
||||
{
|
||||
delete model;
|
||||
model = nullptr;
|
||||
QCOMPARE(model, nullptr);
|
||||
}
|
||||
|
||||
QTEST_MAIN(MessageEventModelTest)
|
||||
#include "messageeventmodeltest.moc"
|
||||
@@ -5,55 +5,30 @@
|
||||
#include <QSignalSpy>
|
||||
#include <QTest>
|
||||
|
||||
#include "neochatroom.h"
|
||||
|
||||
#include <Quotient/connection.h>
|
||||
#include <Quotient/quotient_common.h>
|
||||
#include <Quotient/syncdata.h>
|
||||
|
||||
#include "testutils.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
class TestRoom : public NeoChatRoom
|
||||
{
|
||||
public:
|
||||
using NeoChatRoom::NeoChatRoom;
|
||||
|
||||
void update(SyncRoomData &&data, bool fromCache = false)
|
||||
{
|
||||
Room::updateData(std::move(data), fromCache);
|
||||
}
|
||||
};
|
||||
|
||||
class NeoChatRoomTest : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
Connection *connection = nullptr;
|
||||
TestRoom *room = nullptr;
|
||||
TestUtils::TestRoom *room = nullptr;
|
||||
|
||||
private Q_SLOTS:
|
||||
void initTestCase();
|
||||
void subtitleTextTest();
|
||||
void eventTest();
|
||||
};
|
||||
|
||||
void NeoChatRoomTest::initTestCase()
|
||||
{
|
||||
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
|
||||
room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join);
|
||||
|
||||
QFile testMinSyncFile;
|
||||
testMinSyncFile.setFileName(QLatin1String(DATA_DIR) + u'/' + QLatin1String("test-min-sync.json"));
|
||||
testMinSyncFile.open(QIODevice::ReadOnly);
|
||||
const auto testMinSyncJson = QJsonDocument::fromJson(testMinSyncFile.readAll());
|
||||
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, testMinSyncJson.object());
|
||||
room->update(std::move(roomData));
|
||||
}
|
||||
|
||||
void NeoChatRoomTest::subtitleTextTest()
|
||||
{
|
||||
QCOMPARE(room->timelineSize(), 1);
|
||||
QCOMPARE(room->lastEventToString(), QStringLiteral("@example:example.org: This is an example\ntext message"));
|
||||
room = new TestUtils::TestRoom(connection, QStringLiteral("#myroom:kde.org"), "test-min-sync.json"_ls);
|
||||
}
|
||||
|
||||
void NeoChatRoomTest::eventTest()
|
||||
|
||||
70
autotests/pollhandlertest.cpp
Normal file
70
autotests/pollhandlertest.cpp
Normal file
@@ -0,0 +1,70 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include <QObject>
|
||||
#include <QSignalSpy>
|
||||
#include <QTest>
|
||||
|
||||
#include <Quotient/connection.h>
|
||||
#include <Quotient/quotient_common.h>
|
||||
#include <Quotient/syncdata.h>
|
||||
|
||||
#include "events/pollevent.h"
|
||||
#include "pollhandler.h"
|
||||
#include "testutils.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
class PollHandlerTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
Connection *connection = nullptr;
|
||||
TestUtils::TestRoom *room = nullptr;
|
||||
|
||||
private Q_SLOTS:
|
||||
void initTestCase();
|
||||
void nullObject();
|
||||
void poll();
|
||||
};
|
||||
|
||||
void PollHandlerTest::initTestCase()
|
||||
{
|
||||
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
|
||||
room = new TestUtils::TestRoom(connection, QStringLiteral("#myroom:kde.org"), "test-pollhandlerstart-sync.json"_ls);
|
||||
}
|
||||
|
||||
// Basically don't crash.
|
||||
void PollHandlerTest::nullObject()
|
||||
{
|
||||
auto pollHandler = PollHandler();
|
||||
|
||||
QCOMPARE(pollHandler.hasEnded(), false);
|
||||
QCOMPARE(pollHandler.answerCount(), 0);
|
||||
QCOMPARE(pollHandler.question(), QString());
|
||||
QCOMPARE(pollHandler.options(), QJsonArray());
|
||||
QCOMPARE(pollHandler.answers(), QJsonObject());
|
||||
QCOMPARE(pollHandler.counts(), QJsonObject());
|
||||
QCOMPARE(pollHandler.kind(), QString());
|
||||
}
|
||||
|
||||
void PollHandlerTest::poll()
|
||||
{
|
||||
auto startEvent = eventCast<const PollStartEvent>(room->messageEvents().at(0).get());
|
||||
auto pollHandler = PollHandler(room, startEvent);
|
||||
|
||||
auto options = QJsonArray{QJsonObject{{"id"_ls, "option1"_ls}, {"org.matrix.msc1767.text"_ls, "option1"_ls}},
|
||||
QJsonObject{{"id"_ls, "option2"_ls}, {"org.matrix.msc1767.text"_ls, "option2"_ls}}};
|
||||
|
||||
QCOMPARE(pollHandler.hasEnded(), false);
|
||||
QCOMPARE(pollHandler.answerCount(), 0);
|
||||
QCOMPARE(pollHandler.question(), QStringLiteral("test"));
|
||||
QCOMPARE(pollHandler.options(), options);
|
||||
QCOMPARE(pollHandler.answers(), QJsonObject());
|
||||
QCOMPARE(pollHandler.counts(), QJsonObject());
|
||||
QCOMPARE(pollHandler.kind(), QStringLiteral("org.matrix.msc3381.poll.disclosed"));
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(PollHandlerTest)
|
||||
#include "pollhandlertest.moc"
|
||||
83
autotests/reactionmodeltest.cpp
Normal file
83
autotests/reactionmodeltest.cpp
Normal file
@@ -0,0 +1,83 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include <QObject>
|
||||
#include <QSignalSpy>
|
||||
#include <QTest>
|
||||
|
||||
#include "models/reactionmodel.h"
|
||||
|
||||
#include <Quotient/events/roommessageevent.h>
|
||||
|
||||
#include "testutils.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
class ReactionModelTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
Connection *connection = nullptr;
|
||||
TestUtils::TestRoom *room = nullptr;
|
||||
|
||||
private Q_SLOTS:
|
||||
void initTestCase();
|
||||
|
||||
void nullModel();
|
||||
void basicReaction();
|
||||
void newReaction();
|
||||
};
|
||||
|
||||
void ReactionModelTest::initTestCase()
|
||||
{
|
||||
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
|
||||
room = new TestUtils::TestRoom(connection, QStringLiteral("#myroom:kde.org"), QLatin1String("test-reactionmodel-sync.json"));
|
||||
}
|
||||
|
||||
void ReactionModelTest::nullModel()
|
||||
{
|
||||
auto model = ReactionModel(nullptr, nullptr);
|
||||
|
||||
QCOMPARE(model.rowCount(), 0);
|
||||
QCOMPARE(model.data(model.index(0), ReactionModel::TextContentRole), QVariant());
|
||||
}
|
||||
|
||||
void ReactionModelTest::basicReaction()
|
||||
{
|
||||
auto event = eventCast<const RoomMessageEvent>(room->messageEvents().at(0).get());
|
||||
auto model = ReactionModel(event, room);
|
||||
|
||||
QCOMPARE(model.rowCount(), 1);
|
||||
QCOMPARE(model.data(model.index(0), ReactionModel::TextContentRole), QStringLiteral("<span style=\"font-family: 'emoji';\">👍</span>"));
|
||||
QCOMPARE(model.data(model.index(0), ReactionModel::ReactionRole), QStringLiteral("👍"));
|
||||
QCOMPARE(model.data(model.index(0), ReactionModel::ToolTipRole),
|
||||
QStringLiteral("@alice:matrix.org reacted with <span style=\"font-family: 'emoji';\">👍</span>"));
|
||||
auto authorList = QVariantList{room->getUser(room->user(QStringLiteral("@alice:matrix.org")))};
|
||||
QCOMPARE(model.data(model.index(0), ReactionModel::AuthorsRole), authorList);
|
||||
QCOMPARE(model.data(model.index(0), ReactionModel::HasLocalUser), false);
|
||||
}
|
||||
|
||||
void ReactionModelTest::newReaction()
|
||||
{
|
||||
auto event = eventCast<const RoomMessageEvent>(room->messageEvents().at(0).get());
|
||||
auto model = new ReactionModel(event, room);
|
||||
|
||||
QCOMPARE(model->rowCount(), 1);
|
||||
QCOMPARE(model->data(model->index(0), ReactionModel::ToolTipRole),
|
||||
QStringLiteral("@alice:matrix.org reacted with <span style=\"font-family: 'emoji';\">👍</span>"));
|
||||
|
||||
QSignalSpy spy(model, SIGNAL(modelReset()));
|
||||
|
||||
room->syncNewEvents(QLatin1String("test-reactionmodel-extra-sync.json"));
|
||||
QCOMPARE(model->rowCount(), 2);
|
||||
QCOMPARE(spy.count(), 2); // Once for each of the 2 new reactions.
|
||||
QCOMPARE(model->data(model->index(1), ReactionModel::ReactionRole), QStringLiteral("😆"));
|
||||
QCOMPARE(model->data(model->index(0), ReactionModel::ToolTipRole),
|
||||
QStringLiteral("@alice:matrix.org and @bob:example.org reacted with <span style=\"font-family: 'emoji';\">👍</span>"));
|
||||
|
||||
delete model;
|
||||
}
|
||||
|
||||
QTEST_MAIN(ReactionModelTest)
|
||||
#include "reactionmodeltest.moc"
|
||||
55
autotests/testutils.h
Normal file
55
autotests/testutils.h
Normal file
@@ -0,0 +1,55 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include <Quotient/events/event.h>
|
||||
#include <Quotient/syncdata.h>
|
||||
|
||||
#include "neochatroom.h"
|
||||
|
||||
namespace Quotient
|
||||
{
|
||||
class Connection;
|
||||
}
|
||||
|
||||
namespace TestUtils
|
||||
{
|
||||
class TestRoom : public NeoChatRoom
|
||||
{
|
||||
public:
|
||||
TestRoom(Quotient::Connection *connection, const QString &roomName, const QString &syncFileName = {})
|
||||
: NeoChatRoom(connection, roomName, Quotient::JoinState::Join)
|
||||
{
|
||||
syncNewEvents(syncFileName);
|
||||
}
|
||||
|
||||
void update(Quotient::SyncRoomData &&data, bool fromCache = false)
|
||||
{
|
||||
Room::updateData(std::move(data), fromCache);
|
||||
}
|
||||
|
||||
void syncNewEvents(const QString &syncFileName)
|
||||
{
|
||||
if (!syncFileName.isEmpty()) {
|
||||
QFile testSyncFile;
|
||||
testSyncFile.setFileName(QLatin1String(DATA_DIR) + u'/' + syncFileName);
|
||||
testSyncFile.open(QIODevice::ReadOnly);
|
||||
const auto testSyncJson = QJsonDocument::fromJson(testSyncFile.readAll());
|
||||
Quotient::SyncRoomData roomData(id(), Quotient::JoinState::Join, testSyncJson.object());
|
||||
update(std::move(roomData));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template<Quotient::EventClass EventT>
|
||||
inline Quotient::event_ptr_tt<EventT> loadEventFromFile(const QString &eventFileName)
|
||||
{
|
||||
if (!eventFileName.isEmpty()) {
|
||||
QFile testEventFile;
|
||||
testEventFile.setFileName(QLatin1String(DATA_DIR) + u'/' + eventFileName);
|
||||
testEventFile.open(QIODevice::ReadOnly);
|
||||
auto testSyncJson = QJsonDocument::fromJson(testEventFile.readAll()).object();
|
||||
return Quotient::loadEvent<EventT>(testSyncJson);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
@@ -10,28 +10,21 @@
|
||||
#include <Quotient/syncdata.h>
|
||||
#include <qnamespace.h>
|
||||
|
||||
#include "models/customemojimodel.h"
|
||||
#include "neochatconnection.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include "testutils.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
class TestRoom : public NeoChatRoom
|
||||
{
|
||||
public:
|
||||
using NeoChatRoom::NeoChatRoom;
|
||||
|
||||
void update(SyncRoomData &&data, bool fromCache = false)
|
||||
{
|
||||
Room::updateData(std::move(data), fromCache);
|
||||
}
|
||||
};
|
||||
|
||||
class TextHandlerTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
Connection *connection = nullptr;
|
||||
TestRoom *room = nullptr;
|
||||
TestUtils::TestRoom *room = nullptr;
|
||||
|
||||
private Q_SLOTS:
|
||||
void initTestCase();
|
||||
@@ -48,6 +41,9 @@ private Q_SLOTS:
|
||||
void sendBadLinks();
|
||||
void sendEscapeCode();
|
||||
void sendCodeClass();
|
||||
void sendCustomEmoji();
|
||||
void sendCustomEmojiCode_data();
|
||||
void sendCustomEmojiCode();
|
||||
|
||||
void receiveStripReply();
|
||||
void receivePlainTextIn();
|
||||
@@ -68,24 +64,20 @@ private Q_SLOTS:
|
||||
void receiveRichEdited();
|
||||
void receiveLineSeparator();
|
||||
void receiveRichCodeUrl();
|
||||
|
||||
void linkPreviewsMatch_data();
|
||||
void linkPreviewsMatch();
|
||||
void linkPreviewsReject_data();
|
||||
void linkPreviewsReject();
|
||||
};
|
||||
|
||||
void TextHandlerTest::initTestCase()
|
||||
{
|
||||
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
|
||||
room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join);
|
||||
connection->setAccountData("im.ponies.user_emotes"_ls,
|
||||
QJsonObject{{"images"_ls,
|
||||
QJsonObject{{"test"_ls,
|
||||
QJsonObject{{"body"_ls, "Test custom emoji"_ls},
|
||||
{"url"_ls, "mxc://example.org/test"_ls},
|
||||
{"usage"_ls, QJsonArray{"emoticon"_ls}}}}}}});
|
||||
CustomEmojiModel::instance().setConnection(static_cast<NeoChatConnection *>(connection));
|
||||
|
||||
QFile testTextHandlerSyncFile;
|
||||
testTextHandlerSyncFile.setFileName(QLatin1String(DATA_DIR) + u'/' + QLatin1String("test-texthandler-sync.json"));
|
||||
testTextHandlerSyncFile.open(QIODevice::ReadOnly);
|
||||
const auto testTextHandlerSyncJson = QJsonDocument::fromJson(testTextHandlerSyncFile.readAll());
|
||||
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, testTextHandlerSyncJson.object());
|
||||
room->update(std::move(roomData));
|
||||
room = new TestUtils::TestRoom(connection, QStringLiteral("#myroom:kde.org"), QLatin1String("test-texthandler-sync.json"));
|
||||
}
|
||||
|
||||
void TextHandlerTest::allowedAttributes()
|
||||
@@ -234,6 +226,39 @@ void TextHandlerTest::sendCodeClass()
|
||||
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
|
||||
}
|
||||
|
||||
void TextHandlerTest::sendCustomEmoji()
|
||||
{
|
||||
const QString testInputString = QStringLiteral(":test:");
|
||||
const QString testOutputString = QStringLiteral(
|
||||
"<p><img data-mx-emoticon=\"\" src=\"mxc://example.org/test\" alt=\":test:\" title=\":test:\" height=\"32\" vertical-align=\"middle\" /></p>");
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
|
||||
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
|
||||
}
|
||||
|
||||
void TextHandlerTest::sendCustomEmojiCode_data()
|
||||
{
|
||||
QTest::addColumn<QString>("testInputString");
|
||||
QTest::addColumn<QString>("testOutputString");
|
||||
|
||||
QTest::newRow("inline") << QStringLiteral("`:test:`") << QStringLiteral("<p><code>:test:</code></p>");
|
||||
QTest::newRow("block") << QStringLiteral("```\n:test:\n```") << QStringLiteral("<pre><code>:test:\n</code></pre>");
|
||||
}
|
||||
|
||||
// Custom emojis in code blocks should be left alone.
|
||||
void TextHandlerTest::sendCustomEmojiCode()
|
||||
{
|
||||
QFETCH(QString, testInputString);
|
||||
QFETCH(QString, testOutputString);
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
|
||||
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
|
||||
}
|
||||
|
||||
void TextHandlerTest::receiveStripReply()
|
||||
{
|
||||
const QString testInputString = QStringLiteral(
|
||||
@@ -493,53 +518,6 @@ void TextHandlerTest::receiveLineSeparator()
|
||||
QCOMPARE(textHandler.handleRecievePlainText(Qt::PlainText, true), QStringLiteral("foo bar"));
|
||||
}
|
||||
|
||||
void TextHandlerTest::linkPreviewsMatch_data()
|
||||
{
|
||||
QTest::addColumn<QString>("testInputString");
|
||||
QTest::addColumn<QList<QUrl>>("testOutputLinks");
|
||||
|
||||
QTest::newRow("plainHttps") << QStringLiteral("https://kde.org") << QList<QUrl>({QUrl("https://kde.org"_ls)});
|
||||
QTest::newRow("richHttps") << QStringLiteral("<a href=\"https://kde.org\">Rich Link</a>") << QList<QUrl>({QUrl("https://kde.org"_ls)});
|
||||
QTest::newRow("plainWww") << QStringLiteral("www.example.org") << QList<QUrl>({QUrl("www.example.org"_ls)});
|
||||
QTest::newRow("multipleHttps") << QStringLiteral("https://kde.org www.example.org")
|
||||
<< QList<QUrl>({
|
||||
QUrl("https://kde.org"_ls),
|
||||
QUrl("www.example.org"_ls),
|
||||
});
|
||||
}
|
||||
|
||||
void TextHandlerTest::linkPreviewsMatch()
|
||||
{
|
||||
QFETCH(QString, testInputString);
|
||||
QFETCH(QList<QUrl>, testOutputLinks);
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
|
||||
QCOMPARE(testTextHandler.getLinkPreviews(), testOutputLinks);
|
||||
}
|
||||
|
||||
void TextHandlerTest::linkPreviewsReject_data()
|
||||
{
|
||||
QTest::addColumn<QString>("testInputString");
|
||||
QTest::addColumn<QList<QUrl>>("testOutputLinks");
|
||||
|
||||
QTest::newRow("mxc") << QStringLiteral("mxc://example.org/SEsfnsuifSDFSSEF") << QList<QUrl>();
|
||||
QTest::newRow("matrixTo") << QStringLiteral("https://matrix.to/#/@alice:example.org") << QList<QUrl>();
|
||||
QTest::newRow("noSpace") << QStringLiteral("testhttps://kde.org") << QList<QUrl>();
|
||||
}
|
||||
|
||||
void TextHandlerTest::linkPreviewsReject()
|
||||
{
|
||||
QFETCH(QString, testInputString);
|
||||
QFETCH(QList<QUrl>, testOutputLinks);
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
|
||||
QCOMPARE(testTextHandler.getLinkPreviews(), testOutputLinks);
|
||||
}
|
||||
|
||||
void TextHandlerTest::receiveRichCodeUrl()
|
||||
{
|
||||
auto input = QStringLiteral("<code>https://kde.org</code>");
|
||||
|
||||
102
autotests/windowcontrollertest.cpp
Normal file
102
autotests/windowcontrollertest.cpp
Normal file
@@ -0,0 +1,102 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QTest>
|
||||
#include <QWindow>
|
||||
|
||||
#include <KConfig>
|
||||
#include <KSharedConfig>
|
||||
#include <KWindowConfig>
|
||||
|
||||
#include "windowcontroller.h"
|
||||
|
||||
class WindowControllerTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private Q_SLOTS:
|
||||
void nullWindow();
|
||||
void geometry();
|
||||
void showAndRaise();
|
||||
void toggle();
|
||||
|
||||
void cleanup();
|
||||
};
|
||||
|
||||
// Basically don't crash when no window is set.
|
||||
void WindowControllerTest::nullWindow()
|
||||
{
|
||||
auto &instance = WindowController::instance();
|
||||
QCOMPARE(instance.window(), nullptr);
|
||||
|
||||
instance.restoreGeometry();
|
||||
instance.saveGeometry();
|
||||
instance.showAndRaiseWindow({});
|
||||
instance.toggleWindow();
|
||||
}
|
||||
|
||||
void WindowControllerTest::geometry()
|
||||
{
|
||||
auto &instance = WindowController::instance();
|
||||
|
||||
QWindow window;
|
||||
window.setGeometry(0, 0, 200, 200);
|
||||
instance.setWindow(&window);
|
||||
QCOMPARE(instance.window(), &window);
|
||||
|
||||
instance.saveGeometry();
|
||||
const auto stateConfig = KSharedConfig::openStateConfig();
|
||||
KConfigGroup windowGroup = stateConfig->group(QStringLiteral("Window"));
|
||||
QCOMPARE(KWindowConfig::hasSavedWindowSize(windowGroup), true);
|
||||
|
||||
window.setGeometry(0, 0, 400, 400);
|
||||
QCOMPARE(window.geometry(), QRect(0, 0, 400, 400));
|
||||
instance.restoreGeometry();
|
||||
QCOMPARE(window.geometry(), QRect(0, 0, 200, 200));
|
||||
}
|
||||
|
||||
void WindowControllerTest::showAndRaise()
|
||||
{
|
||||
auto &instance = WindowController::instance();
|
||||
QWindow window;
|
||||
instance.setWindow(&window);
|
||||
QCOMPARE(window.isVisible(), false);
|
||||
|
||||
instance.showAndRaiseWindow({});
|
||||
QCOMPARE(window.isVisible(), true);
|
||||
}
|
||||
|
||||
void WindowControllerTest::cleanup()
|
||||
{
|
||||
auto &instance = WindowController::instance();
|
||||
instance.setWindow(nullptr);
|
||||
QCOMPARE(instance.window(), nullptr);
|
||||
}
|
||||
|
||||
void WindowControllerTest::toggle()
|
||||
{
|
||||
auto &instance = WindowController::instance();
|
||||
QWindow window;
|
||||
instance.setWindow(&window);
|
||||
QCOMPARE(window.isVisible(), false);
|
||||
instance.toggleWindow();
|
||||
QCOMPARE(window.isVisible(), true);
|
||||
instance.toggleWindow();
|
||||
QCOMPARE(window.isVisible(), false);
|
||||
|
||||
// A window is classed as visible by qt when minimized but to the user this is not visible.
|
||||
// So in this case we expect to show it even though visibility is technically true.
|
||||
window.setVisibility(QWindow::Minimized);
|
||||
QCOMPARE(window.windowState(), Qt::WindowMinimized);
|
||||
QCOMPARE(window.isVisible(), true);
|
||||
instance.toggleWindow();
|
||||
QCOMPARE(window.windowState(), Qt::WindowNoState);
|
||||
QCOMPARE(window.isVisible(), true);
|
||||
instance.toggleWindow();
|
||||
QCOMPARE(window.windowState(), Qt::WindowNoState);
|
||||
QCOMPARE(window.isVisible(), false);
|
||||
}
|
||||
|
||||
QTEST_MAIN(WindowControllerTest)
|
||||
#include "windowcontrollertest.moc"
|
||||
@@ -47,10 +47,12 @@
|
||||
<name xml:lang="uk">NeoChat</name>
|
||||
<name xml:lang="x-test">xxNeoChatxx</name>
|
||||
<name xml:lang="zh-CN">NeoChat</name>
|
||||
<name xml:lang="zh-TW">NeoChat</name>
|
||||
<summary>Chat with your friends on matrix</summary>
|
||||
<summary xml:lang="ar">دردش مع أصدقائك على ماتركس</summary>
|
||||
<summary xml:lang="ca">Xategeu amb els vostres amics a Matrix</summary>
|
||||
<summary xml:lang="ca-valencia">Xategeu amb els vostres amics a Matrix</summary>
|
||||
<summary xml:lang="cs">Mluvte se svými přáteli na Matrixu</summary>
|
||||
<summary xml:lang="eo">Babilu kun viaj amikoj sur matrix</summary>
|
||||
<summary xml:lang="es">Charle con sus amigos en matrix</summary>
|
||||
<summary xml:lang="eu">Berriketan jardun zure lagunekin «Matrix»en</summary>
|
||||
@@ -70,6 +72,8 @@
|
||||
<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>
|
||||
@@ -83,6 +87,7 @@ to provide a convergent experience across multiple platforms.</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>
|
||||
@@ -96,6 +101,7 @@ to provide a convergent experience across multiple platforms.</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 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>
|
||||
@@ -120,6 +126,7 @@ to provide a convergent experience across multiple platforms.</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>
|
||||
<p>Due to the nature of the Matrix specification development NeoChat also supports numerous unstable features. Currently these are:</p>
|
||||
<p xml:lang="ar">نظرًا لطبيعة تطوير مواصفات ماتركس، يدعم نيوتشات أيضًا العديد من الميزات غير المستقرة وهي:</p>
|
||||
<p xml:lang="ca">A causa de la naturalesa del desenvolupament de l'especificació de Matrix, el NeoChat també implementa nombroses característiques inestables. Actualment són:</p>
|
||||
@@ -145,6 +152,7 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<p xml:lang="tr">NeoChat, Matrix belirtimi geliştirmesinin doğası gereği çok sayıda kararsız özelliği de destekler. Şu anda bunlar:</p>
|
||||
<p xml:lang="uk">Через природу розробки специфікації Matrix, у NeoChat також передбачено підтримку численних нестабільних можливостей. У поточній версії цими можливостями є:</p>
|
||||
<p xml:lang="x-test">xxDue to the nature of the Matrix specification development NeoChat also supports numerous unstable features. Currently these are:xx</p>
|
||||
<p xml:lang="zh-TW">由於 Matrix 標準的開發流程的緣故,NeoChat 也支援數個非穩定版的功能。目前這些功能是:</p>
|
||||
<ul>
|
||||
<li>Polls - MSC3381</li>
|
||||
<li xml:lang="ar">التصويت - MSC3381</li>
|
||||
@@ -171,6 +179,7 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<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>
|
||||
<li>Sticker Packs - MSC2545</li>
|
||||
<li xml:lang="ar">حزم الملصقات - MSC2545</li>
|
||||
<li xml:lang="ca">Paquets d'adhesius - MSC2545</li>
|
||||
@@ -196,6 +205,7 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<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>
|
||||
<li>Location Events - MSC3488</li>
|
||||
<li xml:lang="ar">موقع الأحداث - MSC3488</li>
|
||||
<li xml:lang="ca">Esdeveniments d'ubicació - MSC3488</li>
|
||||
@@ -221,10 +231,11 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<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>
|
||||
</ul>
|
||||
</description>
|
||||
<url type="homepage">https://apps.kde.org/neochat/</url>
|
||||
<url type="bugtracker">https://bugs.kde.org/buglist.cgi?component=General&product=NeoChat</url>
|
||||
<url type="homepage">https://apps.kde.org/neochat</url>
|
||||
<url type="bugtracker">https://bugs.kde.org/enter_bug.cgi?product=NeoChat</url>
|
||||
<categories>
|
||||
<category>Network</category>
|
||||
</categories>
|
||||
@@ -265,6 +276,7 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<developer_name xml:lang="uk">Спільнота KDE</developer_name>
|
||||
<developer_name xml:lang="x-test">xxThe KDE Communityxx</developer_name>
|
||||
<developer_name xml:lang="zh-CN">KDE 社区</developer_name>
|
||||
<developer_name xml:lang="zh-TW">KDE 社群</developer_name>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>GPL-3.0</project_license>
|
||||
<custom>
|
||||
@@ -310,6 +322,7 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<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 environment="windows">
|
||||
<image>https://cdn.kde.org/screenshots/neochat/NeoChat-Windows-Login.png</image>
|
||||
@@ -317,6 +330,7 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<caption xml:lang="ar">شاشة الدخول</caption>
|
||||
<caption xml:lang="ca">Pantalla d'inici de sessió</caption>
|
||||
<caption xml:lang="ca-valencia">Pantalla d'inici de sessió</caption>
|
||||
<caption xml:lang="cs">Přihlašovací obrazovka</caption>
|
||||
<caption xml:lang="eo">Ensaluta ekrano</caption>
|
||||
<caption xml:lang="es">Pantalla de inicio de sesión</caption>
|
||||
<caption xml:lang="eu">Saio-hasteko pantaila</caption>
|
||||
@@ -337,12 +351,32 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<caption xml:lang="tr">Oturum açma ekranı</caption>
|
||||
<caption xml:lang="uk">Вікно входу</caption>
|
||||
<caption xml:lang="x-test">xxLogin screenxx</caption>
|
||||
<caption xml:lang="zh-TW">登入畫面</caption>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<content_rating type="oars-1.1">
|
||||
<content_attribute id="social-chat">intense</content_attribute>
|
||||
</content_rating>
|
||||
<releases>
|
||||
<release version="24.02.0" date="2024-02-28">
|
||||
<url>https://kde.org/announcements/megarelease/6/#neochat</url>
|
||||
<description>
|
||||
<p>In the newest version, when launching the app, you will get a welcome page that lets you choose which account you want to use and lets you log in to other accounts. The welcome screen will also warn you when NeoChat cannot load an account.</p>
|
||||
<p>NeoChat will also let you register a new account directly from the app itself. Deactivating your Matrix account is also possible from within NeoChat.</p>
|
||||
<p>Spaces are a relatively new feature of Matrix that let you group chat channels together. This is used to improve the discoverability of rooms, manage large communities, or just tidy all the channels you are in. You can now do all this without leaving NeoChat.</p>
|
||||
<p>NeoChat won't let you miss any new notifications anymore. We added a new page that includes all your recent notifications and when NeoChat is closed, you will still be able to receive push notifications. The main timeline will let you know when more messages are loading and when you reach the end of it.</p>
|
||||
<p>More NeoChat Goodies</p>
|
||||
<ul>
|
||||
<li>QR Codes to share contacts</li>
|
||||
<li>Improved room upgrades</li>
|
||||
<li>Added button to reject invitation and ignore user</li>
|
||||
<li>Display device security details</li>
|
||||
<li>Added room security settings</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="23.08.5" date="2024-02-15"/>
|
||||
<release version="23.08.4" date="2023-12-07"/>
|
||||
<release version="23.08.3" date="2023-11-09"/>
|
||||
<release version="23.08.2" date="2023-10-12"/>
|
||||
<release version="23.08.0" date="2023-08-24">
|
||||
|
||||
@@ -42,6 +42,7 @@ Name[tr]=NeoChat
|
||||
Name[uk]=NeoChat
|
||||
Name[x-test]=xxNeoChatxx
|
||||
Name[zh_CN]=NeoChat
|
||||
Name[zh_TW]=NeoChat
|
||||
GenericName=Matrix Client
|
||||
GenericName[ar]=عميل ماتركس
|
||||
GenericName[az]=Matrix Müştərisi
|
||||
@@ -81,6 +82,7 @@ GenericName[tr]=Matrix İstemcisi
|
||||
GenericName[uk]=Клієнт Matrix
|
||||
GenericName[x-test]=xxMatrix Clientxx
|
||||
GenericName[zh_CN]=Matrix 客户端
|
||||
GenericName[zh_TW]=Matrix 用戶端
|
||||
Comment=Client for the Matrix protocol
|
||||
Comment[ar]=عميل لميفاق ماتركس
|
||||
Comment[az]=Matrix protokolu üçün müştəri
|
||||
@@ -119,6 +121,7 @@ Comment[tr]=Matrix protokolü için istemci
|
||||
Comment[uk]=Клієнт протоколу Matrix
|
||||
Comment[x-test]=xxClient for the Matrix protocolxx
|
||||
Comment[zh_CN]=为 Matrix 协议打造的客户端
|
||||
Comment[zh_TW]=Matrix 通訊協定的用戶端
|
||||
MimeType=x-scheme-handler/matrix;
|
||||
Exec=neochat %u
|
||||
Terminal=false
|
||||
|
||||
749
po/ar/neochat.po
749
po/ar/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
733
po/az/neochat.po
733
po/az/neochat.po
File diff suppressed because it is too large
Load Diff
693
po/ca/neochat.po
693
po/ca/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
775
po/cs/neochat.po
775
po/cs/neochat.po
File diff suppressed because it is too large
Load Diff
697
po/da/neochat.po
697
po/da/neochat.po
File diff suppressed because it is too large
Load Diff
750
po/de/neochat.po
750
po/de/neochat.po
File diff suppressed because it is too large
Load Diff
746
po/el/neochat.po
746
po/el/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
700
po/eo/neochat.po
700
po/eo/neochat.po
File diff suppressed because it is too large
Load Diff
725
po/es/neochat.po
725
po/es/neochat.po
File diff suppressed because it is too large
Load Diff
797
po/eu/neochat.po
797
po/eu/neochat.po
File diff suppressed because it is too large
Load Diff
993
po/fi/neochat.po
993
po/fi/neochat.po
File diff suppressed because it is too large
Load Diff
733
po/fr/neochat.po
733
po/fr/neochat.po
File diff suppressed because it is too large
Load Diff
928
po/hu/neochat.po
928
po/hu/neochat.po
File diff suppressed because it is too large
Load Diff
723
po/ia/neochat.po
723
po/ia/neochat.po
File diff suppressed because it is too large
Load Diff
749
po/id/neochat.po
749
po/id/neochat.po
File diff suppressed because it is too large
Load Diff
720
po/ie/neochat.po
720
po/ie/neochat.po
File diff suppressed because it is too large
Load Diff
727
po/it/neochat.po
727
po/it/neochat.po
File diff suppressed because it is too large
Load Diff
681
po/ja/neochat.po
681
po/ja/neochat.po
File diff suppressed because it is too large
Load Diff
723
po/ka/neochat.po
723
po/ka/neochat.po
File diff suppressed because it is too large
Load Diff
916
po/ko/neochat.po
916
po/ko/neochat.po
File diff suppressed because it is too large
Load Diff
681
po/lt/neochat.po
681
po/lt/neochat.po
File diff suppressed because it is too large
Load Diff
728
po/nl/neochat.po
728
po/nl/neochat.po
File diff suppressed because it is too large
Load Diff
707
po/nn/neochat.po
707
po/nn/neochat.po
File diff suppressed because it is too large
Load Diff
722
po/pa/neochat.po
722
po/pa/neochat.po
File diff suppressed because it is too large
Load Diff
782
po/pl/neochat.po
782
po/pl/neochat.po
File diff suppressed because it is too large
Load Diff
749
po/pt/neochat.po
749
po/pt/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1137
po/ru/neochat.po
1137
po/ru/neochat.po
File diff suppressed because it is too large
Load Diff
733
po/sk/neochat.po
733
po/sk/neochat.po
File diff suppressed because it is too large
Load Diff
731
po/sl/neochat.po
731
po/sl/neochat.po
File diff suppressed because it is too large
Load Diff
737
po/sv/neochat.po
737
po/sv/neochat.po
File diff suppressed because it is too large
Load Diff
946
po/ta/neochat.po
946
po/ta/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
735
po/tr/neochat.po
735
po/tr/neochat.po
File diff suppressed because it is too large
Load Diff
725
po/uk/neochat.po
725
po/uk/neochat.po
File diff suppressed because it is too large
Load Diff
1416
po/zh_CN/neochat.po
1416
po/zh_CN/neochat.po
File diff suppressed because it is too large
Load Diff
2342
po/zh_TW/neochat.po
2342
po/zh_TW/neochat.po
File diff suppressed because it is too large
Load Diff
@@ -143,6 +143,7 @@ add_library(neochat STATIC
|
||||
models/notificationsmodel.h
|
||||
models/timelinemodel.cpp
|
||||
models/timelinemodel.h
|
||||
enums/pushrule.h
|
||||
)
|
||||
|
||||
qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
|
||||
@@ -475,6 +476,20 @@ if(ANDROID)
|
||||
"computer-symbolic"
|
||||
"gps"
|
||||
"system-users-symbolic"
|
||||
"map-flat"
|
||||
"documentinfo"
|
||||
"view-list-details"
|
||||
"go-previous"
|
||||
"mail-forward-symbolic"
|
||||
"dialog-warning-symbolic"
|
||||
"object-rotate-left"
|
||||
"object-rotate-right"
|
||||
"add-subtitle"
|
||||
"security-low"
|
||||
"security-low-symbolic"
|
||||
"kde"
|
||||
"list-remove-symbolic"
|
||||
"edit-delete"
|
||||
)
|
||||
ecm_add_android_apk(neochat-app ANDROID_DIR ${CMAKE_SOURCE_DIR}/android)
|
||||
else()
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
#include <QStringBuilder>
|
||||
|
||||
#include "models/actionsmodel.h"
|
||||
#include "models/customemojimodel.h"
|
||||
#include "neochatconfig.h"
|
||||
#include "texthandler.h"
|
||||
|
||||
@@ -40,7 +39,8 @@ void ActionsHandler::setRoom(NeoChatRoom *room)
|
||||
|
||||
void ActionsHandler::handleMessageEvent(ChatBarCache *chatBarCache)
|
||||
{
|
||||
if (!chatBarCache) {
|
||||
if (!m_room || !chatBarCache) {
|
||||
qWarning() << "ActionsHandler::handleMessageEvent - called with m_room and/or chatBarCache set to nullptr.";
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -61,10 +61,6 @@ void ActionsHandler::handleMessageEvent(ChatBarCache *chatBarCache)
|
||||
|
||||
QString ActionsHandler::handleMentions(QString handledText, QList<Mention> *mentions)
|
||||
{
|
||||
if (!m_room) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
std::sort(mentions->begin(), mentions->end(), [](const auto &a, const auto &b) -> bool {
|
||||
return a.cursor.anchor() > b.cursor.anchor();
|
||||
});
|
||||
@@ -135,7 +131,6 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, Cha
|
||||
}
|
||||
}
|
||||
|
||||
handledText = CustomEmojiModel::instance().preprocessText(handledText);
|
||||
TextHandler textHandler;
|
||||
textHandler.setData(handledText);
|
||||
handledText = textHandler.handleSendText();
|
||||
|
||||
@@ -7,10 +7,7 @@
|
||||
#include <qt6keychain/keychain.h>
|
||||
|
||||
#include <KLocalizedString>
|
||||
#include <KWindowConfig>
|
||||
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QGuiApplication>
|
||||
#include <QNetworkProxy>
|
||||
#include <QQuickTextDocument>
|
||||
@@ -28,13 +25,11 @@
|
||||
#include <Quotient/eventstats.h>
|
||||
#include <Quotient/jobs/downloadfilejob.h>
|
||||
#include <Quotient/qt_connection_util.h>
|
||||
#include <Quotient/user.h>
|
||||
|
||||
#include "neochatconfig.h"
|
||||
#include "neochatroom.h"
|
||||
#include "notificationsmanager.h"
|
||||
#include "roommanager.h"
|
||||
#include "windowcontroller.h"
|
||||
|
||||
#if defined(Q_OS_WIN) || defined(Q_OS_MAC)
|
||||
#include "trayicon.h"
|
||||
@@ -68,7 +63,6 @@ Controller::Controller(QObject *parent)
|
||||
connect(c, &Connection::connected, this, [c, this]() {
|
||||
m_accountRegistry.add(c);
|
||||
c->syncLoop();
|
||||
Q_EMIT initiated();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -78,8 +72,7 @@ Controller::Controller(QObject *parent)
|
||||
});
|
||||
|
||||
#ifndef Q_OS_WINDOWS
|
||||
// Setup Unix signal handlers
|
||||
const auto unixExitHandler = [](int /*sig*/) -> void {
|
||||
const auto unixExitHandler = [](int) -> void {
|
||||
QCoreApplication::quit();
|
||||
};
|
||||
|
||||
@@ -105,7 +98,7 @@ Controller::Controller(QObject *parent)
|
||||
connect(&m_accountRegistry, &AccountRegistry::accountCountChanged, this, [this]() {
|
||||
if (m_accountRegistry.size() > oldAccountCount) {
|
||||
auto connection = dynamic_cast<NeoChatConnection *>(m_accountRegistry.accounts()[m_accountRegistry.size() - 1]);
|
||||
connect(connection, &NeoChatConnection::syncDone, this, [this, connection]() {
|
||||
connect(connection, &NeoChatConnection::syncDone, this, [connection]() {
|
||||
NotificationsManager::instance().handleNotifications(connection);
|
||||
});
|
||||
connectSingleShot(connection, &NeoChatConnection::syncDone, this, [this, connection] {
|
||||
@@ -137,23 +130,6 @@ Controller &Controller::instance()
|
||||
return _instance;
|
||||
}
|
||||
|
||||
void Controller::toggleWindow()
|
||||
{
|
||||
auto &instance = WindowController::instance();
|
||||
auto window = instance.window();
|
||||
if (window->isVisible()) {
|
||||
if (window->windowStates() & Qt::WindowMinimized) {
|
||||
window->showNormal();
|
||||
window->requestActivate();
|
||||
} else {
|
||||
window->close();
|
||||
}
|
||||
} else {
|
||||
instance.showAndRaiseWindow({});
|
||||
instance.window()->requestActivate();
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::addConnection(NeoChatConnection *c)
|
||||
{
|
||||
Q_ASSERT_X(c, __FUNCTION__, "Attempt to add a null connection");
|
||||
@@ -162,9 +138,7 @@ void Controller::addConnection(NeoChatConnection *c)
|
||||
|
||||
c->setLazyLoading(true);
|
||||
|
||||
connect(c, &NeoChatConnection::syncDone, this, [this, c] {
|
||||
Q_EMIT syncDone();
|
||||
|
||||
connect(c, &NeoChatConnection::syncDone, this, [c] {
|
||||
c->sync(30000);
|
||||
c->saveState();
|
||||
});
|
||||
@@ -172,12 +146,6 @@ void Controller::addConnection(NeoChatConnection *c)
|
||||
dropConnection(c);
|
||||
});
|
||||
|
||||
connect(c, &NeoChatConnection::requestFailed, this, [this](BaseJob *job) {
|
||||
if (job->error() == BaseJob::UserConsentRequired) {
|
||||
Q_EMIT userConsentRequired(job->errorUrl());
|
||||
}
|
||||
});
|
||||
|
||||
c->sync();
|
||||
|
||||
Q_EMIT connectionAdded(c);
|
||||
@@ -194,18 +162,13 @@ void Controller::dropConnection(NeoChatConnection *c)
|
||||
void Controller::invokeLogin()
|
||||
{
|
||||
const auto accounts = SettingsGroup("Accounts"_ls).childGroups();
|
||||
QString id = NeoChatConfig::self()->activeConnection();
|
||||
for (const auto &accountId : accounts) {
|
||||
AccountSettings account{accountId};
|
||||
m_accountsLoading += accountId;
|
||||
Q_EMIT accountsLoadingChanged();
|
||||
if (id.isEmpty()) {
|
||||
// handle case where the account config is empty
|
||||
id = accountId;
|
||||
}
|
||||
if (!account.homeserver().isEmpty()) {
|
||||
auto accessTokenLoadingJob = loadAccessTokenFromKeyChain(account);
|
||||
connect(accessTokenLoadingJob, &QKeychain::Job::finished, this, [accountId, id, this, accessTokenLoadingJob](QKeychain::Job *) {
|
||||
connect(accessTokenLoadingJob, &QKeychain::Job::finished, this, [accountId, this, accessTokenLoadingJob](QKeychain::Job *) {
|
||||
AccountSettings account{accountId};
|
||||
QString accessToken;
|
||||
if (accessTokenLoadingJob->error() == QKeychain::Error::NoError) {
|
||||
@@ -215,41 +178,21 @@ void Controller::invokeLogin()
|
||||
}
|
||||
|
||||
auto connection = new NeoChatConnection(account.homeserver());
|
||||
connect(connection, &NeoChatConnection::connected, this, [this, connection, id] {
|
||||
m_connectionsLoading[accountId] = connection;
|
||||
connect(connection, &NeoChatConnection::connected, this, [this, connection, accountId] {
|
||||
connection->loadState();
|
||||
addConnection(connection);
|
||||
m_accountsLoading.removeAll(connection->userId());
|
||||
m_connectionsLoading.remove(accountId);
|
||||
Q_EMIT accountsLoadingChanged();
|
||||
if (connection->userId() == id) {
|
||||
setActiveConnection(connection);
|
||||
connectSingleShot(connection, &NeoChatConnection::syncDone, this, &Controller::initiated);
|
||||
}
|
||||
});
|
||||
connect(connection, &NeoChatConnection::loginError, this, [this, connection](const QString &error, const QString &) {
|
||||
if (error == "Unrecognised access token"_ls) {
|
||||
Q_EMIT errorOccured(i18n("Login Failed: Access Token invalid or revoked"));
|
||||
connection->logout(false);
|
||||
} else if (error == "Connection closed"_ls) {
|
||||
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
|
||||
// Failed due to network connection issue. This might happen when the homeserver is
|
||||
// temporary down, or the user trying to re-launch NeoChat in a network that cannot
|
||||
// connect to the homeserver. In this case, we don't want to do logout().
|
||||
} else {
|
||||
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
|
||||
connection->logout(true);
|
||||
}
|
||||
Q_EMIT initiated();
|
||||
});
|
||||
connect(connection, &NeoChatConnection::networkError, this, [this](const QString &error, const QString &, int, int) {
|
||||
Q_EMIT errorOccured(i18n("Network Error: %1", error));
|
||||
Q_EMIT errorOccured(i18n("Network Error: %1", error), {});
|
||||
});
|
||||
connection->assumeIdentity(account.userId(), accessToken);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (accounts.isEmpty()) {
|
||||
Q_EMIT initiated();
|
||||
}
|
||||
}
|
||||
|
||||
QKeychain::ReadPasswordJob *Controller::loadAccessTokenFromKeyChain(const AccountSettings &account)
|
||||
@@ -266,17 +209,17 @@ QKeychain::ReadPasswordJob *Controller::loadAccessTokenFromKeyChain(const Accoun
|
||||
|
||||
switch (job->error()) {
|
||||
case QKeychain::EntryNotFound:
|
||||
Q_EMIT globalErrorOccured(i18n("Access token wasn't found"), i18n("Maybe it was deleted?"));
|
||||
Q_EMIT errorOccured(i18n("Access token wasn't found"), i18n("Maybe it was deleted?"));
|
||||
break;
|
||||
case QKeychain::AccessDeniedByUser:
|
||||
case QKeychain::AccessDenied:
|
||||
Q_EMIT globalErrorOccured(i18n("Access to keychain was denied."), i18n("Please allow NeoChat to read the access token"));
|
||||
Q_EMIT errorOccured(i18n("Access to keychain was denied."), i18n("Please allow NeoChat to read the access token"));
|
||||
break;
|
||||
case QKeychain::NoBackendAvailable:
|
||||
Q_EMIT globalErrorOccured(i18n("No keychain available."), i18n("Please install a keychain, e.g. KWallet or GNOME keyring on Linux"));
|
||||
Q_EMIT errorOccured(i18n("No keychain available."), i18n("Please install a keychain, e.g. KWallet or GNOME keyring on Linux"));
|
||||
break;
|
||||
case QKeychain::OtherError:
|
||||
Q_EMIT globalErrorOccured(i18n("Unable to read access token"), job->errorString());
|
||||
Q_EMIT errorOccured(i18n("Unable to read access token"), job->errorString());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -322,10 +265,8 @@ void Controller::setQuitOnLastWindowClosed()
|
||||
if (NeoChatConfig::self()->systemTray()) {
|
||||
m_trayIcon = new TrayIcon(this);
|
||||
m_trayIcon->show();
|
||||
connect(m_trayIcon, &TrayIcon::toggleWindow, this, &Controller::toggleWindow);
|
||||
} else {
|
||||
if (m_trayIcon) {
|
||||
disconnect(m_trayIcon, &TrayIcon::toggleWindow, this, &Controller::toggleWindow);
|
||||
delete m_trayIcon;
|
||||
m_trayIcon = nullptr;
|
||||
}
|
||||
@@ -352,24 +293,16 @@ void Controller::setActiveConnection(NeoChatConnection *connection)
|
||||
}
|
||||
m_connection = connection;
|
||||
if (connection != nullptr) {
|
||||
NeoChatConfig::self()->setActiveConnection(connection->userId());
|
||||
connect(connection, &NeoChatConnection::requestFailed, this, [](BaseJob *job) {
|
||||
if (dynamic_cast<DownloadFileJob *>(job) && job->jsonData()["errcode"_ls].toString() == "M_TOO_LARGE"_ls) {
|
||||
RoomManager::instance().warning(i18n("File too large to download."), i18n("Contact your matrix server administrator for support."));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
NeoChatConfig::self()->setActiveConnection(QString());
|
||||
}
|
||||
NeoChatConfig::self()->save();
|
||||
Q_EMIT activeConnectionChanged();
|
||||
}
|
||||
|
||||
void Controller::saveWindowGeometry()
|
||||
{
|
||||
WindowController::instance().saveGeometry();
|
||||
}
|
||||
|
||||
void Controller::forceRefreshTextDocument(QQuickTextDocument *textDocument, QQuickItem *item)
|
||||
{
|
||||
// HACK: Workaround bug QTBUG 93281
|
||||
@@ -381,10 +314,18 @@ void Controller::listenForNotifications()
|
||||
#ifdef HAVE_KUNIFIEDPUSH
|
||||
auto connector = new KUnifiedPush::Connector(QStringLiteral("org.kde.neochat"));
|
||||
|
||||
connect(connector, &KUnifiedPush::Connector::messageReceived, [](const QByteArray &data) {
|
||||
auto timer = new QTimer();
|
||||
connect(timer, &QTimer::timeout, qGuiApp, &QGuiApplication::quit);
|
||||
|
||||
connect(connector, &KUnifiedPush::Connector::messageReceived, [timer](const QByteArray &data) {
|
||||
NotificationsManager::instance().postPushNotification(data);
|
||||
timer->stop();
|
||||
});
|
||||
|
||||
// Wait five seconds to see if we received any messages or this happened to be an erroneous activation.
|
||||
// Otherwise, messageReceived is never activated, and this daemon could stick around forever.
|
||||
timer->start(5000);
|
||||
|
||||
connector->registerClient(i18n("Receiving push notifications"));
|
||||
#endif
|
||||
}
|
||||
@@ -439,3 +380,13 @@ void Controller::setTestMode(bool test)
|
||||
{
|
||||
testMode = test;
|
||||
}
|
||||
|
||||
void Controller::removeConnection(const QString &userId)
|
||||
{
|
||||
if (m_connectionsLoading.contains(userId) && m_connectionsLoading[userId]) {
|
||||
auto connection = m_connectionsLoading[userId];
|
||||
m_accountsLoading.removeAll(userId);
|
||||
Q_EMIT accountsLoadingChanged();
|
||||
SettingsGroup("Accounts"_ls).remove(userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,23 +9,15 @@
|
||||
|
||||
#include "neochatconnection.h"
|
||||
#include <Quotient/accountregistry.h>
|
||||
#include <Quotient/jobs/basejob.h>
|
||||
#include <Quotient/settings.h>
|
||||
|
||||
#ifdef HAVE_KUNIFIEDPUSH
|
||||
#include <kunifiedpush/connector.h>
|
||||
#endif
|
||||
|
||||
class NeoChatRoom;
|
||||
class TrayIcon;
|
||||
class QQuickTextDocument;
|
||||
|
||||
namespace Quotient
|
||||
{
|
||||
class Room;
|
||||
class User;
|
||||
}
|
||||
|
||||
namespace QKeychain
|
||||
{
|
||||
class ReadPasswordJob;
|
||||
@@ -65,16 +57,6 @@ class Controller : public QObject
|
||||
Q_PROPERTY(QStringList accountsLoading MEMBER m_accountsLoading NOTIFY accountsLoadingChanged)
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the status after an attempt to change the password on an account.
|
||||
*/
|
||||
enum PasswordStatus {
|
||||
Success, /**< The password was successfully changed. */
|
||||
Wrong, /**< The current password entered was wrong. */
|
||||
Other, /**< An unknown problem occurred. */
|
||||
};
|
||||
Q_ENUM(PasswordStatus)
|
||||
|
||||
static Controller &instance();
|
||||
static Controller *create(QQmlEngine *engine, QJSEngine *)
|
||||
{
|
||||
@@ -128,6 +110,8 @@ public:
|
||||
|
||||
static void setTestMode(bool testMode);
|
||||
|
||||
Q_INVOKABLE void removeConnection(const QString &userId);
|
||||
|
||||
private:
|
||||
explicit Controller(QObject *parent = nullptr);
|
||||
|
||||
@@ -138,34 +122,20 @@ private:
|
||||
|
||||
void loadSettings();
|
||||
void saveSettings() const;
|
||||
QMap<Quotient::Room *, int> m_notificationCounts;
|
||||
|
||||
Quotient::AccountRegistry m_accountRegistry;
|
||||
QStringList m_accountsLoading;
|
||||
QMap<QString, QPointer<Quotient::Connection>> m_connectionsLoading;
|
||||
QString m_endpoint;
|
||||
|
||||
private Q_SLOTS:
|
||||
void invokeLogin();
|
||||
void toggleWindow();
|
||||
void setQuitOnLastWindowClosed();
|
||||
|
||||
Q_SIGNALS:
|
||||
/// Error occurred because of user inputs
|
||||
void errorOccured(const QString &error);
|
||||
|
||||
/// Error occurred because of server or bug in NeoChat
|
||||
void globalErrorOccured(QString error, QString detail);
|
||||
void syncDone();
|
||||
void errorOccured(const QString &error, const QString &detail);
|
||||
void connectionAdded(NeoChatConnection *connection);
|
||||
void connectionDropped(NeoChatConnection *connection);
|
||||
void initiated();
|
||||
void quitOnLastWindowClosedChanged();
|
||||
void unreadCountChanged();
|
||||
void activeConnectionChanged();
|
||||
void passwordStatus(Controller::PasswordStatus status);
|
||||
void userConsentRequired(QUrl url);
|
||||
void accountsLoadingChanged();
|
||||
|
||||
public Q_SLOTS:
|
||||
void saveWindowGeometry();
|
||||
};
|
||||
|
||||
186
src/enums/pushrule.h
Normal file
186
src/enums/pushrule.h
Normal file
@@ -0,0 +1,186 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QQmlEngine>
|
||||
|
||||
/**
|
||||
* @class PushRuleKind
|
||||
*
|
||||
* A class with the Kind enum for push notifications and helper functions.
|
||||
*
|
||||
* The kind relates to the kinds of push rule defined in the matrix spec, see
|
||||
* https://spec.matrix.org/v1.7/client-server-api/#push-rules for full details.
|
||||
*/
|
||||
class PushRuleKind : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("")
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the different kinds of push rule.
|
||||
*/
|
||||
enum Kind {
|
||||
Override = 0, /**< The highest priority rules. */
|
||||
Content, /**< These configure behaviour for messages that match certain patterns. */
|
||||
Room, /**< These rules change the behaviour of all messages for a given room. */
|
||||
Sender, /**< These rules configure notification behaviour for messages from a specific Matrix user ID. */
|
||||
Underride, /**< These are identical to override rules, but have a lower priority than content, room and sender rules. */
|
||||
};
|
||||
Q_ENUM(Kind)
|
||||
|
||||
/**
|
||||
* @brief Translate the Kind enum value to a human readable string.
|
||||
*
|
||||
* @sa Kind
|
||||
*/
|
||||
static QString kindString(Kind kind)
|
||||
{
|
||||
switch (kind) {
|
||||
case Kind::Override:
|
||||
return QLatin1String("override");
|
||||
case Kind::Content:
|
||||
return QLatin1String("content");
|
||||
case Kind::Room:
|
||||
return QLatin1String("room");
|
||||
case Kind::Sender:
|
||||
return QLatin1String("sender");
|
||||
case Kind::Underride:
|
||||
return QLatin1String("underride");
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @class PushRuleAction
|
||||
*
|
||||
* A class with the Action enum for push notifications.
|
||||
*
|
||||
* The action relates to the actions of push rule defined in the matrix spec, see
|
||||
* https://spec.matrix.org/v1.7/client-server-api/#push-rules for full details.
|
||||
*/
|
||||
class PushRuleAction : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("")
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the global push notification actions.
|
||||
*/
|
||||
enum Action {
|
||||
Unknown = 0, /**< The action has not yet been obtained from the server. */
|
||||
Off, /**< No push notifications are to be sent. */
|
||||
On, /**< Push notifications are on. */
|
||||
Noisy, /**< Push notifications are on, also trigger a notification sound. */
|
||||
Highlight, /**< Push notifications are on, also the event should be highlighted in chat. */
|
||||
NoisyHighlight, /**< Push notifications are on, also trigger a notification sound and highlight in chat. */
|
||||
};
|
||||
Q_ENUM(Action)
|
||||
};
|
||||
|
||||
/**
|
||||
* @class PushNotificationState
|
||||
*
|
||||
* A class with the State enum for room push notification state.
|
||||
*
|
||||
* The state define whether the room adheres to the global push rule states for the
|
||||
* account or is overridden for a room.
|
||||
*
|
||||
* @note This is different to the PushRuleAction which defines the type of notification
|
||||
* for an individual rule.
|
||||
*
|
||||
* @sa PushRuleAction
|
||||
*/
|
||||
class PushNotificationState : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("")
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Describes the push notification state for the room.
|
||||
*/
|
||||
enum State {
|
||||
Unknown, /**< The state has not yet been obtained from the server. */
|
||||
Default, /**< The room follows the globally configured rules for the local user. */
|
||||
Mute, /**< No notifications for messages in the room. */
|
||||
MentionKeyword, /**< Notifications only for local user mentions and keywords. */
|
||||
All, /**< Notifications for all messages. */
|
||||
};
|
||||
Q_ENUM(State)
|
||||
};
|
||||
|
||||
/**
|
||||
* @class PushRuleSection
|
||||
*
|
||||
* A class with the Section enum for push notifications and helper functions.
|
||||
*
|
||||
* @note This is different from the PushRuleKind and instead is used for sorting
|
||||
* in the settings page which is not necessarily by Kind.
|
||||
*
|
||||
* @sa PushRuleKind
|
||||
*/
|
||||
class PushRuleSection : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("")
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the sections to sort push rules into.
|
||||
*/
|
||||
enum Section {
|
||||
Master = 0, /**< The master push rule */
|
||||
Room, /**< Push rules relating to all rooms. */
|
||||
Mentions, /**< Push rules relating to user mentions. */
|
||||
Keywords, /**< Global Keyword push rules. */
|
||||
RoomKeywords, /**< Keyword push rules that only apply to a specific room. */
|
||||
Invites, /**< Push rules relating to invites. */
|
||||
Unknown, /**< New default push rules that have not been added to the model yet. */
|
||||
/**
|
||||
* @brief Push rules that should never be shown.
|
||||
*
|
||||
* There are numerous rules that get set that shouldn't be shown in the general
|
||||
* list e.g. The array of rules used to override global settings in individual
|
||||
* rooms.
|
||||
*
|
||||
* This is specifically different to unknown which are just new default push
|
||||
* rule that haven't been added to the model yet.
|
||||
*/
|
||||
Undefined,
|
||||
};
|
||||
Q_ENUM(Section)
|
||||
|
||||
/**
|
||||
* @brief Translate the Section enum value to a human readable string.
|
||||
*
|
||||
* @sa Section
|
||||
*/
|
||||
static QString sectionString(Section section)
|
||||
{
|
||||
switch (section) {
|
||||
case Section::Master:
|
||||
return QLatin1String("Master");
|
||||
case Section::Room:
|
||||
return QLatin1String("Room Notifications");
|
||||
case Section::Mentions:
|
||||
return QLatin1String("@Mentions");
|
||||
case Section::Keywords:
|
||||
return QLatin1String("Keywords");
|
||||
case Section::Invites:
|
||||
return QLatin1String("Invites");
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -181,7 +181,7 @@ QString EventHandler::singleLineAuthorDisplayname(bool isPending) const
|
||||
}
|
||||
|
||||
const auto author = isPending ? m_room->localUser() : m_room->user(m_event->senderId());
|
||||
auto displayName = m_room->htmlSafeMemberName(author->id());
|
||||
auto displayName = m_room->safeMemberName(author->id());
|
||||
displayName.replace(QStringLiteral("<br>\n"), QStringLiteral(" "));
|
||||
displayName.replace(QStringLiteral("<br>"), QStringLiteral(" "));
|
||||
displayName.replace(QStringLiteral("<br />\n"), QStringLiteral(" "));
|
||||
@@ -355,7 +355,7 @@ QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat f
|
||||
if (e.repeatsState()) {
|
||||
auto text = i18n("reinvited %1 to the room", subjectName);
|
||||
if (!e.reason().isEmpty()) {
|
||||
text += i18nc("Optional reason for an invitation", ": %1") + e.reason().toHtmlEscaped();
|
||||
text += i18nc("Optional reason for an invitation", ": %1") + (prettyPrint ? e.reason().toHtmlEscaped() : e.reason());
|
||||
}
|
||||
return text;
|
||||
}
|
||||
@@ -379,7 +379,9 @@ QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat f
|
||||
if (!e.newDisplayName()) {
|
||||
text = i18nc("their refers to a singular user", "cleared their display name");
|
||||
} else {
|
||||
text = i18nc("their refers to a singular user", "changed their display name to %1", e.newDisplayName()->toHtmlEscaped());
|
||||
text = i18nc("their refers to a singular user",
|
||||
"changed their display name to %1",
|
||||
prettyPrint ? e.newDisplayName()->toHtmlEscaped() : *e.newDisplayName());
|
||||
}
|
||||
}
|
||||
if (e.isAvatarUpdate()) {
|
||||
@@ -415,7 +417,7 @@ QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat f
|
||||
if (e.reason().isEmpty()) {
|
||||
return i18n("banned %1 from the room", subjectName);
|
||||
} else {
|
||||
return i18n("banned %1 from the room: %2", subjectName, e.reason().toHtmlEscaped());
|
||||
return i18n("banned %1 from the room: %2", subjectName, prettyPrint ? e.reason().toHtmlEscaped() : e.reason());
|
||||
}
|
||||
} else {
|
||||
return i18n("self-banned from the room");
|
||||
@@ -431,8 +433,8 @@ QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat f
|
||||
[](const RoomCanonicalAliasEvent &e) {
|
||||
return (e.alias().isEmpty()) ? i18n("cleared the room main alias") : i18n("set the room main alias to: %1", e.alias());
|
||||
},
|
||||
[](const RoomNameEvent &e) {
|
||||
return (e.name().isEmpty()) ? i18n("cleared the room name") : i18n("set the room name to: %1", e.name().toHtmlEscaped());
|
||||
[prettyPrint](const RoomNameEvent &e) {
|
||||
return (e.name().isEmpty()) ? i18n("cleared the room name") : i18n("set the room name to: %1", prettyPrint ? e.name().toHtmlEscaped() : e.name());
|
||||
},
|
||||
[prettyPrint, stripNewlines](const RoomTopicEvent &e) {
|
||||
return (e.topic().isEmpty()) ? i18n("cleared the topic")
|
||||
@@ -447,14 +449,15 @@ QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat f
|
||||
[](const EncryptionEvent &) {
|
||||
return i18n("activated End-to-End Encryption");
|
||||
},
|
||||
[](const RoomCreateEvent &e) {
|
||||
return e.isUpgrade() ? i18n("upgraded the room to version %1", e.version().isEmpty() ? "1"_ls : e.version().toHtmlEscaped())
|
||||
: i18n("created the room, version %1", e.version().isEmpty() ? "1"_ls : e.version().toHtmlEscaped());
|
||||
[prettyPrint](const RoomCreateEvent &e) {
|
||||
return e.isUpgrade()
|
||||
? i18n("upgraded the room to version %1", e.version().isEmpty() ? "1"_ls : (prettyPrint ? e.version().toHtmlEscaped() : e.version()))
|
||||
: i18n("created the room, version %1", e.version().isEmpty() ? "1"_ls : (prettyPrint ? e.version().toHtmlEscaped() : e.version()));
|
||||
},
|
||||
[](const RoomPowerLevelsEvent &) {
|
||||
return i18nc("'power level' means permission level", "changed the power levels for this room");
|
||||
},
|
||||
[](const StateEvent &e) {
|
||||
[prettyPrint](const StateEvent &e) {
|
||||
if (e.matrixType() == QLatin1String("m.room.server_acl")) {
|
||||
return i18n("changed the server access control lists for this room");
|
||||
}
|
||||
@@ -471,7 +474,7 @@ QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat f
|
||||
return e.contentJson()["description"_ls].toString();
|
||||
}
|
||||
return e.stateKey().isEmpty() ? i18n("updated %1 state", e.matrixType())
|
||||
: i18n("updated %1 state for %2", e.matrixType(), e.stateKey().toHtmlEscaped());
|
||||
: i18n("updated %1 state for %2", e.matrixType(), prettyPrint ? e.stateKey().toHtmlEscaped() : e.stateKey());
|
||||
},
|
||||
[](const PollStartEvent &e) {
|
||||
return e.question();
|
||||
@@ -645,6 +648,15 @@ QString EventHandler::getGenericBody() const
|
||||
i18n("Unknown event"));
|
||||
}
|
||||
|
||||
QString EventHandler::subtitleText() const
|
||||
{
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "subtitleText called with m_event set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
return singleLineAuthorDisplayname() + (m_event->isStateEvent() ? QLatin1String(" ") : QLatin1String(": ")) + getPlainBody(true);
|
||||
}
|
||||
|
||||
QVariantMap EventHandler::getMediaInfo() const
|
||||
{
|
||||
if (m_room == nullptr) {
|
||||
@@ -765,97 +777,6 @@ QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo
|
||||
return mediaInfo;
|
||||
}
|
||||
|
||||
QSharedPointer<LinkPreviewer> EventHandler::getLinkPreviewer() const
|
||||
{
|
||||
if (m_room == nullptr) {
|
||||
qCWarning(EventHandling) << "getLinkPreviewer called with m_room set to nullptr.";
|
||||
return nullptr;
|
||||
}
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getLinkPreviewer called with m_event set to nullptr.";
|
||||
return nullptr;
|
||||
}
|
||||
if (!m_event->is<RoomMessageEvent>()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QString text;
|
||||
auto event = eventCast<const RoomMessageEvent>(m_event);
|
||||
if (event->hasTextContent()) {
|
||||
auto textContent = static_cast<const EventContent::TextContent *>(event->content());
|
||||
if (textContent) {
|
||||
text = textContent->body;
|
||||
} else {
|
||||
text = event->plainBody();
|
||||
}
|
||||
} else {
|
||||
text = event->plainBody();
|
||||
}
|
||||
TextHandler textHandler;
|
||||
textHandler.setData(text);
|
||||
|
||||
QList<QUrl> links = textHandler.getLinkPreviews();
|
||||
if (links.size() > 0) {
|
||||
return QSharedPointer<LinkPreviewer>(new LinkPreviewer(nullptr, m_room, links.size() > 0 ? links[0] : QUrl()));
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
QSharedPointer<ReactionModel> EventHandler::getReactions() const
|
||||
{
|
||||
if (m_room == nullptr) {
|
||||
qCWarning(EventHandling) << "getReactions called with m_room set to nullptr.";
|
||||
return nullptr;
|
||||
}
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getReactions called with m_event set to nullptr.";
|
||||
return nullptr;
|
||||
}
|
||||
if (!m_event->is<RoomMessageEvent>()) {
|
||||
qCWarning(EventHandling) << "getReactions called with on a non-message event.";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto eventId = m_event->id();
|
||||
const auto &annotations = m_room->relatedEvents(eventId, EventRelation::AnnotationType);
|
||||
if (annotations.isEmpty()) {
|
||||
return nullptr;
|
||||
};
|
||||
|
||||
QMap<QString, QList<User *>> reactions = {};
|
||||
for (const auto &a : annotations) {
|
||||
if (a->isRedacted()) { // Just in case?
|
||||
continue;
|
||||
}
|
||||
if (const auto &e = eventCast<const ReactionEvent>(a)) {
|
||||
reactions[e->key()].append(m_room->user(e->senderId()));
|
||||
}
|
||||
}
|
||||
|
||||
if (reactions.isEmpty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QList<ReactionModel::Reaction> res;
|
||||
auto i = reactions.constBegin();
|
||||
while (i != reactions.constEnd()) {
|
||||
QVariantList authors;
|
||||
for (const auto &author : i.value()) {
|
||||
authors.append(m_room->getUser(author));
|
||||
}
|
||||
|
||||
res.append(ReactionModel::Reaction{i.key(), authors});
|
||||
++i;
|
||||
}
|
||||
|
||||
if (res.size() > 0) {
|
||||
return QSharedPointer<ReactionModel>(new ReactionModel(nullptr, res, m_room->localUser()));
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool EventHandler::hasReply() const
|
||||
{
|
||||
if (m_event == nullptr) {
|
||||
|
||||
@@ -206,6 +206,14 @@ public:
|
||||
*/
|
||||
QString getGenericBody() const;
|
||||
|
||||
/**
|
||||
* @brief Output a string for the event to be used as a RoomList subtitle.
|
||||
*
|
||||
* The output includes the username followed by the plain message, all with no
|
||||
* line breaks.
|
||||
*/
|
||||
QString subtitleText() const;
|
||||
|
||||
/**
|
||||
* @brief Return the media info for the event.
|
||||
*
|
||||
@@ -223,25 +231,6 @@ public:
|
||||
*/
|
||||
QVariantMap getMediaInfo() const;
|
||||
|
||||
/**
|
||||
* @brief Return a LinkPreviewer object for the event.
|
||||
*
|
||||
* A nullptr will be returned for any event that doesn't have any links so the
|
||||
* return should be null checked and an empty LinkPreviewer provided if null.
|
||||
*
|
||||
* @sa LinkPreviewer
|
||||
*/
|
||||
QSharedPointer<LinkPreviewer> getLinkPreviewer() const;
|
||||
|
||||
/**
|
||||
* @brief Return a ReactionModel object for the event.
|
||||
*
|
||||
* A nullptr will be returned for any event that doesn't have any links so the
|
||||
* return should be null checked and an empty QVariantList (or other suitable
|
||||
* empty mode) provided if null.
|
||||
*/
|
||||
QSharedPointer<ReactionModel> getReactions() const;
|
||||
|
||||
/**
|
||||
* @brief Whether the event is a reply to another in the timeline.
|
||||
*/
|
||||
|
||||
@@ -3,25 +3,37 @@
|
||||
|
||||
#include "linkpreviewer.h"
|
||||
|
||||
#include "controller.h"
|
||||
|
||||
#include <Quotient/connection.h>
|
||||
#include <Quotient/csapi/content-repo.h>
|
||||
#include <Quotient/events/roommessageevent.h>
|
||||
|
||||
#include "neochatconfig.h"
|
||||
#include "neochatroom.h"
|
||||
#include "utils.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
LinkPreviewer::LinkPreviewer(QObject *parent, const NeoChatRoom *room, const QUrl &url)
|
||||
: QObject(parent)
|
||||
LinkPreviewer::LinkPreviewer(const NeoChatRoom *room, const Quotient::RoomMessageEvent *event)
|
||||
: QObject(nullptr)
|
||||
, m_currentRoom(room)
|
||||
, m_event(event)
|
||||
, m_loaded(false)
|
||||
, m_url(url)
|
||||
, m_url(linkPreview(event))
|
||||
{
|
||||
loadUrlPreview();
|
||||
if (m_currentRoom) {
|
||||
connect(this, &LinkPreviewer::urlChanged, this, &LinkPreviewer::emptyChanged);
|
||||
|
||||
if (m_event != nullptr && m_currentRoom != nullptr) {
|
||||
loadUrlPreview();
|
||||
connect(m_currentRoom, &NeoChatRoom::urlPreviewEnabledChanged, this, &LinkPreviewer::loadUrlPreview);
|
||||
// Make sure that we react to edits
|
||||
connect(m_currentRoom, &NeoChatRoom::replacedEvent, this, [this](const Quotient::RoomEvent *newEvent) {
|
||||
if (m_event->id() == newEvent->id()) {
|
||||
m_event = eventCast<const Quotient::RoomMessageEvent>(newEvent);
|
||||
m_url = linkPreview(m_event);
|
||||
Q_EMIT urlChanged();
|
||||
loadUrlPreview();
|
||||
}
|
||||
});
|
||||
}
|
||||
connect(NeoChatConfig::self(), &NeoChatConfig::ShowLinkPreviewChanged, this, &LinkPreviewer::loadUrlPreview);
|
||||
}
|
||||
@@ -51,15 +63,6 @@ QUrl LinkPreviewer::url() const
|
||||
return m_url;
|
||||
}
|
||||
|
||||
void LinkPreviewer::setUrl(QUrl url)
|
||||
{
|
||||
if (url != m_url) {
|
||||
m_url = url;
|
||||
urlChanged();
|
||||
loadUrlPreview();
|
||||
}
|
||||
}
|
||||
|
||||
void LinkPreviewer::loadUrlPreview()
|
||||
{
|
||||
if (!m_currentRoom || !NeoChatConfig::showLinkPreview() || !m_currentRoom->urlPreviewEnabled()) {
|
||||
@@ -98,4 +101,38 @@ bool LinkPreviewer::empty() const
|
||||
return m_url.isEmpty();
|
||||
}
|
||||
|
||||
QUrl LinkPreviewer::linkPreview(const Quotient::RoomMessageEvent *event)
|
||||
{
|
||||
if (event == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QString text;
|
||||
if (event->hasTextContent()) {
|
||||
auto textContent = static_cast<const Quotient::EventContent::TextContent *>(event->content());
|
||||
if (textContent) {
|
||||
text = textContent->body;
|
||||
} else {
|
||||
text = event->plainBody();
|
||||
}
|
||||
} else {
|
||||
text = event->plainBody();
|
||||
}
|
||||
|
||||
auto data = text.remove(TextRegex::removeRichReply);
|
||||
auto linksMatch = TextRegex::url.globalMatch(data);
|
||||
while (linksMatch.hasNext()) {
|
||||
auto link = linksMatch.next().captured();
|
||||
if (!link.contains(QStringLiteral("matrix.to"))) {
|
||||
return QUrl(link);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool LinkPreviewer::hasPreviewableLinks(const Quotient::RoomMessageEvent *event)
|
||||
{
|
||||
return !linkPreview(event).isEmpty();
|
||||
}
|
||||
|
||||
#include "moc_linkpreviewer.cpp"
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
#include <QQmlEngine>
|
||||
#include <QUrl>
|
||||
|
||||
namespace Quotient
|
||||
{
|
||||
class RoomMessageEvent;
|
||||
}
|
||||
|
||||
class NeoChatRoom;
|
||||
|
||||
/**
|
||||
@@ -25,7 +30,7 @@ class LinkPreviewer : public QObject
|
||||
/**
|
||||
* @brief The URL to get the preview for.
|
||||
*/
|
||||
Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged)
|
||||
Q_PROPERTY(QUrl url READ url NOTIFY urlChanged)
|
||||
|
||||
/**
|
||||
* @brief Whether the preview information has been loaded.
|
||||
@@ -55,18 +60,25 @@ class LinkPreviewer : public QObject
|
||||
Q_PROPERTY(bool empty READ empty NOTIFY emptyChanged)
|
||||
|
||||
public:
|
||||
explicit LinkPreviewer(QObject *parent = nullptr, const NeoChatRoom *room = nullptr, const QUrl &url = {});
|
||||
explicit LinkPreviewer(const NeoChatRoom *room = nullptr, const Quotient::RoomMessageEvent *event = nullptr);
|
||||
|
||||
[[nodiscard]] QUrl url() const;
|
||||
void setUrl(QUrl);
|
||||
[[nodiscard]] bool loaded() const;
|
||||
[[nodiscard]] QString title() const;
|
||||
[[nodiscard]] QString description() const;
|
||||
[[nodiscard]] QUrl imageSource() const;
|
||||
[[nodiscard]] bool empty() const;
|
||||
|
||||
/**
|
||||
* @brief Whether the given event has at least 1 pre-viewable link.
|
||||
*
|
||||
* A link is only pre-viewable if it is http, https or something starting with www.
|
||||
*/
|
||||
static bool hasPreviewableLinks(const Quotient::RoomMessageEvent *event);
|
||||
|
||||
private:
|
||||
const NeoChatRoom *m_currentRoom = nullptr;
|
||||
const NeoChatRoom *m_currentRoom;
|
||||
const Quotient::RoomMessageEvent *m_event;
|
||||
|
||||
bool m_loaded;
|
||||
QString m_title = QString();
|
||||
@@ -76,6 +88,14 @@ private:
|
||||
|
||||
void loadUrlPreview();
|
||||
|
||||
/**
|
||||
* @brief Return the link to be previewed from the given event.
|
||||
*
|
||||
* This function is designed to give only links that should be previewed so
|
||||
* http, https or something starting with www. The first valid link is returned.
|
||||
*/
|
||||
static QUrl linkPreview(const Quotient::RoomMessageEvent *event);
|
||||
|
||||
Q_SIGNALS:
|
||||
void loadedChanged();
|
||||
void titleChanged();
|
||||
|
||||
@@ -82,7 +82,7 @@ void LoginHelper::init()
|
||||
m_connection = nullptr;
|
||||
});
|
||||
connect(m_connection, &Connection::networkError, this, [this](QString error, const QString &, int, int) {
|
||||
Q_EMIT Controller::instance().globalErrorOccured(i18n("Network Error"), std::move(error));
|
||||
Q_EMIT Controller::instance().errorOccured(i18n("Network Error"), std::move(error));
|
||||
m_isLoggingIn = false;
|
||||
Q_EMIT isLoggingInChanged();
|
||||
});
|
||||
@@ -97,12 +97,11 @@ void LoginHelper::init()
|
||||
});
|
||||
|
||||
connect(m_connection, &Connection::resolveError, this, [](QString error) {
|
||||
Q_EMIT Controller::instance().globalErrorOccured(i18n("Network Error"), std::move(error));
|
||||
Q_EMIT Controller::instance().errorOccured(i18n("Network Error"), std::move(error));
|
||||
});
|
||||
|
||||
connectSingleShot(m_connection, &Connection::syncDone, this, [this]() {
|
||||
Q_EMIT loaded();
|
||||
Q_EMIT Controller::instance().initiated();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ int main(int argc, char *argv[])
|
||||
QStringLiteral(NEOCHAT_VERSION_STRING),
|
||||
i18n("Matrix client"),
|
||||
KAboutLicense::GPL_V3,
|
||||
i18n("© 2018-2020 Black Hat, 2020-2023 KDE Community"));
|
||||
i18n("© 2018-2020 Black Hat, 2020-2024 KDE Community"));
|
||||
about.addAuthor(i18n("Carl Schwan"),
|
||||
i18n("Maintainer"),
|
||||
QStringLiteral("carl@carlschwan.eu"),
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include "controller.h"
|
||||
#include "neochatconnection.h"
|
||||
|
||||
#include <Quotient/connection.h>
|
||||
|
||||
@@ -24,6 +24,10 @@ int AccountEmoticonModel::rowCount(const QModelIndex &index) const
|
||||
|
||||
QVariant AccountEmoticonModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (m_connection == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto &row = index.row();
|
||||
const auto &image = m_images->images[row];
|
||||
if (role == UrlRole) {
|
||||
@@ -92,6 +96,10 @@ void AccountEmoticonModel::setConnection(Connection *connection)
|
||||
|
||||
void AccountEmoticonModel::reloadEmoticons()
|
||||
{
|
||||
if (m_connection == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject json;
|
||||
if (m_connection->hasAccountData("im.ponies.user_emotes"_ls)) {
|
||||
json = m_connection->accountData("im.ponies.user_emotes"_ls)->contentJson();
|
||||
@@ -104,6 +112,10 @@ void AccountEmoticonModel::reloadEmoticons()
|
||||
|
||||
void AccountEmoticonModel::deleteEmoticon(int index)
|
||||
{
|
||||
if (m_connection == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject data;
|
||||
m_images->images.removeAt(index);
|
||||
m_images->fillJson(&data);
|
||||
@@ -112,6 +124,10 @@ void AccountEmoticonModel::deleteEmoticon(int index)
|
||||
|
||||
void AccountEmoticonModel::setEmoticonBody(int index, const QString &text)
|
||||
{
|
||||
if (m_connection == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_images->images[index].body = text;
|
||||
QJsonObject data;
|
||||
m_images->fillJson(&data);
|
||||
@@ -120,6 +136,10 @@ void AccountEmoticonModel::setEmoticonBody(int index, const QString &text)
|
||||
|
||||
void AccountEmoticonModel::setEmoticonShortcode(int index, const QString &shortcode)
|
||||
{
|
||||
if (m_connection == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_images->images[index].shortcode = shortcode;
|
||||
QJsonObject data;
|
||||
m_images->fillJson(&data);
|
||||
@@ -128,6 +148,9 @@ void AccountEmoticonModel::setEmoticonShortcode(int index, const QString &shortc
|
||||
|
||||
void AccountEmoticonModel::setEmoticonImage(int index, const QUrl &source)
|
||||
{
|
||||
if (m_connection == nullptr) {
|
||||
return;
|
||||
}
|
||||
doSetEmoticonImage(index, source);
|
||||
}
|
||||
|
||||
@@ -166,6 +189,9 @@ QCoro::Task<void> AccountEmoticonModel::doAddEmoticon(QUrl source, QString short
|
||||
|
||||
void AccountEmoticonModel::addEmoticon(const QUrl &source, const QString &shortcode, const QString &description, const QString &type)
|
||||
{
|
||||
if (m_connection == nullptr) {
|
||||
return;
|
||||
}
|
||||
doAddEmoticon(source, shortcode, description, type);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "actionsmodel.h"
|
||||
|
||||
#include "chatbarcache.h"
|
||||
#include "controller.h"
|
||||
#include "neochatroom.h"
|
||||
#include "roommanager.h"
|
||||
#include <Quotient/events/roommemberevent.h>
|
||||
|
||||
@@ -6,10 +6,8 @@
|
||||
#include <QImage>
|
||||
#include <QMimeDatabase>
|
||||
|
||||
#include "controller.h"
|
||||
#include "emojimodel.h"
|
||||
|
||||
#include <Quotient/connection.h>
|
||||
#include <Quotient/csapi/account-data.h>
|
||||
#include <Quotient/csapi/content-repo.h>
|
||||
|
||||
@@ -188,22 +186,14 @@ QHash<int, QByteArray> CustomEmojiModel::roleNames() const
|
||||
};
|
||||
}
|
||||
|
||||
QString CustomEmojiModel::preprocessText(const QString &text)
|
||||
QString CustomEmojiModel::preprocessText(QString text)
|
||||
{
|
||||
auto parts = text.split("```"_ls);
|
||||
bool skip = true;
|
||||
for (auto &part : parts) {
|
||||
skip = !skip;
|
||||
if (skip) {
|
||||
continue;
|
||||
}
|
||||
for (const auto &emoji : std::as_const(m_emojis)) {
|
||||
part.replace(
|
||||
emoji.regexp,
|
||||
QStringLiteral(R"(<img data-mx-emoticon="" src="%1" alt="%2" title="%2" height="32" vertical-align="middle" />)").arg(emoji.url, emoji.name));
|
||||
}
|
||||
for (const auto &emoji : std::as_const(m_emojis)) {
|
||||
text.replace(
|
||||
emoji.regexp,
|
||||
QStringLiteral(R"(<img data-mx-emoticon="" src="%1" alt="%2" title="%2" height="32" vertical-align="middle" />)").arg(emoji.url, emoji.name));
|
||||
}
|
||||
return parts.join("```"_ls);
|
||||
return text;
|
||||
}
|
||||
|
||||
QVariantList CustomEmojiModel::filterModel(const QString &filter)
|
||||
|
||||
@@ -6,9 +6,8 @@
|
||||
#include <QAbstractListModel>
|
||||
#include <QQmlEngine>
|
||||
#include <QRegularExpression>
|
||||
#include <memory>
|
||||
|
||||
class NeoChatConnection;
|
||||
#include "neochatconnection.h"
|
||||
|
||||
struct CustomEmoji {
|
||||
QString name; // with :semicolons:
|
||||
@@ -85,7 +84,7 @@ public:
|
||||
/**
|
||||
* @brief Substitute any custom emojis for an image in the input text.
|
||||
*/
|
||||
Q_INVOKABLE QString preprocessText(const QString &it);
|
||||
Q_INVOKABLE QString preprocessText(QString text);
|
||||
|
||||
/**
|
||||
* @brief Return a list of custom emojis where the name contains the filter text.
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
#include "devicesmodel.h"
|
||||
|
||||
#include "controller.h"
|
||||
#include "jobs/neochatdeletedevicejob.h"
|
||||
|
||||
#include <QDateTime>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "messageeventmodel.h"
|
||||
#include "linkpreviewer.h"
|
||||
#include "messageeventmodel_logging.h"
|
||||
|
||||
#include "neochatconfig.h"
|
||||
@@ -20,7 +21,9 @@
|
||||
|
||||
#include "enums/delegatetype.h"
|
||||
#include "eventhandler.h"
|
||||
#include "events/pollevent.h"
|
||||
#include "models/reactionmodel.h"
|
||||
#include "texthandler.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
@@ -34,7 +37,6 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
||||
roles[TimeStringRole] = "timeString";
|
||||
roles[SectionRole] = "section";
|
||||
roles[AuthorRole] = "author";
|
||||
roles[ContentRole] = "content";
|
||||
roles[HighlightRole] = "isHighlighted";
|
||||
roles[SpecialMarksRole] = "marks";
|
||||
roles[ProgressInfoRole] = "progressInfo";
|
||||
@@ -65,12 +67,19 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
||||
roles[LatitudeRole] = "latitude";
|
||||
roles[LongitudeRole] = "longitude";
|
||||
roles[AssetRole] = "asset";
|
||||
roles[PollHandlerRole] = "pollHandler";
|
||||
return roles;
|
||||
}
|
||||
|
||||
MessageEventModel::MessageEventModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
connect(this, &MessageEventModel::modelAboutToBeReset, this, [this]() {
|
||||
resetting = true;
|
||||
});
|
||||
connect(this, &MessageEventModel::modelReset, this, [this]() {
|
||||
resetting = false;
|
||||
});
|
||||
}
|
||||
|
||||
NeoChatRoom *MessageEventModel::room() const
|
||||
@@ -92,6 +101,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
}
|
||||
|
||||
m_currentRoom = room;
|
||||
Q_EMIT roomChanged();
|
||||
if (room) {
|
||||
m_lastReadEventIndex = QPersistentModelIndex(QModelIndex());
|
||||
room->setDisplayed();
|
||||
@@ -100,6 +110,9 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
if (const auto &roomMessageEvent = &*event->viewAs<RoomMessageEvent>()) {
|
||||
createEventObjects(roomMessageEvent);
|
||||
}
|
||||
if (event->event()->is<PollStartEvent>()) {
|
||||
m_currentRoom->createPollHandler(eventCast<const PollStartEvent>(event->event()));
|
||||
}
|
||||
}
|
||||
|
||||
if (m_currentRoom->timelineSize() < 10 && !room->allHistoryLoaded()) {
|
||||
@@ -150,6 +163,9 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (event->is<PollStartEvent>()) {
|
||||
m_currentRoom->createPollHandler(eventCast<const PollStartEvent>(event.get()));
|
||||
}
|
||||
}
|
||||
m_initialized = true;
|
||||
beginInsertRows({}, timelineBaseIndex(), timelineBaseIndex() + int(events.size()) - 1);
|
||||
@@ -159,6 +175,9 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
if (const auto &roomMessageEvent = dynamic_cast<RoomMessageEvent *>(event.get())) {
|
||||
createEventObjects(roomMessageEvent);
|
||||
}
|
||||
if (event->is<PollStartEvent>()) {
|
||||
m_currentRoom->createPollHandler(eventCast<const PollStartEvent>(event.get()));
|
||||
}
|
||||
}
|
||||
if (rowCount() > 0) {
|
||||
rowBelowInserted = rowCount() - 1; // See #312
|
||||
@@ -220,6 +239,10 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
});
|
||||
connect(m_currentRoom, &Room::replacedEvent, this, [this](const RoomEvent *newEvent) {
|
||||
refreshLastUserEvents(refreshEvent(newEvent->id()) - timelineBaseIndex());
|
||||
const RoomMessageEvent *message = eventCast<const RoomMessageEvent>(newEvent);
|
||||
if (message != nullptr) {
|
||||
createEventObjects(message);
|
||||
}
|
||||
});
|
||||
connect(m_currentRoom, &Room::updatedEvent, this, [this](const QString &eventId) {
|
||||
if (eventId.isEmpty()) { // How did we get here?
|
||||
@@ -230,8 +253,11 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
if (const auto &event = dynamic_cast<const RoomMessageEvent *>(&**eventIt)) {
|
||||
createEventObjects(event);
|
||||
}
|
||||
if (eventIt->event()->is<PollStartEvent>()) {
|
||||
m_currentRoom->createPollHandler(eventCast<const PollStartEvent>(eventIt->event()));
|
||||
}
|
||||
}
|
||||
refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole, Qt::DisplayRole});
|
||||
refreshEventRoles(eventId, {Qt::DisplayRole});
|
||||
});
|
||||
connect(m_currentRoom, &Room::changed, this, [this]() {
|
||||
for (auto it = m_currentRoom->messageEvents().rbegin(); it != m_currentRoom->messageEvents().rend(); ++it) {
|
||||
@@ -252,6 +278,12 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
lastReadEventId.clear();
|
||||
}
|
||||
endResetModel();
|
||||
|
||||
// After reset put a read marker in if required.
|
||||
// This is needed when changing back to a room that has already loaded messages.
|
||||
if (room) {
|
||||
moveReadMarker(m_currentRoom->lastFullyReadEventId());
|
||||
}
|
||||
}
|
||||
|
||||
int MessageEventModel::refreshEvent(const QString &eventId)
|
||||
@@ -478,28 +510,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
return eventHandler.getAuthor(isPending);
|
||||
}
|
||||
|
||||
if (role == ContentRole) {
|
||||
if (evt.isRedacted()) {
|
||||
auto reason = evt.redactedBecause()->reason();
|
||||
return (reason.isEmpty()) ? i18n("[REDACTED]") : i18n("[REDACTED: %1]").arg(evt.redactedBecause()->reason());
|
||||
}
|
||||
|
||||
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
|
||||
if (e->msgtype() == Quotient::MessageEventType::Location) {
|
||||
return e->contentJson();
|
||||
}
|
||||
// Cannot use e.contentJson() here because some
|
||||
// EventContent classes inject values into the copy of the
|
||||
// content JSON stored in EventContent::Base
|
||||
return e->hasFileContent() ? QVariant::fromValue(e->content()->originalJson) : QVariant();
|
||||
};
|
||||
|
||||
if (auto e = eventCast<const StickerEvent>(&evt)) {
|
||||
return QVariant::fromValue(e->image().originalJson);
|
||||
}
|
||||
return evt.contentJson();
|
||||
}
|
||||
|
||||
if (role == HighlightRole) {
|
||||
return eventHandler.isHighlighted();
|
||||
}
|
||||
@@ -608,7 +618,10 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
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();
|
||||
|| 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -692,6 +705,10 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
return row < static_cast<int>(m_currentRoom->pendingEvents().size());
|
||||
}
|
||||
|
||||
if (role == PollHandlerRole) {
|
||||
return QVariant::fromValue<PollHandler *>(m_currentRoom->poll(evt.id()));
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -709,19 +726,37 @@ void MessageEventModel::createEventObjects(const Quotient::RoomMessageEvent *eve
|
||||
{
|
||||
auto eventId = event->id();
|
||||
|
||||
EventHandler eventHandler;
|
||||
eventHandler.setRoom(m_currentRoom);
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
if (auto linkPreviewer = eventHandler.getLinkPreviewer()) {
|
||||
m_linkPreviewers[eventId] = linkPreviewer;
|
||||
if (m_linkPreviewers.contains(eventId)) {
|
||||
if (!LinkPreviewer::hasPreviewableLinks(event)) {
|
||||
m_linkPreviewers.remove(eventId);
|
||||
}
|
||||
} else {
|
||||
m_linkPreviewers.remove(eventId);
|
||||
if (LinkPreviewer::hasPreviewableLinks(event)) {
|
||||
m_linkPreviewers[eventId] = QSharedPointer<LinkPreviewer>(new LinkPreviewer(m_currentRoom, event));
|
||||
}
|
||||
}
|
||||
if (auto reactionModel = eventHandler.getReactions()) {
|
||||
m_reactionModels[eventId] = reactionModel;
|
||||
|
||||
// ReactionModel handles updates to add and remove reactions, we only need to
|
||||
// handle adding and removing whole models here.
|
||||
if (m_reactionModels.contains(eventId)) {
|
||||
// If a model already exists but now has no reactions remove it
|
||||
if (m_reactionModels[eventId]->rowCount() <= 0) {
|
||||
m_reactionModels.remove(eventId);
|
||||
if (!resetting) {
|
||||
refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
m_reactionModels.remove(eventId);
|
||||
if (m_currentRoom->relatedEvents(*event, Quotient::EventRelation::AnnotationType).count() > 0) {
|
||||
// If a model doesn't exist and there are reactions add it.
|
||||
auto reactionModel = QSharedPointer<ReactionModel>(new ReactionModel(event, m_currentRoom));
|
||||
if (reactionModel->rowCount() > 0) {
|
||||
m_reactionModels[eventId] = reactionModel;
|
||||
if (!resetting) {
|
||||
refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#include "linkpreviewer.h"
|
||||
#include "neochatroom.h"
|
||||
#include "pollhandler.h"
|
||||
|
||||
class ReactionModel;
|
||||
|
||||
@@ -45,7 +46,6 @@ public:
|
||||
TimeStringRole, /**< The timestamp for when the event was sent as a string (in QLocale::ShortFormat). */
|
||||
SectionRole, /**< The date of the event as a string. */
|
||||
AuthorRole, /**< The author of the event. */
|
||||
ContentRole, /**< The full message content. */
|
||||
HighlightRole, /**< Whether the event should be highlighted. */
|
||||
SpecialMarksRole, /**< Whether the event is hidden or not. */
|
||||
ProgressInfoRole, /**< Progress info when downloading files. */
|
||||
@@ -83,6 +83,7 @@ public:
|
||||
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. */
|
||||
PollHandlerRole, /**< The PollHandler for the event, if any. */
|
||||
LastRole, // Keep this last
|
||||
};
|
||||
Q_ENUM(EventRoles)
|
||||
@@ -130,6 +131,7 @@ private:
|
||||
QString lastReadEventId;
|
||||
QPersistentModelIndex m_lastReadEventIndex;
|
||||
int rowBelowInserted = -1;
|
||||
bool resetting = false;
|
||||
bool movingEvent = false;
|
||||
KFormat m_format;
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ void NotificationsModel::setConnection(NeoChatConnection *connection)
|
||||
}
|
||||
m_connection = connection;
|
||||
Q_EMIT connectionChanged();
|
||||
connect(connection, &Connection::syncDone, this, [=]() {
|
||||
connect(connection, &Connection::syncDone, this, [this]() {
|
||||
loadData();
|
||||
});
|
||||
loadData();
|
||||
|
||||
@@ -11,46 +11,47 @@
|
||||
#include <Quotient/csapi/pushrules.h>
|
||||
#include <Quotient/jobs/basejob.h>
|
||||
|
||||
#include "controller.h"
|
||||
#include "neochatconfig.h"
|
||||
|
||||
#include <KLazyLocalizedString>
|
||||
|
||||
// Alternate name text for default rules.
|
||||
static const QHash<QString, QString> defaultRuleNames = {
|
||||
{QStringLiteral(".m.rule.master"), QStringLiteral("Enable notifications for this account")},
|
||||
{QStringLiteral(".m.rule.room_one_to_one"), QStringLiteral("Messages in one-to-one chats")},
|
||||
{QStringLiteral(".m.rule.encrypted_room_one_to_one"), QStringLiteral("Encrypted messages in one-to-one chats")},
|
||||
{QStringLiteral(".m.rule.message"), QStringLiteral("Messages in group chats")},
|
||||
{QStringLiteral(".m.rule.encrypted"), QStringLiteral("Messages in encrypted group chats")},
|
||||
{QStringLiteral(".m.rule.tombstone"), QStringLiteral("Room upgrade messages")},
|
||||
{QStringLiteral(".m.rule.contains_display_name"), QStringLiteral("Messages containing my display name")},
|
||||
{QStringLiteral(".m.rule.is_user_mention"), QStringLiteral("Messages which mention my Matrix user ID.")},
|
||||
{QStringLiteral(".m.rule.is_room_mention"), QStringLiteral("Messages which mention a room.")},
|
||||
{QStringLiteral(".m.rule.contains_user_name"), QStringLiteral("Messages containing the local part of my Matrix ID.")},
|
||||
{QStringLiteral(".m.rule.roomnotif"), QStringLiteral("Whole room (@room) notifications")},
|
||||
{QStringLiteral(".m.rule.invite_for_me"), QStringLiteral("Invites to a room")},
|
||||
{QStringLiteral(".m.rule.call"), QStringLiteral("Call invitation")},
|
||||
static const QHash<QString, KLazyLocalizedString> defaultRuleNames = {
|
||||
{QStringLiteral(".m.rule.master"), kli18nc("Notification type", "Enable notifications for this account")},
|
||||
{QStringLiteral(".m.rule.room_one_to_one"), kli18nc("Notification type", "Messages in one-to-one chats")},
|
||||
{QStringLiteral(".m.rule.encrypted_room_one_to_one"), kli18nc("Notification type", "Encrypted messages in one-to-one chats")},
|
||||
{QStringLiteral(".m.rule.message"), kli18nc("Notification type", "Messages in group chats")},
|
||||
{QStringLiteral(".m.rule.encrypted"), kli18nc("Notification type", "Messages in encrypted group chats")},
|
||||
{QStringLiteral(".m.rule.tombstone"), kli18nc("Notification type", "Room upgrade messages")},
|
||||
{QStringLiteral(".m.rule.contains_display_name"), kli18nc("Notification type", "Messages containing my display name")},
|
||||
{QStringLiteral(".m.rule.is_user_mention"), kli18nc("Notification type", "Messages which mention my Matrix user ID")},
|
||||
{QStringLiteral(".m.rule.is_room_mention"), kli18nc("Notification type", "Messages which mention a room")},
|
||||
{QStringLiteral(".m.rule.contains_user_name"), kli18nc("Notification type", "Messages containing the local part of my Matrix ID")},
|
||||
{QStringLiteral(".m.rule.roomnotif"), kli18nc("Notification type", "Whole room (@room) notifications")},
|
||||
{QStringLiteral(".m.rule.invite_for_me"), kli18nc("Notification type", "Invites to a room")},
|
||||
{QStringLiteral(".m.rule.call"), kli18nc("Notification type", "Call invitation")},
|
||||
};
|
||||
|
||||
// Sections for default rules.
|
||||
static const QHash<QString, PushNotificationSection::Section> defaultSections = {
|
||||
{QStringLiteral(".m.rule.master"), PushNotificationSection::Master},
|
||||
{QStringLiteral(".m.rule.room_one_to_one"), PushNotificationSection::Room},
|
||||
{QStringLiteral(".m.rule.encrypted_room_one_to_one"), PushNotificationSection::Room},
|
||||
{QStringLiteral(".m.rule.message"), PushNotificationSection::Room},
|
||||
{QStringLiteral(".m.rule.encrypted"), PushNotificationSection::Room},
|
||||
{QStringLiteral(".m.rule.tombstone"), PushNotificationSection::Room},
|
||||
{QStringLiteral(".m.rule.contains_display_name"), PushNotificationSection::Mentions},
|
||||
{QStringLiteral(".m.rule.is_user_mention"), PushNotificationSection::Mentions},
|
||||
{QStringLiteral(".m.rule.is_room_mention"), PushNotificationSection::Mentions},
|
||||
{QStringLiteral(".m.rule.contains_user_name"), PushNotificationSection::Mentions},
|
||||
{QStringLiteral(".m.rule.roomnotif"), PushNotificationSection::Mentions},
|
||||
{QStringLiteral(".m.rule.invite_for_me"), PushNotificationSection::Invites},
|
||||
{QStringLiteral(".m.rule.call"), PushNotificationSection::Undefined}, // TODO: make invites when VOIP added.
|
||||
{QStringLiteral(".m.rule.suppress_notices"), PushNotificationSection::Undefined},
|
||||
{QStringLiteral(".m.rule.member_event"), PushNotificationSection::Undefined},
|
||||
{QStringLiteral(".m.rule.reaction"), PushNotificationSection::Undefined},
|
||||
{QStringLiteral(".m.rule.room.server_acl"), PushNotificationSection::Undefined},
|
||||
{QStringLiteral(".im.vector.jitsi"), PushNotificationSection::Undefined},
|
||||
static const QHash<QString, PushRuleSection::Section> defaultSections = {
|
||||
{QStringLiteral(".m.rule.master"), PushRuleSection::Master},
|
||||
{QStringLiteral(".m.rule.room_one_to_one"), PushRuleSection::Room},
|
||||
{QStringLiteral(".m.rule.encrypted_room_one_to_one"), PushRuleSection::Room},
|
||||
{QStringLiteral(".m.rule.message"), PushRuleSection::Room},
|
||||
{QStringLiteral(".m.rule.encrypted"), PushRuleSection::Room},
|
||||
{QStringLiteral(".m.rule.tombstone"), PushRuleSection::Room},
|
||||
{QStringLiteral(".m.rule.contains_display_name"), PushRuleSection::Mentions},
|
||||
{QStringLiteral(".m.rule.is_user_mention"), PushRuleSection::Mentions},
|
||||
{QStringLiteral(".m.rule.is_room_mention"), PushRuleSection::Mentions},
|
||||
{QStringLiteral(".m.rule.contains_user_name"), PushRuleSection::Mentions},
|
||||
{QStringLiteral(".m.rule.roomnotif"), PushRuleSection::Mentions},
|
||||
{QStringLiteral(".m.rule.invite_for_me"), PushRuleSection::Invites},
|
||||
{QStringLiteral(".m.rule.call"), PushRuleSection::Undefined}, // TODO: make invites when VOIP added.
|
||||
{QStringLiteral(".m.rule.suppress_notices"), PushRuleSection::Undefined},
|
||||
{QStringLiteral(".m.rule.member_event"), PushRuleSection::Undefined},
|
||||
{QStringLiteral(".m.rule.reaction"), PushRuleSection::Undefined},
|
||||
{QStringLiteral(".m.rule.room.server_acl"), PushRuleSection::Undefined},
|
||||
{QStringLiteral(".im.vector.jitsi"), PushRuleSection::Undefined},
|
||||
};
|
||||
|
||||
// Default rules that don't have a highlight option as it would lead to all messages
|
||||
@@ -65,7 +66,7 @@ static const QStringList noHighlight = {
|
||||
PushRuleModel::PushRuleModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
m_defaultKeywordAction = static_cast<PushNotificationAction::Action>(NeoChatConfig::self()->keywordPushRuleDefault());
|
||||
m_defaultKeywordAction = static_cast<PushRuleAction::Action>(NeoChatConfig::self()->keywordPushRuleDefault());
|
||||
}
|
||||
|
||||
void PushRuleModel::updateNotificationRules(const QString &type)
|
||||
@@ -81,11 +82,11 @@ void PushRuleModel::updateNotificationRules(const QString &type)
|
||||
m_rules.clear();
|
||||
|
||||
// Doing this 5 times because PushRuleset is a struct.
|
||||
setRules(ruleData.override, PushNotificationKind::Override);
|
||||
setRules(ruleData.content, PushNotificationKind::Content);
|
||||
setRules(ruleData.room, PushNotificationKind::Room);
|
||||
setRules(ruleData.sender, PushNotificationKind::Sender);
|
||||
setRules(ruleData.underride, PushNotificationKind::Underride);
|
||||
setRules(ruleData.override, PushRuleKind::Override);
|
||||
setRules(ruleData.content, PushRuleKind::Content);
|
||||
setRules(ruleData.room, PushRuleKind::Room);
|
||||
setRules(ruleData.sender, PushRuleKind::Sender);
|
||||
setRules(ruleData.underride, PushRuleKind::Underride);
|
||||
|
||||
Q_EMIT globalNotificationsEnabledChanged();
|
||||
Q_EMIT globalNotificationsSetChanged();
|
||||
@@ -93,7 +94,7 @@ void PushRuleModel::updateNotificationRules(const QString &type)
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void PushRuleModel::setRules(QList<Quotient::PushRule> rules, PushNotificationKind::Kind kind)
|
||||
void PushRuleModel::setRules(QList<Quotient::PushRule> rules, PushRuleKind::Kind kind)
|
||||
{
|
||||
for (const auto &rule : rules) {
|
||||
QString roomId;
|
||||
@@ -126,7 +127,7 @@ int PushRuleModel::getRuleIndex(const QString &ruleId) const
|
||||
return -1;
|
||||
}
|
||||
|
||||
PushNotificationSection::Section PushRuleModel::getSection(Quotient::PushRule rule)
|
||||
PushRuleSection::Section PushRuleModel::getSection(Quotient::PushRule rule)
|
||||
{
|
||||
auto ruleId = rule.ruleId;
|
||||
|
||||
@@ -134,7 +135,7 @@ PushNotificationSection::Section PushRuleModel::getSection(Quotient::PushRule ru
|
||||
return defaultSections.value(ruleId);
|
||||
} else {
|
||||
if (rule.ruleId.startsWith(u'.')) {
|
||||
return PushNotificationSection::Unknown;
|
||||
return PushRuleSection::Unknown;
|
||||
}
|
||||
/**
|
||||
* If the rule name resolves to a matrix id for a room that the user is part
|
||||
@@ -144,7 +145,7 @@ PushNotificationSection::Section PushRuleModel::getSection(Quotient::PushRule ru
|
||||
* Rooms that the user hasn't joined shouldn't have a rule.
|
||||
*/
|
||||
if (m_connection->room(ruleId) != nullptr) {
|
||||
return PushNotificationSection::Undefined;
|
||||
return PushRuleSection::Undefined;
|
||||
}
|
||||
/**
|
||||
* If the rule name resolves to a matrix id for a user it shouldn't appear
|
||||
@@ -158,26 +159,26 @@ PushNotificationSection::Section PushRuleModel::getSection(Quotient::PushRule ru
|
||||
testUserId.prepend(u'@');
|
||||
}
|
||||
if (testUserId.startsWith(u'@') && !Quotient::serverPart(testUserId).isEmpty() && m_connection->user(testUserId) != nullptr) {
|
||||
return PushNotificationSection::Undefined;
|
||||
return PushRuleSection::Undefined;
|
||||
}
|
||||
// If the rule has push conditions and one is a room ID it is a room only keyword.
|
||||
if (!rule.conditions.isEmpty()) {
|
||||
for (auto condition : rule.conditions) {
|
||||
if (condition.key == QStringLiteral("room_id")) {
|
||||
return PushNotificationSection::RoomKeywords;
|
||||
return PushRuleSection::RoomKeywords;
|
||||
}
|
||||
}
|
||||
}
|
||||
return PushNotificationSection::Keywords;
|
||||
return PushRuleSection::Keywords;
|
||||
}
|
||||
}
|
||||
|
||||
PushNotificationAction::Action PushRuleModel::defaultState() const
|
||||
PushRuleAction::Action PushRuleModel::defaultState() const
|
||||
{
|
||||
return m_defaultKeywordAction;
|
||||
}
|
||||
|
||||
void PushRuleModel::setDefaultState(PushNotificationAction::Action defaultState)
|
||||
void PushRuleModel::setDefaultState(PushRuleAction::Action defaultState)
|
||||
{
|
||||
if (defaultState == m_defaultKeywordAction) {
|
||||
return;
|
||||
@@ -220,7 +221,7 @@ QVariant PushRuleModel::data(const QModelIndex &index, int role) const
|
||||
if (role == NameRole) {
|
||||
auto ruleId = m_rules.at(index.row()).id;
|
||||
if (defaultRuleNames.contains(ruleId)) {
|
||||
return defaultRuleNames.value(ruleId);
|
||||
return defaultRuleNames.value(ruleId).toString();
|
||||
} else {
|
||||
return ruleId;
|
||||
}
|
||||
@@ -271,7 +272,7 @@ QHash<int, QByteArray> PushRuleModel::roleNames() const
|
||||
return roles;
|
||||
}
|
||||
|
||||
void PushRuleModel::setPushRuleAction(const QString &id, PushNotificationAction::Action action)
|
||||
void PushRuleModel::setPushRuleAction(const QString &id, PushRuleAction::Action action)
|
||||
{
|
||||
int index = getRuleIndex(id);
|
||||
if (index == -1) {
|
||||
@@ -281,22 +282,22 @@ void PushRuleModel::setPushRuleAction(const QString &id, PushNotificationAction:
|
||||
auto rule = m_rules[index];
|
||||
|
||||
// Override rules need to be disabled when off so that other rules can match the message if they apply.
|
||||
if (action == PushNotificationAction::Off && rule.kind == PushNotificationKind::Override) {
|
||||
setNotificationRuleEnabled(PushNotificationKind::kindString(rule.kind), rule.id, false);
|
||||
} else if (rule.kind == PushNotificationKind::Override) {
|
||||
setNotificationRuleEnabled(PushNotificationKind::kindString(rule.kind), rule.id, true);
|
||||
if (action == PushRuleAction::Off && rule.kind == PushRuleKind::Override) {
|
||||
setNotificationRuleEnabled(PushRuleKind::kindString(rule.kind), rule.id, false);
|
||||
} else if (rule.kind == PushRuleKind::Override) {
|
||||
setNotificationRuleEnabled(PushRuleKind::kindString(rule.kind), rule.id, true);
|
||||
}
|
||||
|
||||
setNotificationRuleActions(PushNotificationKind::kindString(rule.kind), rule.id, action);
|
||||
setNotificationRuleActions(PushRuleKind::kindString(rule.kind), rule.id, action);
|
||||
}
|
||||
|
||||
void PushRuleModel::addKeyword(const QString &keyword, const QString &roomId)
|
||||
{
|
||||
PushNotificationKind::Kind kind = PushNotificationKind::Content;
|
||||
PushRuleKind::Kind kind = PushRuleKind::Content;
|
||||
const QList<QVariant> actions = actionToVariant(m_defaultKeywordAction);
|
||||
QList<Quotient::PushCondition> pushConditions;
|
||||
if (!roomId.isEmpty()) {
|
||||
kind = PushNotificationKind::Override;
|
||||
kind = PushRuleKind::Override;
|
||||
|
||||
Quotient::PushCondition roomCondition;
|
||||
roomCondition.kind = QStringLiteral("event_match");
|
||||
@@ -312,7 +313,7 @@ void PushRuleModel::addKeyword(const QString &keyword, const QString &roomId)
|
||||
}
|
||||
|
||||
auto job = m_connection->callApi<Quotient::SetPushRuleJob>(QLatin1String("global"),
|
||||
PushNotificationKind::kindString(kind),
|
||||
PushRuleKind::kindString(kind),
|
||||
keyword,
|
||||
actions,
|
||||
QString(),
|
||||
@@ -336,7 +337,7 @@ void PushRuleModel::removeKeyword(const QString &keyword)
|
||||
return;
|
||||
}
|
||||
|
||||
auto kind = PushNotificationKind::kindString(m_rules[index].kind);
|
||||
auto kind = PushRuleKind::kindString(m_rules[index].kind);
|
||||
auto job = m_connection->callApi<Quotient::DeletePushRuleJob>(QStringLiteral("global"), kind, m_rules[index].id);
|
||||
connect(job, &Quotient::BaseJob::failure, this, [this, job, index]() {
|
||||
qWarning() << QLatin1String("Unable to remove push rule for keyword %1: ").arg(m_rules[index].id) << job->errorString();
|
||||
@@ -353,7 +354,7 @@ void PushRuleModel::setNotificationRuleEnabled(const QString &kind, const QStrin
|
||||
});
|
||||
}
|
||||
|
||||
void PushRuleModel::setNotificationRuleActions(const QString &kind, const QString &ruleId, PushNotificationAction::Action action)
|
||||
void PushRuleModel::setNotificationRuleActions(const QString &kind, const QString &ruleId, PushRuleAction::Action action)
|
||||
{
|
||||
QList<QVariant> actions;
|
||||
if (ruleId == QStringLiteral(".m.rule.call")) {
|
||||
@@ -365,7 +366,7 @@ void PushRuleModel::setNotificationRuleActions(const QString &kind, const QStrin
|
||||
m_connection->callApi<Quotient::SetPushRuleActionsJob>(QStringLiteral("global"), kind, ruleId, actions);
|
||||
}
|
||||
|
||||
PushNotificationAction::Action PushRuleModel::variantToAction(const QList<QVariant> &actions, bool enabled)
|
||||
PushRuleAction::Action PushRuleModel::variantToAction(const QList<QVariant> &actions, bool enabled)
|
||||
{
|
||||
bool notify = false;
|
||||
bool isNoisy = false;
|
||||
@@ -390,47 +391,47 @@ PushNotificationAction::Action PushRuleModel::variantToAction(const QList<QVaria
|
||||
}
|
||||
|
||||
if (!enabled) {
|
||||
return PushNotificationAction::Off;
|
||||
return PushRuleAction::Off;
|
||||
}
|
||||
|
||||
if (notify) {
|
||||
if (isNoisy && highlightEnabled) {
|
||||
return PushNotificationAction::NoisyHighlight;
|
||||
return PushRuleAction::NoisyHighlight;
|
||||
} else if (isNoisy) {
|
||||
return PushNotificationAction::Noisy;
|
||||
return PushRuleAction::Noisy;
|
||||
} else if (highlightEnabled) {
|
||||
return PushNotificationAction::Highlight;
|
||||
return PushRuleAction::Highlight;
|
||||
} else {
|
||||
return PushNotificationAction::On;
|
||||
return PushRuleAction::On;
|
||||
}
|
||||
} else {
|
||||
return PushNotificationAction::Off;
|
||||
return PushRuleAction::Off;
|
||||
}
|
||||
}
|
||||
|
||||
QList<QVariant> PushRuleModel::actionToVariant(PushNotificationAction::Action action, const QString &sound)
|
||||
QList<QVariant> PushRuleModel::actionToVariant(PushRuleAction::Action action, const QString &sound)
|
||||
{
|
||||
// The caller should never try to set the state to unknown.
|
||||
// It exists only as a default state to diable the settings options until the actual state is retrieved from the server.
|
||||
if (action == PushNotificationAction::Unknown) {
|
||||
if (action == PushRuleAction::Unknown) {
|
||||
Q_ASSERT(false);
|
||||
return QList<QVariant>();
|
||||
}
|
||||
|
||||
QList<QVariant> actions;
|
||||
|
||||
if (action != PushNotificationAction::Off) {
|
||||
if (action != PushRuleAction::Off) {
|
||||
actions.append(QStringLiteral("notify"));
|
||||
} else {
|
||||
actions.append(QStringLiteral("dont_notify"));
|
||||
}
|
||||
if (action == PushNotificationAction::Noisy || action == PushNotificationAction::NoisyHighlight) {
|
||||
if (action == PushRuleAction::Noisy || action == PushRuleAction::NoisyHighlight) {
|
||||
QJsonObject soundTweak;
|
||||
soundTweak.insert(QStringLiteral("set_tweak"), QStringLiteral("sound"));
|
||||
soundTweak.insert(QStringLiteral("value"), sound);
|
||||
actions.append(soundTweak);
|
||||
}
|
||||
if (action == PushNotificationAction::Highlight || action == PushNotificationAction::NoisyHighlight) {
|
||||
if (action == PushRuleAction::Highlight || action == PushRuleAction::NoisyHighlight) {
|
||||
QJsonObject highlightTweak;
|
||||
highlightTweak.insert(QStringLiteral("set_tweak"), QStringLiteral("highlight"));
|
||||
actions.append(highlightTweak);
|
||||
|
||||
@@ -8,124 +8,8 @@
|
||||
|
||||
#include <Quotient/csapi/definitions/push_rule.h>
|
||||
|
||||
#include "notificationsmanager.h"
|
||||
|
||||
/**
|
||||
* @class PushNotificationKind
|
||||
*
|
||||
* A class with the Kind enum for push notifications and helper functions.
|
||||
*
|
||||
* The kind relates to the kinds of push rule definied in the matrix spec, see
|
||||
* https://spec.matrix.org/v1.7/client-server-api/#push-rules for full details.
|
||||
*/
|
||||
class PushNotificationKind : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("")
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the different kinds of push rule.
|
||||
*/
|
||||
enum Kind {
|
||||
Override = 0, /**< The highest priority rules. */
|
||||
Content, /**< These configure behaviour for messages that match certain patterns. */
|
||||
Room, /**< These rules change the behaviour of all messages for a given room. */
|
||||
Sender, /**< These rules configure notification behaviour for messages from a specific Matrix user ID. */
|
||||
Underride, /**< These are identical to override rules, but have a lower priority than content, room and sender rules. */
|
||||
};
|
||||
Q_ENUM(Kind)
|
||||
|
||||
/**
|
||||
* @brief Translate the Kind enum value to a human readable string.
|
||||
*
|
||||
* @sa Kind
|
||||
*/
|
||||
static QString kindString(Kind kind)
|
||||
{
|
||||
switch (kind) {
|
||||
case Kind::Override:
|
||||
return QLatin1String("override");
|
||||
case Kind::Content:
|
||||
return QLatin1String("content");
|
||||
case Kind::Room:
|
||||
return QLatin1String("room");
|
||||
case Kind::Sender:
|
||||
return QLatin1String("sender");
|
||||
case Kind::Underride:
|
||||
return QLatin1String("underride");
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @class PushNotificationSection
|
||||
*
|
||||
* A class with the Section enum for push notifications and helper functions.
|
||||
*
|
||||
* @note This is different from the PushNotificationKind and instead is used for sorting
|
||||
* in the settings page which is not necessarily by Kind.
|
||||
*
|
||||
* @sa PushNotificationKind
|
||||
*/
|
||||
class PushNotificationSection : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("")
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the sections to sort push rules into.
|
||||
*/
|
||||
enum Section {
|
||||
Master = 0, /**< The master push rule */
|
||||
Room, /**< Push rules relating to all rooms. */
|
||||
Mentions, /**< Push rules relating to user mentions. */
|
||||
Keywords, /**< Global Keyword push rules. */
|
||||
RoomKeywords, /**< Keyword push rules that only apply to a specific room. */
|
||||
Invites, /**< Push rules relating to invites. */
|
||||
Unknown, /**< New default push rules that have not been added to the model yet. */
|
||||
/**
|
||||
* @brief Push rules that should never be shown.
|
||||
*
|
||||
* There are numerous rules that get set that shouldn't be shown in the general
|
||||
* list e.g. The array of rules used to override global settings in individual
|
||||
* rooms.
|
||||
*
|
||||
* This is specifically different to unknown which are just new default push
|
||||
* rule that haven't been added to the model yet.
|
||||
*/
|
||||
Undefined,
|
||||
};
|
||||
Q_ENUM(Section)
|
||||
|
||||
/**
|
||||
* @brief Translate the Section enum value to a human readable string.
|
||||
*
|
||||
* @sa Section
|
||||
*/
|
||||
static QString sectionString(Section section)
|
||||
{
|
||||
switch (section) {
|
||||
case Section::Master:
|
||||
return QLatin1String("Master");
|
||||
case Section::Room:
|
||||
return QLatin1String("Room Notifications");
|
||||
case Section::Mentions:
|
||||
return QLatin1String("@Mentions");
|
||||
case Section::Keywords:
|
||||
return QLatin1String("Keywords");
|
||||
case Section::Invites:
|
||||
return QLatin1String("Invites");
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
};
|
||||
};
|
||||
#include "enums/pushrule.h"
|
||||
#include "neochatconnection.h"
|
||||
|
||||
/**
|
||||
* @class PushRuleModel
|
||||
@@ -140,7 +24,7 @@ class PushRuleModel : public QAbstractListModel
|
||||
/**
|
||||
* @brief The default state for any newly created keyword rule.
|
||||
*/
|
||||
Q_PROPERTY(PushNotificationAction::Action defaultState READ defaultState WRITE setDefaultState NOTIFY defaultStateChanged)
|
||||
Q_PROPERTY(PushRuleAction::Action defaultState READ defaultState WRITE setDefaultState NOTIFY defaultStateChanged)
|
||||
|
||||
/**
|
||||
* @brief The global notification state.
|
||||
@@ -153,7 +37,7 @@ class PushRuleModel : public QAbstractListModel
|
||||
/**
|
||||
* @brief Whether the global notification state has been retrieved from the server.
|
||||
*
|
||||
* @sa globalNotificationsEnabled, PushNotificationAction::Action
|
||||
* @sa globalNotificationsEnabled, PushRuleAction::Action
|
||||
*/
|
||||
Q_PROPERTY(bool globalNotificationsSet READ globalNotificationsSet NOTIFY globalNotificationsSetChanged)
|
||||
|
||||
@@ -162,9 +46,9 @@ class PushRuleModel : public QAbstractListModel
|
||||
public:
|
||||
struct Rule {
|
||||
QString id;
|
||||
PushNotificationKind::Kind kind;
|
||||
PushNotificationAction::Action action;
|
||||
PushNotificationSection::Section section;
|
||||
PushRuleKind::Kind kind;
|
||||
PushRuleAction::Action action;
|
||||
PushRuleSection::Section section;
|
||||
bool enabled;
|
||||
QString roomId;
|
||||
};
|
||||
@@ -176,7 +60,7 @@ public:
|
||||
NameRole = Qt::DisplayRole, /**< The push rule name. */
|
||||
IdRole, /**< The push rule ID. */
|
||||
KindRole, /**< The kind of notification rule; override, content, etc. */
|
||||
ActionRole, /**< The PushNotificationAction for the rule. */
|
||||
ActionRole, /**< The PushRuleAction for the rule. */
|
||||
HighlightableRole, /**< Whether the rule can have a highlight action. */
|
||||
DeletableRole, /**< Whether the rule can be deleted the rule. */
|
||||
SectionRole, /**< The section to sort into in the settings page. */
|
||||
@@ -186,8 +70,8 @@ public:
|
||||
|
||||
explicit PushRuleModel(QObject *parent = nullptr);
|
||||
|
||||
[[nodiscard]] PushNotificationAction::Action defaultState() const;
|
||||
void setDefaultState(PushNotificationAction::Action defaultState);
|
||||
[[nodiscard]] PushRuleAction::Action defaultState() const;
|
||||
void setDefaultState(PushRuleAction::Action defaultState);
|
||||
|
||||
[[nodiscard]] bool globalNotificationsEnabled() const;
|
||||
void setGlobalNotificationsEnabled(bool enabled);
|
||||
@@ -215,7 +99,7 @@ public:
|
||||
*/
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
Q_INVOKABLE void setPushRuleAction(const QString &id, PushNotificationAction::Action action);
|
||||
Q_INVOKABLE void setPushRuleAction(const QString &id, PushRuleAction::Action action);
|
||||
|
||||
/**
|
||||
* @brief Add a new keyword to the model.
|
||||
@@ -240,18 +124,18 @@ private Q_SLOTS:
|
||||
void updateNotificationRules(const QString &type);
|
||||
|
||||
private:
|
||||
PushNotificationAction::Action m_defaultKeywordAction;
|
||||
PushRuleAction::Action m_defaultKeywordAction;
|
||||
QList<Rule> m_rules;
|
||||
NeoChatConnection *m_connection;
|
||||
|
||||
void setRules(QList<Quotient::PushRule> rules, PushNotificationKind::Kind kind);
|
||||
void setRules(QList<Quotient::PushRule> rules, PushRuleKind::Kind kind);
|
||||
|
||||
int getRuleIndex(const QString &ruleId) const;
|
||||
PushNotificationSection::Section getSection(Quotient::PushRule rule);
|
||||
PushRuleSection::Section getSection(Quotient::PushRule rule);
|
||||
|
||||
void setNotificationRuleEnabled(const QString &kind, const QString &ruleId, bool enabled);
|
||||
void setNotificationRuleActions(const QString &kind, const QString &ruleId, PushNotificationAction::Action action);
|
||||
PushNotificationAction::Action variantToAction(const QList<QVariant> &actions, bool enabled);
|
||||
QList<QVariant> actionToVariant(PushNotificationAction::Action action, const QString &sound = QStringLiteral("default"));
|
||||
void setNotificationRuleActions(const QString &kind, const QString &ruleId, PushRuleAction::Action action);
|
||||
PushRuleAction::Action variantToAction(const QList<QVariant> &actions, bool enabled);
|
||||
QList<QVariant> actionToVariant(PushRuleAction::Action action, const QString &sound = QStringLiteral("default"));
|
||||
};
|
||||
Q_DECLARE_METATYPE(PushRuleModel *)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include "reactionmodel.h"
|
||||
#include "neochatroom.h"
|
||||
|
||||
#include <QDebug>
|
||||
#ifdef HAVE_ICU
|
||||
@@ -15,11 +16,20 @@
|
||||
|
||||
#include <Quotient/user.h>
|
||||
|
||||
ReactionModel::ReactionModel(QObject *parent, QList<Reaction> reactions, Quotient::User *localUser)
|
||||
: QAbstractListModel(parent)
|
||||
, m_localUser(localUser)
|
||||
ReactionModel::ReactionModel(const Quotient::RoomMessageEvent *event, const NeoChatRoom *room)
|
||||
: QAbstractListModel(nullptr)
|
||||
, m_room(room)
|
||||
, m_event(event)
|
||||
{
|
||||
setReactions(reactions);
|
||||
if (m_event != nullptr && m_room != nullptr) {
|
||||
connect(m_room, &NeoChatRoom::updatedEvent, this, [this](const QString &eventId) {
|
||||
if (m_event && m_event->id() == eventId) {
|
||||
updateReactions();
|
||||
}
|
||||
});
|
||||
|
||||
updateReactions();
|
||||
}
|
||||
}
|
||||
|
||||
QVariant ReactionModel::data(const QModelIndex &index, int role) const
|
||||
@@ -35,38 +45,11 @@ QVariant ReactionModel::data(const QModelIndex &index, int role) const
|
||||
|
||||
const auto &reaction = m_reactions.at(index.row());
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
const auto reactionText = isEmoji(reaction.reaction)
|
||||
? QStringLiteral("<span style=\"font-family: 'emoji';\">") + reaction.reaction + QStringLiteral("</span>")
|
||||
: reaction.reaction;
|
||||
|
||||
if (role == TextContentRole) {
|
||||
if (reaction.authors.count() > 1) {
|
||||
return QStringLiteral("%1 %2").arg(reactionText, QString::number(reaction.authors.count()));
|
||||
return QStringLiteral("%1 %2").arg(reactionText(reaction.reaction), QString::number(reaction.authors.count()));
|
||||
} else {
|
||||
return reactionText;
|
||||
return reactionText(reaction.reaction);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +80,7 @@ QVariant ReactionModel::data(const QModelIndex &index, int role) const
|
||||
"%2 reacted with %3",
|
||||
reaction.authors.count(),
|
||||
text,
|
||||
reactionText);
|
||||
m_shortcodes.contains(reaction.reaction) ? m_shortcodes[reaction.reaction] : reactionText(reaction.reaction));
|
||||
return text;
|
||||
}
|
||||
|
||||
@@ -107,7 +90,7 @@ QVariant ReactionModel::data(const QModelIndex &index, int role) const
|
||||
|
||||
if (role == HasLocalUser) {
|
||||
for (auto author : reaction.authors) {
|
||||
if (author.toMap()[QStringLiteral("id")] == m_localUser->id()) {
|
||||
if (author.toMap()[QStringLiteral("id")] == m_room->localUser()->id()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -123,11 +106,48 @@ int ReactionModel::rowCount(const QModelIndex &parent) const
|
||||
return m_reactions.count();
|
||||
}
|
||||
|
||||
void ReactionModel::setReactions(QList<Reaction> reactions)
|
||||
void ReactionModel::updateReactions()
|
||||
{
|
||||
beginResetModel();
|
||||
|
||||
m_reactions.clear();
|
||||
m_reactions = reactions;
|
||||
m_shortcodes.clear();
|
||||
|
||||
const auto &annotations = m_room->relatedEvents(*m_event, Quotient::EventRelation::AnnotationType);
|
||||
if (annotations.isEmpty()) {
|
||||
endResetModel();
|
||||
return;
|
||||
};
|
||||
|
||||
QMap<QString, QList<Quotient::User *>> reactions = {};
|
||||
for (const auto &a : annotations) {
|
||||
if (a->isRedacted()) { // Just in case?
|
||||
continue;
|
||||
}
|
||||
if (const auto &e = eventCast<const Quotient::ReactionEvent>(a)) {
|
||||
reactions[e->key()].append(m_room->user(e->senderId()));
|
||||
if (e->contentJson()[QStringLiteral("shortcode")].toString().length()) {
|
||||
m_shortcodes[e->key()] = e->contentJson()[QStringLiteral("shortcode")].toString().toHtmlEscaped();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (reactions.isEmpty()) {
|
||||
endResetModel();
|
||||
return;
|
||||
}
|
||||
|
||||
auto i = reactions.constBegin();
|
||||
while (i != reactions.constEnd()) {
|
||||
QVariantList authors;
|
||||
for (const auto &author : i.value()) {
|
||||
authors.append(m_room->getUser(author));
|
||||
}
|
||||
|
||||
m_reactions.append(ReactionModel::Reaction{i.key(), authors});
|
||||
++i;
|
||||
}
|
||||
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
@@ -142,4 +162,42 @@ QHash<int, QByteArray> ReactionModel::roleNames() const
|
||||
};
|
||||
}
|
||||
|
||||
QString ReactionModel::reactionText(QString text) const
|
||||
{
|
||||
text = text.toHtmlEscaped();
|
||||
if (text.startsWith(QStringLiteral("mxc://"))) {
|
||||
static QFont font;
|
||||
static int size = font.pixelSize();
|
||||
if (size == -1) {
|
||||
size = font.pointSizeF() * 1.333;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
#include "moc_reactionmodel.cpp"
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "neochatroom.h"
|
||||
#include <QAbstractListModel>
|
||||
#include <QQmlEngine>
|
||||
#include <Quotient/events/reactionevent.h>
|
||||
|
||||
namespace Quotient
|
||||
{
|
||||
@@ -20,6 +22,7 @@ class ReactionModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("")
|
||||
|
||||
public:
|
||||
/**
|
||||
@@ -41,7 +44,7 @@ public:
|
||||
HasLocalUser, /**< Whether the local user is in the list of authors. */
|
||||
};
|
||||
|
||||
explicit ReactionModel(QObject *parent = nullptr, QList<Reaction> reactions = {}, Quotient::User *localUser = nullptr);
|
||||
explicit ReactionModel(const Quotient::RoomMessageEvent *event, const NeoChatRoom *room);
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
@@ -64,14 +67,13 @@ public:
|
||||
*/
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
/**
|
||||
* @brief Set the reactions data in the model.
|
||||
*/
|
||||
void setReactions(QList<Reaction> reactions);
|
||||
|
||||
private:
|
||||
const NeoChatRoom *m_room;
|
||||
const Quotient::RoomMessageEvent *m_event;
|
||||
QList<Reaction> m_reactions;
|
||||
QMap<QString, QString> m_shortcodes;
|
||||
|
||||
Quotient::User *m_localUser;
|
||||
void updateReactions();
|
||||
QString reactionText(QString text) const;
|
||||
};
|
||||
Q_DECLARE_METATYPE(ReactionModel *)
|
||||
|
||||
@@ -3,14 +3,12 @@
|
||||
|
||||
#include "roomlistmodel.h"
|
||||
|
||||
#include "controller.h"
|
||||
#include "eventhandler.h"
|
||||
#include "neochatconfig.h"
|
||||
#include "neochatroom.h"
|
||||
#include "roommanager.h"
|
||||
#include "spacehierarchycache.h"
|
||||
|
||||
#include <Quotient/user.h>
|
||||
|
||||
#include <QDebug>
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
|
||||
#ifndef Q_OS_ANDROID
|
||||
@@ -22,7 +20,6 @@
|
||||
|
||||
#include <KLocalizedString>
|
||||
#include <QGuiApplication>
|
||||
#include <utility>
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
@@ -174,10 +171,10 @@ void RoomListModel::connectRoomSignals(NeoChatRoom *room)
|
||||
refresh(room);
|
||||
});
|
||||
connect(room, &Room::addedMessages, this, [this, room] {
|
||||
refresh(room, {LastEventRole, SubtitleTextRole, LastActiveTimeRole});
|
||||
refresh(room, {SubtitleTextRole, LastActiveTimeRole});
|
||||
});
|
||||
connect(room, &Room::pendingEventMerged, this, [this, room] {
|
||||
refresh(room, {LastEventRole, SubtitleTextRole});
|
||||
refresh(room, {SubtitleTextRole});
|
||||
});
|
||||
connect(room, &Room::unreadStatsChanged, this, &RoomListModel::refreshNotificationCount);
|
||||
connect(room, &Room::highlightCountChanged, this, &RoomListModel::refreshHighlightCount);
|
||||
@@ -332,12 +329,6 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
|
||||
if (role == HighlightCountRole) {
|
||||
return room->highlightCount();
|
||||
}
|
||||
if (role == LastEventRole) {
|
||||
if (room->lastEventIsSpoiler()) {
|
||||
return QString();
|
||||
}
|
||||
return room->lastEventToString();
|
||||
}
|
||||
if (role == LastActiveTimeRole) {
|
||||
return room->lastActiveTime();
|
||||
}
|
||||
@@ -354,7 +345,13 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
|
||||
return m_categoryVisibility.value(data(index, CategoryRole).toInt(), true);
|
||||
}
|
||||
if (role == SubtitleTextRole) {
|
||||
return room->lastEventToString(Qt::PlainText, true);
|
||||
if (room->lastEvent() == nullptr || room->lastEventIsSpoiler()) {
|
||||
return QString();
|
||||
}
|
||||
EventHandler eventHandler;
|
||||
eventHandler.setRoom(room);
|
||||
eventHandler.setEvent(room->lastEvent());
|
||||
return eventHandler.subtitleText();
|
||||
}
|
||||
if (role == AvatarImageRole) {
|
||||
return room->avatar(128);
|
||||
@@ -396,7 +393,6 @@ QHash<int, QByteArray> RoomListModel::roleNames() const
|
||||
roles[CategoryRole] = "category";
|
||||
roles[NotificationCountRole] = "notificationCount";
|
||||
roles[HighlightCountRole] = "highlightCount";
|
||||
roles[LastEventRole] = "lastEvent";
|
||||
roles[LastActiveTimeRole] = "lastActiveTime";
|
||||
roles[JoinStateRole] = "joinState";
|
||||
roles[CurrentRoomRole] = "currentRoom";
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Quotient/events/roomevent.h>
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QQmlEngine>
|
||||
|
||||
@@ -69,7 +67,6 @@ public:
|
||||
CategoryRole, /**< The room category, e.g favourite. */
|
||||
NotificationCountRole, /**< The number of notifications in the room. */
|
||||
HighlightCountRole, /**< The number of highlighted messages in the room. */
|
||||
LastEventRole, /**< Text for the last event in the room. */
|
||||
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. */
|
||||
|
||||
@@ -136,6 +136,10 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
|
||||
return eventHandler.isHighlighted();
|
||||
case EventIdRole:
|
||||
return eventHandler.getId();
|
||||
case IsThreadedRole:
|
||||
return eventHandler.isThreaded();
|
||||
case ThreadRootRole:
|
||||
return eventHandler.threadRoot();
|
||||
}
|
||||
return DelegateType::Message;
|
||||
}
|
||||
@@ -181,6 +185,8 @@ QHash<int, QByteArray> SearchModel::roleNames() const
|
||||
{MimeTypeRole, "mimeType"},
|
||||
{ShowLinkPreviewRole, "showLinkPreview"},
|
||||
{LinkPreviewRole, "linkPreview"},
|
||||
{IsThreadedRole, "isThreaded"},
|
||||
{ThreadRootRole, "threadRoot"},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user