Compare commits
258 Commits
work/fuf/d
...
work/carl/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29cc585b06 | ||
|
|
cbab810a2e | ||
|
|
e78dfaec34 | ||
|
|
3cfa773820 | ||
|
|
94c0e8b6cd | ||
|
|
35b1f24cb7 | ||
|
|
de3072125e | ||
|
|
f7d2ffac66 | ||
|
|
ff0990bb7c | ||
|
|
8285961c42 | ||
|
|
cd39d5b129 | ||
|
|
83b3fefbf5 | ||
|
|
3718bd716a | ||
|
|
a18257ee17 | ||
|
|
def46d90a8 | ||
|
|
8afd7b2892 | ||
|
|
deb11367cb | ||
|
|
ffd1b06a82 | ||
|
|
2597f0aec9 | ||
|
|
33ca72efd9 | ||
|
|
2fbf659eb4 | ||
|
|
763198b2c3 | ||
|
|
50551541ed | ||
|
|
323cd4962e | ||
|
|
b2f592afeb | ||
|
|
f7f98c43e2 | ||
|
|
06134f96eb | ||
|
|
bb62849a19 | ||
|
|
d05385caed | ||
|
|
209ca747b5 | ||
|
|
2c6ab498ac | ||
|
|
597633f824 | ||
|
|
65da416b8e | ||
|
|
c528929b30 | ||
|
|
8650ce1755 | ||
|
|
8936b23da9 | ||
|
|
cf72fbea2e | ||
|
|
0d5929b4bc | ||
|
|
d43cc9a044 | ||
|
|
682e3967ba | ||
|
|
ceebee3a56 | ||
|
|
52ab052aef | ||
|
|
e6a2b2d125 | ||
|
|
92dadc2cc9 | ||
|
|
6ca9380a51 | ||
|
|
51d41ab5bf | ||
|
|
f1047fa474 | ||
|
|
011b11681f | ||
|
|
8baeb236bc | ||
|
|
b9d173d0b5 | ||
|
|
3790955c56 | ||
|
|
199772a013 | ||
|
|
54cc3ac761 | ||
|
|
1675c14c92 | ||
|
|
56f5ef2611 | ||
|
|
7ba63eb680 | ||
|
|
ddd690f6d5 | ||
|
|
d9dc5f48bc | ||
|
|
a3b40a5e6d | ||
|
|
be71a4349b | ||
|
|
ede2707767 | ||
|
|
5871903529 | ||
|
|
87745c360d | ||
|
|
40d2eb5aba | ||
|
|
cfd9f36a97 | ||
|
|
48f7e06c01 | ||
|
|
43ecd188f1 | ||
|
|
063056bebd | ||
|
|
dfba655527 | ||
|
|
23178b6224 | ||
|
|
a9a6e31b8c | ||
|
|
0afd74d96a | ||
|
|
c5501f2b5d | ||
|
|
68602fc177 | ||
|
|
8ce5a57499 | ||
|
|
bf3c5708c7 | ||
|
|
8359c1fe90 | ||
|
|
774121ea8c | ||
|
|
370bbf8a4b | ||
|
|
4fd36fb221 | ||
|
|
167349b9d2 | ||
|
|
296aa9ddad | ||
|
|
94c1b67ec9 | ||
|
|
a9965acdd3 | ||
|
|
35313271a3 | ||
|
|
e40179f641 | ||
|
|
dacf3ff8a0 | ||
|
|
0274d70f71 | ||
|
|
0c19052f32 | ||
|
|
a3423e5724 | ||
|
|
e09f31f891 | ||
|
|
e05e0f5489 | ||
|
|
002961f638 | ||
|
|
e28419ef88 | ||
|
|
0e4df58fc9 | ||
|
|
21a55885b3 | ||
|
|
6a8ac8f6e5 | ||
|
|
9fcfad7058 | ||
|
|
cfcc1756dd | ||
|
|
e15bec2295 | ||
|
|
9071cf827f | ||
|
|
c7deaaba84 | ||
|
|
414035de8b | ||
|
|
442612d31d | ||
|
|
59164d3bb2 | ||
|
|
cc60dde62d | ||
|
|
cbed8148a3 | ||
|
|
50e8b9ebf6 | ||
|
|
f5ad2ad162 | ||
|
|
d11d6c74b3 | ||
|
|
63ed69a5d4 | ||
|
|
a8aa775575 | ||
|
|
6305359b3c | ||
|
|
7e859364af | ||
|
|
8abd0db012 | ||
|
|
dbc10685f0 | ||
|
|
96582a12bc | ||
|
|
0ac61854bd | ||
|
|
68298f038d | ||
|
|
405fd5841a | ||
|
|
5da9bba844 | ||
|
|
84373712ef | ||
|
|
50f4f96341 | ||
|
|
1b27b1a4e2 | ||
|
|
33811a4c49 | ||
|
|
43715486e5 | ||
|
|
5c72bd4ab7 | ||
|
|
092f1be99b | ||
|
|
550d55cb1a | ||
|
|
e9edb61245 | ||
|
|
89aae665b1 | ||
|
|
1e3c3dd1f4 | ||
|
|
6f4d2c0216 | ||
|
|
803cd2b4e4 | ||
|
|
9f3012061d | ||
|
|
c1604a9c4f | ||
|
|
3c7fcee244 | ||
|
|
d33a50a00d | ||
|
|
ed033a1c5e | ||
|
|
2b961703ae | ||
|
|
0539665779 | ||
|
|
df127b88e6 | ||
|
|
27c4b57f0f | ||
|
|
f875a23e83 | ||
|
|
7f5cfbf21c | ||
|
|
f7c7643c1c | ||
|
|
692edf52f1 | ||
|
|
e48cfaa41f | ||
|
|
ba116460d5 | ||
|
|
c71672dab3 | ||
|
|
932ef72311 | ||
|
|
7e53a2234f | ||
|
|
a0499e5140 | ||
|
|
093ef0a18c | ||
|
|
36bf862ab9 | ||
|
|
6d8c1d0780 | ||
|
|
7fe85066a4 | ||
|
|
1fefa228e6 | ||
|
|
4104e10d95 | ||
|
|
05f3c3ee0a | ||
|
|
0a1c489401 | ||
|
|
e53d63ad8b | ||
|
|
78541b32f0 | ||
|
|
3bd93996c0 | ||
|
|
61968aa475 | ||
|
|
24d0082048 | ||
|
|
2980af11b0 | ||
|
|
91d9406c38 | ||
|
|
280d1e38e2 | ||
|
|
7b520da4b4 | ||
|
|
419b4cea98 | ||
|
|
38824f30ac | ||
|
|
9c4d8ef823 | ||
|
|
fe3bf3a638 | ||
|
|
d678a446e2 | ||
|
|
6df60a39b0 | ||
|
|
6f9fa76ab7 | ||
|
|
6e8b0f001f | ||
|
|
0bfad95d8b | ||
|
|
1b43846196 | ||
|
|
4ea183b139 | ||
|
|
22fd24549b | ||
|
|
af793fb865 | ||
|
|
13414b5834 | ||
|
|
65a1df4a75 | ||
|
|
d15157703a | ||
|
|
2d21330a36 | ||
|
|
fbc4611d02 | ||
|
|
e257865b19 | ||
|
|
018154f555 | ||
|
|
80a5daa1f7 | ||
|
|
bf5ce049d9 | ||
|
|
58213ee3e6 | ||
|
|
2d1862a637 | ||
|
|
67453d9fb8 | ||
|
|
3ab04583ae | ||
|
|
794683b5d2 | ||
|
|
9f7f5409bd | ||
|
|
7d7b9d7ae0 | ||
|
|
c7c892c51c | ||
|
|
97903a7cb0 | ||
|
|
0c8f488ac4 | ||
|
|
88368cefaa | ||
|
|
ab4bb41979 | ||
|
|
c2398b19dc | ||
|
|
4b3dac025f | ||
|
|
9eeff06328 | ||
|
|
657a8291d0 | ||
|
|
bc4431462a | ||
|
|
fdd8a62873 | ||
|
|
c164228894 | ||
|
|
7d418f0883 | ||
|
|
e90aecbead | ||
|
|
221c27fbbe | ||
|
|
1907404124 | ||
|
|
81e953dd8c | ||
|
|
9311052e39 | ||
|
|
648fff20f2 | ||
|
|
980211f36a | ||
|
|
25e0e3fa8e | ||
|
|
403c1ed391 | ||
|
|
c2f76612cf | ||
|
|
2a651b8946 | ||
|
|
00c0d1c276 | ||
|
|
0162d91b60 | ||
|
|
5f41b7b4c1 | ||
|
|
d9691bac7c | ||
|
|
719b916837 | ||
|
|
f9514fe925 | ||
|
|
399b9e6944 | ||
|
|
00cf5c8a5b | ||
|
|
02f26424e2 | ||
|
|
3bd639775a | ||
|
|
163c1ef407 | ||
|
|
9997b1074a | ||
|
|
f52a25717b | ||
|
|
5746c24b73 | ||
|
|
dca1b22521 | ||
|
|
b15f1461d8 | ||
|
|
24fd7a0f5b | ||
|
|
0ac3932303 | ||
|
|
807a9ee779 | ||
|
|
4f32a168cd | ||
|
|
45d2da56c7 | ||
|
|
d794e26d62 | ||
|
|
42cc42642d | ||
|
|
ffd6205bd1 | ||
|
|
9ba6b2f0a5 | ||
|
|
f8d3e25f8e | ||
|
|
75cd77facb | ||
|
|
8546d76a54 | ||
|
|
00b7f68a03 | ||
|
|
b486cb905c | ||
|
|
cdd5c9f484 | ||
|
|
3e6c426397 | ||
|
|
847f41e1ad | ||
|
|
ed5bb270ff | ||
|
|
c8b4da2b96 |
@@ -5,7 +5,7 @@ include:
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/reuse-lint.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android.yml
|
||||
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android-qt6.yml
|
||||
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux-qt6.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows.yml
|
||||
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows-qt6.yml
|
||||
|
||||
@@ -57,10 +57,12 @@ Dependencies:
|
||||
- 'on': ['Linux/Qt6', 'FreeBSD/Qt6']
|
||||
'require':
|
||||
'frameworks/kdbusaddons': '@latest-kf6'
|
||||
'frameworks/kstatusnotifieritem': '@latest-kf6'
|
||||
|
||||
- 'on': ['Linux/Qt6', 'Linux/Qt5']
|
||||
'require':
|
||||
'sdk/selenium-webdriver-at-spi': '@latest-kf6'
|
||||
|
||||
Options:
|
||||
per-test-timeout: 90
|
||||
require-passing-tests-on: [ 'Linux/Qt5', 'FreeBSD', 'Windows' ]
|
||||
|
||||
@@ -27,7 +27,7 @@ set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
set(KDE_COMPILERSETTINGS_LEVEL 5.84)
|
||||
set(KDE_COMPILERSETTINGS_LEVEL 5.105)
|
||||
|
||||
include(FeatureSummary)
|
||||
include(ECMSetupVersion)
|
||||
@@ -77,12 +77,12 @@ ecm_setup_version(${PROJECT_VERSION}
|
||||
VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/neochat-version.h
|
||||
)
|
||||
|
||||
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} NO_MODULE COMPONENTS Core Quick Gui QuickControls2 Multimedia Svg)
|
||||
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} NO_MODULE COMPONENTS Core Quick Gui QuickControls2 Multimedia Svg WebView)
|
||||
set_package_properties(Qt${QT_MAJOR_VERSION} PROPERTIES
|
||||
TYPE REQUIRED
|
||||
PURPOSE "Basic application components"
|
||||
)
|
||||
find_package(KF${QT_MAJOR_VERSION} ${KF_MIN_VERSION} COMPONENTS Kirigami2 I18n Notifications Config CoreAddons Sonnet ItemModels)
|
||||
find_package(KF${QT_MAJOR_VERSION} ${KF_MIN_VERSION} COMPONENTS Kirigami2 I18n Notifications Config CoreAddons Sonnet ItemModels SyntaxHighlighting)
|
||||
set_package_properties(KF${QT_MAJOR_VERSION} PROPERTIES
|
||||
TYPE REQUIRED
|
||||
PURPOSE "Basic application components"
|
||||
@@ -93,6 +93,10 @@ set_package_properties(KF${QT_MAJOR_VERSION}Kirigami2 PROPERTIES
|
||||
)
|
||||
find_package(KF${QT_MAJOR_VERSION}KirigamiAddons 0.7.2 REQUIRED)
|
||||
|
||||
if(QT_MAJOR_VERSION STREQUAL "6")
|
||||
find_package(KF6StatusNotifierItem ${KF_MIN_VERSION} REQUIRED)
|
||||
endif()
|
||||
|
||||
if(ANDROID)
|
||||
find_package(OpenSSL)
|
||||
set_package_properties(OpenSSL PROPERTIES
|
||||
@@ -124,6 +128,11 @@ set_package_properties(Quotient${QUOTIENT_SUFFIX} PROPERTIES
|
||||
PURPOSE "Talk with matrix server"
|
||||
)
|
||||
|
||||
if (NOT TARGET Olm::Olm)
|
||||
message(FATAL_ERROR "NeoChat requires Quotient with the E2EE feature enabled")
|
||||
endif()
|
||||
|
||||
|
||||
find_package(cmark)
|
||||
set_package_properties(cmark PROPERTIES
|
||||
TYPE REQUIRED
|
||||
@@ -135,6 +144,7 @@ set_package_properties(cmark PROPERTIES
|
||||
ecm_find_qmlmodule(org.kde.kquickimageeditor 1.0)
|
||||
ecm_find_qmlmodule(org.kde.kitemmodels 1.0)
|
||||
ecm_find_qmlmodule(org.kde.quickcharts 1.0)
|
||||
ecm_find_qmlmodule(QtLocation ${QTLOCATION_MODULE_QML_VERSION})
|
||||
|
||||
find_package(KQuickImageEditor COMPONENTS)
|
||||
set_package_properties(KQuickImageEditor PROPERTIES
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
A Qt/QML based Matrix client.
|
||||
|
||||
<a href='https://matrix.org'><img src='https://matrix.org/docs/projects/images/made-for-matrix.png' alt='Made for Matrix' height=64 target=_blank /></a>
|
||||
<a href='https://matrix.org'><img src='https://matrix.org/docs/legacy/made-for-matrix.png' alt='Made for Matrix' height=64 target=_blank /></a>
|
||||
<a href='https://flathub.org/apps/details/org.kde.neochat'><img width='190px' alt='Download on Flathub' src='https://flathub.org/assets/badges/flathub-badge-i-en.png'/></a>
|
||||
|
||||
## Introduction
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
android:versionName="${versionName}"
|
||||
android:versionCode="${versionCode}"
|
||||
android:installLocation="auto">
|
||||
<application android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="NeoChat" android:icon="@drawable/neochat">
|
||||
<application android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="NeoChat" android:icon="@drawable/neochat" android:usesCleartextTraffic="true">
|
||||
<activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation"
|
||||
android:name="org.qtproject.qt5.android.bindings.QtActivity"
|
||||
android:label="NeoChat"
|
||||
|
||||
@@ -18,6 +18,6 @@ if(NOT SeleniumWebDriverATSPI_FOUND)
|
||||
endif()
|
||||
|
||||
add_test(
|
||||
NAME logintest
|
||||
COMMAND selenium-webdriver-at-spi-run ${CMAKE_CURRENT_SOURCE_DIR}/logintest.py
|
||||
NAME logintest
|
||||
COMMAND selenium-webdriver-at-spi-run ${CMAKE_CURRENT_SOURCE_DIR}/logintest.py
|
||||
)
|
||||
|
||||
@@ -8,8 +8,10 @@ import unittest
|
||||
from appium import webdriver
|
||||
from appium.webdriver.common.appiumby import AppiumBy
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
import os
|
||||
import time
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
class LoginTest(unittest.TestCase):
|
||||
@classmethod
|
||||
@@ -20,7 +22,7 @@ class LoginTest(unittest.TestCase):
|
||||
self.driver = webdriver.Remote(
|
||||
command_executor='http://127.0.0.1:4723',
|
||||
desired_capabilities=desired_caps)
|
||||
subprocess.Popen(["python","login-server.py"])
|
||||
self.mockServerProcess = subprocess.Popen([sys.executable, os.path.join(os.path.dirname(__file__), "login-server.py")])
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
@@ -31,9 +33,12 @@ class LoginTest(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(self):
|
||||
self.mockServerProcess.terminate()
|
||||
self.driver.quit()
|
||||
|
||||
def test_login(self):
|
||||
self.driver.find_element(by=AppiumBy.NAME, value="Login").click()
|
||||
|
||||
self.driver.find_element(by=AppiumBy.NAME, value="Matrix ID").send_keys("@user:localhost:1234")
|
||||
self.driver.find_element(by=AppiumBy.NAME, value="Continue").click()
|
||||
self.driver.find_element(by=AppiumBy.NAME, value="Password").send_keys("1234")
|
||||
|
||||
@@ -20,3 +20,15 @@ ecm_add_test(
|
||||
LINK_LIBRARIES neochat Qt::Test
|
||||
TEST_NAME delegatesizehelpertest
|
||||
)
|
||||
|
||||
ecm_add_test(
|
||||
mediasizehelpertest.cpp
|
||||
LINK_LIBRARIES neochat Qt::Test
|
||||
TEST_NAME mediasizehelpertest
|
||||
)
|
||||
|
||||
ecm_add_test(
|
||||
eventhandlertest.cpp
|
||||
LINK_LIBRARIES neochat Qt::Test
|
||||
TEST_NAME eventhandlertest
|
||||
)
|
||||
|
||||
729
autotests/eventhandlertest.cpp
Normal file
729
autotests/eventhandlertest.cpp
Normal file
@@ -0,0 +1,729 @@
|
||||
// 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 "eventhandler.h"
|
||||
|
||||
#include <KFormat>
|
||||
|
||||
#include <Quotient/connection.h>
|
||||
#include <Quotient/quotient_common.h>
|
||||
#include <Quotient/syncdata.h>
|
||||
|
||||
#include "enums/delegatetype.h"
|
||||
#include "linkpreviewer.h"
|
||||
#include "models/reactionmodel.h"
|
||||
#include "neochatroom.h"
|
||||
#include "utils.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;
|
||||
EventHandler eventHandler;
|
||||
|
||||
private Q_SLOTS:
|
||||
void initTestCase();
|
||||
|
||||
void eventId();
|
||||
void delegateType_data();
|
||||
void delegateType();
|
||||
void author();
|
||||
void authorDisplayName();
|
||||
void time();
|
||||
void timeString();
|
||||
void highlighted();
|
||||
void hidden();
|
||||
void body();
|
||||
void genericBody_data();
|
||||
void genericBody();
|
||||
void mediaInfo();
|
||||
void linkPreviewer();
|
||||
void reactions();
|
||||
void hasReply();
|
||||
void replyId();
|
||||
void replyDelegateType();
|
||||
void replyAuthor();
|
||||
void replyBody();
|
||||
void replyMediaInfo();
|
||||
void location();
|
||||
void readMarkers();
|
||||
};
|
||||
|
||||
void EventHandlerTest::initTestCase()
|
||||
{
|
||||
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
|
||||
room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join);
|
||||
|
||||
const auto json = QJsonDocument::fromJson(R"EVENT({
|
||||
"account_data": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"tags": {
|
||||
"u.work": {
|
||||
"order": 0.9
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.tag"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"custom_config_key": "custom_config_value"
|
||||
},
|
||||
"type": "org.example.custom.room.config"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ephemeral": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"user_ids": [
|
||||
"@alice:matrix.org",
|
||||
"@bob:example.com"
|
||||
]
|
||||
},
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"type": "m.typing"
|
||||
},
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"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": {
|
||||
"avatar_url": "mxc://kde.org/123456",
|
||||
"displayname": "after",
|
||||
"membership": "join"
|
||||
},
|
||||
"origin_server_ts": 1690651134736,
|
||||
"sender": "@example:example.org",
|
||||
"state_key": "@example:example.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"replaces_state": "$1234567890:example.org",
|
||||
"prev_content": {
|
||||
"avatar_url": "mxc://kde.org/12345",
|
||||
"displayname": "before",
|
||||
"membership": "join"
|
||||
},
|
||||
"prev_sender": "@example:example.orgg",
|
||||
"age": 1234
|
||||
},
|
||||
"event_id": "$143273583553PhrSn:example.org",
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "This is a highlight @bob:kde.org and this is a link https://kde.org",
|
||||
"format": "org.matrix.custom.html",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$1532735824654:example.org",
|
||||
"origin_server_ts": 1532735824654,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1233
|
||||
}
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
{
|
||||
"age": 4926305285,
|
||||
"content": {
|
||||
"body": "video caption",
|
||||
"filename": "video.mp4",
|
||||
"info": {
|
||||
"duration": 10,
|
||||
"h": 1080,
|
||||
"mimetype": "video/mp4",
|
||||
"size": 62650636,
|
||||
"w": 1920,
|
||||
"thumbnail_info": {
|
||||
"h": 450,
|
||||
"mimetype": "image/jpeg",
|
||||
"size": 382249,
|
||||
"w": 800
|
||||
},
|
||||
"thumbnail_url": "mxc://kde.org/2234567"
|
||||
},
|
||||
"msgtype": "m.video",
|
||||
"url": "mxc://kde.org/1234567"
|
||||
},
|
||||
"event_id": "$263456789:example.org",
|
||||
"origin_server_ts": 1685793783330,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 4926305285
|
||||
},
|
||||
"user_id": "@example:example.org"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "> <@example:example.org> This is an example\ntext message\n\nreply",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "<mx-reply><blockquote><a href=\"https://matrix.to/#/!jEsUZKDJdhlrceRyVU:example.org/$153456789:example.org?via=kde.org&via=matrix.org\">In reply to</a> <a href=\"https://matrix.to/#/@example:example.org\">@example:example.org</a><br><b>This is an example<br>text message</b></blockquote></mx-reply>reply",
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
"event_id": "$153456789:example.org"
|
||||
}
|
||||
},
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"origin_server_ts": 1690725965572,
|
||||
"sender": "@alice:matrix.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 98
|
||||
},
|
||||
"event_id": "$154456789:example.org",
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "> <@example:example.org> video caption\n\nreply",
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
"event_id": "$263456789:example.org"
|
||||
}
|
||||
},
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"origin_server_ts": 1690725965573,
|
||||
"sender": "@alice:matrix.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 98
|
||||
},
|
||||
"event_id": "$154456799:example.org",
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
|
||||
},
|
||||
{
|
||||
"age": 96845207,
|
||||
"content": {
|
||||
"body": "Lat: 51.7035, Lon: -1.14394",
|
||||
"geo_uri": "geo:51.7035,-1.14394",
|
||||
"msgtype": "m.location",
|
||||
"org.matrix.msc1767.text": "Lat: 51.7035, Lon: -1.14394",
|
||||
"org.matrix.msc3488.asset": {
|
||||
"type": "m.pin"
|
||||
},
|
||||
"org.matrix.msc3488.location": {
|
||||
"uri": "geo:51.7035,-1.14394"
|
||||
}
|
||||
},
|
||||
"event_id": "$1544567999:example.org",
|
||||
"origin_server_ts": 1690821582876,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 96845207
|
||||
}
|
||||
}
|
||||
],
|
||||
"limited": true,
|
||||
"prev_batch": "t34-23535_0_0"
|
||||
}
|
||||
})EVENT");
|
||||
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, json.object());
|
||||
room->update(std::move(roomData));
|
||||
|
||||
eventHandler.setRoom(room);
|
||||
}
|
||||
|
||||
void EventHandlerTest::eventId()
|
||||
{
|
||||
eventHandler.setEvent(room->messageEvents().at(0).get());
|
||||
|
||||
QCOMPARE(eventHandler.getId(), QStringLiteral("$153456789:example.org"));
|
||||
}
|
||||
|
||||
void EventHandlerTest::delegateType_data()
|
||||
{
|
||||
QTest::addColumn<int>("eventNum");
|
||||
QTest::addColumn<DelegateType::Type>("delegateType");
|
||||
|
||||
QTest::newRow("message") << 0 << DelegateType::Message;
|
||||
QTest::newRow("state") << 1 << DelegateType::State;
|
||||
QTest::newRow("message 2") << 2 << DelegateType::Message;
|
||||
QTest::newRow("reaction") << 3 << DelegateType::Other;
|
||||
QTest::newRow("video") << 4 << DelegateType::Video;
|
||||
QTest::newRow("location") << 7 << DelegateType::Location;
|
||||
}
|
||||
|
||||
void EventHandlerTest::delegateType()
|
||||
{
|
||||
QFETCH(int, eventNum);
|
||||
QFETCH(DelegateType::Type, delegateType);
|
||||
|
||||
eventHandler.setEvent(room->messageEvents().at(eventNum).get());
|
||||
|
||||
QCOMPARE(eventHandler.getDelegateType(), delegateType);
|
||||
}
|
||||
|
||||
void EventHandlerTest::author()
|
||||
{
|
||||
auto event = room->messageEvents().at(0).get();
|
||||
auto author = room->user(event->senderId());
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
auto eventHandlerAuthor = eventHandler.getAuthor();
|
||||
|
||||
QCOMPARE(eventHandlerAuthor["isLocalUser"_ls], author->id() == room->localUser()->id());
|
||||
QCOMPARE(eventHandlerAuthor["id"_ls], author->id());
|
||||
QCOMPARE(eventHandlerAuthor["displayName"_ls], author->displayname(room));
|
||||
QCOMPARE(eventHandlerAuthor["avatarSource"_ls], room->avatarForMember(author));
|
||||
QCOMPARE(eventHandlerAuthor["avatarMediaId"_ls], author->avatarMediaId(room));
|
||||
QCOMPARE(eventHandlerAuthor["color"_ls], Utils::getUserColor(author->hueF()));
|
||||
QCOMPARE(eventHandlerAuthor["object"_ls], QVariant::fromValue(author));
|
||||
}
|
||||
|
||||
void EventHandlerTest::authorDisplayName()
|
||||
{
|
||||
auto event = room->messageEvents().at(1).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getAuthorDisplayName(), QStringLiteral("before"));
|
||||
}
|
||||
|
||||
void EventHandlerTest::time()
|
||||
{
|
||||
auto event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getTime(), QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC));
|
||||
QCOMPARE(eventHandler.getTime(true, QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC)), QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC));
|
||||
}
|
||||
|
||||
void EventHandlerTest::timeString()
|
||||
{
|
||||
auto event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
KFormat format;
|
||||
|
||||
QCOMPARE(eventHandler.getTimeString(false),
|
||||
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC).toLocalTime().time(), QLocale::ShortFormat));
|
||||
QCOMPARE(eventHandler.getTimeString(true),
|
||||
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC).toLocalTime().date(), QLocale::ShortFormat));
|
||||
QCOMPARE(eventHandler.getTimeString(false, QLocale::ShortFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
|
||||
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().time(), QLocale::ShortFormat));
|
||||
QCOMPARE(eventHandler.getTimeString(true, QLocale::ShortFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
|
||||
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().date(), QLocale::ShortFormat));
|
||||
QCOMPARE(eventHandler.getTimeString(false, QLocale::LongFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
|
||||
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().time(), QLocale::LongFormat));
|
||||
QCOMPARE(eventHandler.getTimeString(true, QLocale::LongFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
|
||||
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().date(), QLocale::LongFormat));
|
||||
}
|
||||
|
||||
void EventHandlerTest::highlighted()
|
||||
{
|
||||
auto event = room->messageEvents().at(2).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.isHighlighted(), true);
|
||||
|
||||
event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.isHighlighted(), false);
|
||||
}
|
||||
|
||||
void EventHandlerTest::hidden()
|
||||
{
|
||||
auto event = room->messageEvents().at(3).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.isHidden(), true);
|
||||
|
||||
event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.isHidden(), false);
|
||||
}
|
||||
|
||||
void EventHandlerTest::body()
|
||||
{
|
||||
auto event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getRichBody(), QStringLiteral("<b>This is an example<br>text message</b>"));
|
||||
QCOMPARE(eventHandler.getRichBody(true), QStringLiteral("<b>This is an example text message</b>"));
|
||||
QCOMPARE(eventHandler.getPlainBody(), QStringLiteral("This is an example\ntext message"));
|
||||
QCOMPARE(eventHandler.getPlainBody(true), QStringLiteral("This is an example text message"));
|
||||
}
|
||||
|
||||
void EventHandlerTest::genericBody_data()
|
||||
{
|
||||
QTest::addColumn<int>("eventNum");
|
||||
QTest::addColumn<QString>("output");
|
||||
|
||||
QTest::newRow("message") << 0 << QStringLiteral("sent a message");
|
||||
QTest::newRow("member") << 1 << QStringLiteral("changed their display name and updated their avatar");
|
||||
QTest::newRow("message 2") << 2 << QStringLiteral("sent a message");
|
||||
QTest::newRow("reaction") << 3 << QStringLiteral("Unknown event");
|
||||
QTest::newRow("video") << 4 << QStringLiteral("sent a message");
|
||||
}
|
||||
|
||||
void EventHandlerTest::genericBody()
|
||||
{
|
||||
QFETCH(int, eventNum);
|
||||
QFETCH(QString, output);
|
||||
|
||||
eventHandler.setEvent(room->messageEvents().at(eventNum).get());
|
||||
|
||||
QCOMPARE(eventHandler.getGenericBody(), output);
|
||||
}
|
||||
|
||||
void EventHandlerTest::mediaInfo()
|
||||
{
|
||||
auto event = room->messageEvents().at(4).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
auto mediaInfo = eventHandler.getMediaInfo();
|
||||
auto thumbnailInfo = mediaInfo["tempInfo"_ls].toMap();
|
||||
|
||||
QCOMPARE(mediaInfo["source"_ls], room->makeMediaUrl(event->id(), QUrl("mxc://kde.org/1234567"_ls)));
|
||||
QCOMPARE(mediaInfo["mimeType"_ls], QStringLiteral("video/mp4"));
|
||||
QCOMPARE(mediaInfo["mimeIcon"_ls], QStringLiteral("video-mp4"));
|
||||
QCOMPARE(mediaInfo["size"_ls], 62650636);
|
||||
QCOMPARE(mediaInfo["duration"_ls], 10);
|
||||
QCOMPARE(mediaInfo["width"_ls], 1920);
|
||||
QCOMPARE(mediaInfo["height"_ls], 1080);
|
||||
QCOMPARE(thumbnailInfo["source"_ls], room->makeMediaUrl(event->id(), QUrl("mxc://kde.org/2234567"_ls)));
|
||||
QCOMPARE(thumbnailInfo["mimeType"_ls], QStringLiteral("image/jpeg"));
|
||||
QCOMPARE(thumbnailInfo["mimeIcon"_ls], QStringLiteral("image-jpeg"));
|
||||
QCOMPARE(thumbnailInfo["size"_ls], 382249);
|
||||
QCOMPARE(thumbnailInfo["width"_ls], 800);
|
||||
QCOMPARE(thumbnailInfo["height"_ls], 450);
|
||||
}
|
||||
|
||||
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::reactions()
|
||||
{
|
||||
auto event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getReactions()->rowCount(), 1);
|
||||
}
|
||||
|
||||
void EventHandlerTest::hasReply()
|
||||
{
|
||||
auto event = room->messageEvents().at(5).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.hasReply(), true);
|
||||
|
||||
event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.hasReply(), false);
|
||||
}
|
||||
|
||||
void EventHandlerTest::replyId()
|
||||
{
|
||||
auto event = room->messageEvents().at(5).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getReplyId(), QStringLiteral("$153456789:example.org"));
|
||||
|
||||
event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getReplyId(), QStringLiteral(""));
|
||||
}
|
||||
|
||||
void EventHandlerTest::replyDelegateType()
|
||||
{
|
||||
auto event = room->messageEvents().at(5).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getReplyDelegateType(), DelegateType::Message);
|
||||
|
||||
event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getReplyDelegateType(), DelegateType::Other);
|
||||
}
|
||||
|
||||
void EventHandlerTest::replyAuthor()
|
||||
{
|
||||
auto event = room->messageEvents().at(5).get();
|
||||
auto replyEvent = room->messageEvents().at(0).get();
|
||||
auto replyAuthor = room->user(replyEvent->senderId());
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
auto eventHandlerReplyAuthor = eventHandler.getReplyAuthor();
|
||||
|
||||
QCOMPARE(eventHandlerReplyAuthor["isLocalUser"_ls], replyAuthor->id() == room->localUser()->id());
|
||||
QCOMPARE(eventHandlerReplyAuthor["id"_ls], replyAuthor->id());
|
||||
QCOMPARE(eventHandlerReplyAuthor["displayName"_ls], replyAuthor->displayname(room));
|
||||
QCOMPARE(eventHandlerReplyAuthor["avatarSource"_ls], room->avatarForMember(replyAuthor));
|
||||
QCOMPARE(eventHandlerReplyAuthor["avatarMediaId"_ls], replyAuthor->avatarMediaId(room));
|
||||
QCOMPARE(eventHandlerReplyAuthor["color"_ls], Utils::getUserColor(replyAuthor->hueF()));
|
||||
QCOMPARE(eventHandlerReplyAuthor["object"_ls], QVariant::fromValue(replyAuthor));
|
||||
|
||||
event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getReplyAuthor(), room->getUser(nullptr));
|
||||
}
|
||||
|
||||
void EventHandlerTest::replyBody()
|
||||
{
|
||||
auto event = room->messageEvents().at(5).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getReplyRichBody(), QStringLiteral("<b>This is an example<br>text message</b>"));
|
||||
QCOMPARE(eventHandler.getReplyRichBody(true), QStringLiteral("<b>This is an example text message</b>"));
|
||||
QCOMPARE(eventHandler.getReplyPlainBody(), QStringLiteral("This is an example\ntext message"));
|
||||
QCOMPARE(eventHandler.getReplyPlainBody(true), QStringLiteral("This is an example text message"));
|
||||
}
|
||||
|
||||
void EventHandlerTest::replyMediaInfo()
|
||||
{
|
||||
auto event = room->messageEvents().at(6).get();
|
||||
auto replyEvent = room->messageEvents().at(4).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
auto mediaInfo = eventHandler.getReplyMediaInfo();
|
||||
auto thumbnailInfo = mediaInfo["tempInfo"_ls].toMap();
|
||||
|
||||
QCOMPARE(mediaInfo["source"_ls], room->makeMediaUrl(replyEvent->id(), QUrl("mxc://kde.org/1234567"_ls)));
|
||||
QCOMPARE(mediaInfo["mimeType"_ls], QStringLiteral("video/mp4"));
|
||||
QCOMPARE(mediaInfo["mimeIcon"_ls], QStringLiteral("video-mp4"));
|
||||
QCOMPARE(mediaInfo["size"_ls], 62650636);
|
||||
QCOMPARE(mediaInfo["duration"_ls], 10);
|
||||
QCOMPARE(mediaInfo["width"_ls], 1920);
|
||||
QCOMPARE(mediaInfo["height"_ls], 1080);
|
||||
QCOMPARE(thumbnailInfo["source"_ls], room->makeMediaUrl(replyEvent->id(), QUrl("mxc://kde.org/2234567"_ls)));
|
||||
QCOMPARE(thumbnailInfo["mimeType"_ls], QStringLiteral("image/jpeg"));
|
||||
QCOMPARE(thumbnailInfo["mimeIcon"_ls], QStringLiteral("image-jpeg"));
|
||||
QCOMPARE(thumbnailInfo["size"_ls], 382249);
|
||||
QCOMPARE(thumbnailInfo["width"_ls], 800);
|
||||
QCOMPARE(thumbnailInfo["height"_ls], 450);
|
||||
}
|
||||
|
||||
void EventHandlerTest::location()
|
||||
{
|
||||
auto event = room->messageEvents().at(7).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getLatitude(), QStringLiteral("51.7035").toFloat());
|
||||
QCOMPARE(eventHandler.getLongitude(), QStringLiteral("-1.14394").toFloat());
|
||||
QCOMPARE(eventHandler.getLocationAssetType(), QStringLiteral("m.pin"));
|
||||
}
|
||||
|
||||
void EventHandlerTest::readMarkers()
|
||||
{
|
||||
auto event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.hasReadMarkers(), true);
|
||||
|
||||
auto readMarkers = eventHandler.getReadMarkers();
|
||||
|
||||
QCOMPARE(readMarkers.size(), 1);
|
||||
QCOMPARE(readMarkers[0].toMap()["id"_ls], QStringLiteral("@alice:matrix.org"));
|
||||
|
||||
QCOMPARE(eventHandler.getNumberExcessReadMarkers(), QString());
|
||||
QCOMPARE(eventHandler.getReadMarkersString(), QStringLiteral("1 user: @alice:matrix.org"));
|
||||
|
||||
event = room->messageEvents().at(2).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.hasReadMarkers(), true);
|
||||
|
||||
readMarkers = eventHandler.getReadMarkers();
|
||||
|
||||
QCOMPARE(readMarkers.size(), 5);
|
||||
|
||||
QCOMPARE(eventHandler.getNumberExcessReadMarkers(), QStringLiteral("+ 1"));
|
||||
// There are no guarantees on the order of the users it will be different every time so don't match the whole string.
|
||||
QCOMPARE(eventHandler.getReadMarkersString().startsWith(QStringLiteral("6 users:")), true);
|
||||
}
|
||||
|
||||
QTEST_MAIN(EventHandlerTest)
|
||||
#include "eventhandlertest.moc"
|
||||
72
autotests/mediasizehelpertest.cpp
Normal file
72
autotests/mediasizehelpertest.cpp
Normal file
@@ -0,0 +1,72 @@
|
||||
// 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 <qglobal.h>
|
||||
#include <qtestcase.h>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "mediasizehelper.h"
|
||||
#include "neochatconfig.h"
|
||||
|
||||
class MediaSizeHelperTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private Q_SLOTS:
|
||||
void uninitialized();
|
||||
|
||||
void limits_data();
|
||||
void limits();
|
||||
};
|
||||
|
||||
void MediaSizeHelperTest::uninitialized()
|
||||
{
|
||||
MediaSizeHelper mediasizehelper;
|
||||
QCOMPARE(mediasizehelper.currentSize(), QSize(540, qRound(qreal(NeoChatConfig::self()->mediaMaxWidth()) / qreal(16.0) * qreal(9.0))));
|
||||
}
|
||||
|
||||
void MediaSizeHelperTest::limits_data()
|
||||
{
|
||||
QTest::addColumn<qreal>("mediaWidth");
|
||||
QTest::addColumn<qreal>("mediaHeight");
|
||||
QTest::addColumn<qreal>("contentMaxWidth");
|
||||
QTest::addColumn<qreal>("contentMaxHeight");
|
||||
QTest::addColumn<QSize>("currentSize");
|
||||
|
||||
QTest::newRow("media smaller than content limits") << qreal(200) << qreal(150) << qreal(400) << qreal(900) << QSize(200, 150);
|
||||
QTest::newRow("media smaller than max limits") << qreal(200) << qreal(150) << qreal(-1) << qreal(-1) << QSize(200, 150);
|
||||
|
||||
QTest::newRow("limit by max width") << qreal(600) << qreal(50) << qreal(-1) << qreal(-1) << QSize(540, qRound(qreal(540) / (qreal(600) / qreal(50))));
|
||||
QTest::newRow("limit by max height") << qreal(50) << qreal(600) << qreal(-1) << qreal(-1) << QSize(qRound(qreal(540) * (qreal(50) / qreal(600))), 540);
|
||||
|
||||
QTest::newRow("limit by content width") << qreal(600) << qreal(50) << qreal(300) << qreal(-1) << QSize(300, qRound(qreal(300) / (qreal(600) / qreal(50))));
|
||||
QTest::newRow("limit by content height") << qreal(50) << qreal(600) << qreal(-1) << qreal(300) << QSize(qRound(qreal(300) * (qreal(50) / qreal(600))), 300);
|
||||
|
||||
QTest::newRow("limit by content width tall media")
|
||||
<< qreal(400) << qreal(600) << qreal(100) << qreal(400) << QSize(100, qRound(qreal(100) / (qreal(400) / qreal(600))));
|
||||
QTest::newRow("limit by content height wide media")
|
||||
<< qreal(1000) << qreal(600) << qreal(400) << qreal(100) << QSize(qRound(qreal(100) * (qreal(1000) / qreal(600))), 100);
|
||||
}
|
||||
|
||||
void MediaSizeHelperTest::limits()
|
||||
{
|
||||
QFETCH(qreal, mediaWidth);
|
||||
QFETCH(qreal, mediaHeight);
|
||||
QFETCH(qreal, contentMaxWidth);
|
||||
QFETCH(qreal, contentMaxHeight);
|
||||
QFETCH(QSize, currentSize);
|
||||
|
||||
MediaSizeHelper mediasizehelper;
|
||||
mediasizehelper.setMediaWidth(mediaWidth);
|
||||
mediasizehelper.setMediaHeight(mediaHeight);
|
||||
mediasizehelper.setContentMaxWidth(contentMaxWidth);
|
||||
mediasizehelper.setContentMaxHeight(contentMaxHeight);
|
||||
|
||||
QCOMPARE(mediasizehelper.currentSize(), currentSize);
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(MediaSizeHelperTest)
|
||||
#include "mediasizehelpertest.moc"
|
||||
@@ -10,6 +10,8 @@
|
||||
#include <Quotient/syncdata.h>
|
||||
#include <qnamespace.h>
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
class TestRoom : public NeoChatRoom
|
||||
@@ -361,6 +363,7 @@ void TextHandlerTest::receiveRichInPlainOut_data()
|
||||
|
||||
QTest::newRow("ampersand") << QStringLiteral("a & b") << QStringLiteral("a & b");
|
||||
QTest::newRow("quote") << QStringLiteral(""a and b"") << QStringLiteral("\"a and b\"");
|
||||
QTest::newRow("new line") << QStringLiteral("new<br>line") << QStringLiteral("new\nline");
|
||||
}
|
||||
|
||||
void TextHandlerTest::receiveRichInPlainOut()
|
||||
@@ -555,10 +558,10 @@ void TextHandlerTest::receiveRichPlainUrl()
|
||||
void TextHandlerTest::receiveRichEmote()
|
||||
{
|
||||
auto event = room->messageEvents().at(1).get();
|
||||
auto author = static_cast<NeoChatUser *>(room->user(event->senderId()));
|
||||
auto author = room->user(event->senderId());
|
||||
const QString testInputString = QStringLiteral("This is an emote.");
|
||||
const QString testOutputString = QStringLiteral("* <a href=\"https://matrix.to/#/@example:example.org\" style=\"color:") + author->color().name()
|
||||
+ QStringLiteral("\">@example:example.org</a> This is an emote.");
|
||||
const QString testOutputString = QStringLiteral("* <a href=\"https://matrix.to/#/@example:example.org\" style=\"color:")
|
||||
+ Utils::getUserColor(author->hueF()).name() + QStringLiteral("\">@example:example.org</a> This is an emote.");
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
@@ -602,13 +605,13 @@ 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")});
|
||||
QTest::newRow("richHttps") << QStringLiteral("<a href=\"https://kde.org\">Rich Link</a>") << QList<QUrl>({QUrl("https://kde.org")});
|
||||
QTest::newRow("plainWww") << QStringLiteral("www.example.org") << QList<QUrl>({QUrl("www.example.org")});
|
||||
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"),
|
||||
QUrl("www.example.org"),
|
||||
QUrl("https://kde.org"_ls),
|
||||
QUrl("www.example.org"_ls),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -644,5 +647,5 @@ void TextHandlerTest::linkPreviewsReject()
|
||||
QCOMPARE(testTextHandler.getLinkPreviews(), testOutputLinks);
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(TextHandlerTest)
|
||||
QTEST_MAIN(TextHandlerTest)
|
||||
#include "texthandlertest.moc"
|
||||
|
||||
@@ -46,43 +46,26 @@
|
||||
<name xml:lang="uk">NeoChat</name>
|
||||
<name xml:lang="x-test">xxNeoChatxx</name>
|
||||
<name xml:lang="zh-CN">NeoChat</name>
|
||||
<summary>A client for matrix, the decentralized communication protocol</summary>
|
||||
<summary xml:lang="ar">عميل لماتركس، ميفاق الاتصال اللامركزي</summary>
|
||||
<summary xml:lang="az">Matrix üçün müştəri, mərkəzləşməmiş kommunikasiya protokolu</summary>
|
||||
<summary xml:lang="ca">Un client de Matrix, el protocol de comunicacions descentralitzat</summary>
|
||||
<summary xml:lang="ca-valencia">Un client de Matrix, el protocol de comunicacions descentralitzat</summary>
|
||||
<summary xml:lang="cs">Klient pro decentralizovaný komunikační protokol matrix</summary>
|
||||
<summary xml:lang="de">Ein Programm für Matrix, das dezentrale Kommunikationsprotokoll</summary>
|
||||
<summary xml:lang="el">Ένας πελάτης για το Matrix, το αποκεντρωμένο πρωτόκολλο επικοινωνίας</summary>
|
||||
<summary xml:lang="en-GB">A client for matrix, the decentralised communication protocol</summary>
|
||||
<summary xml:lang="eo">Kliento por matrix, la malcentra komunikprotokolo</summary>
|
||||
<summary xml:lang="es">Un cliente para Matrix, el protocolo de comunicaciones descentralizado</summary>
|
||||
<summary xml:lang="eu">Matrix, deszentralizatutako komunikazio protokolorako bezero bat</summary>
|
||||
<summary xml:lang="fi">Asiakas Matrixille, hajautetulle viestintäyhteyskäytännölle</summary>
|
||||
<summary xml:lang="fr">Un client pour « Matrix », le protocole décentralisé de communications.</summary>
|
||||
<summary xml:lang="gl">Un cliente para Matrix, o protocolo de comunicación descentralizada</summary>
|
||||
<summary xml:lang="hu">Kliens a matrixhoz, a decentralizált kommunikációs protokollhoz</summary>
|
||||
<summary xml:lang="ia">Un cliente per Matrix, le protocollo de communication decentralisate</summary>
|
||||
<summary xml:lang="id">Klien untuk matrix, protokol komunikasi terdesentralisasi</summary>
|
||||
<summary xml:lang="ie">Un cliente de Matrix, li protocol de communication decentralisat</summary>
|
||||
<summary xml:lang="it">Un client per matrix, il protocollo di comunicazione decentralizzato</summary>
|
||||
<summary xml:lang="ka">კლიენტი Matrix-სთვის, დეცენტრალიზებული კომუნიკაციის პროტოკოლისთვის</summary>
|
||||
<summary xml:lang="ko">Matrix, 분산 대화 프로토콜 클라이언트</summary>
|
||||
<summary xml:lang="nl">Een client voor matrix, het gedecentraliseerde communicatieprotocol</summary>
|
||||
<summary xml:lang="nn">Ein klient for Matrix – protokollen for desentralisert kommunikasjon</summary>
|
||||
<summary xml:lang="pa">ਮੈਟਰਿਕਸ, ਸਰਬ-ਸਾਂਝੇ ਸੰਚਾਰ ਪਰੋਟੋਕਾਲ, ਲਈ ਕਲਾਈਂਟ ਹੈ</summary>
|
||||
<summary xml:lang="pl">Program do obsługi matriksa, rozproszonego protokołu porozumiewania się</summary>
|
||||
<summary xml:lang="pt">Um cliente para o Matrix, o protocolo de comunicação descentralizado</summary>
|
||||
<summary xml:lang="pt-BR">Um cliente do Matrix, o protocolo de comunicação descentralizado</summary>
|
||||
<summary xml:lang="ru">Клиент для Matrix — децентрализованного коммуникационного протокола</summary>
|
||||
<summary xml:lang="sk">Klient pre matrix, decentralizovaný komunikačný protokol</summary>
|
||||
<summary xml:lang="sl">Odjemalec za matrix, decentralizirani komunikacijski protokol</summary>
|
||||
<summary xml:lang="sv">En klient för Matrix, det decentraliserade kommunikationsprotokollet</summary>
|
||||
<summary xml:lang="ta">மையமில்லா தகவல் பரிமாற்ற நெறிமுறையான மேட்ரிக்ஸுக்கான செயலி</summary>
|
||||
<summary xml:lang="tr">Merkezi olmayan iletişim protokolü Matrix için bir istemci</summary>
|
||||
<summary xml:lang="uk">Клієнт matrix, децентралізованого протоколу обміну даними</summary>
|
||||
<summary xml:lang="x-test">xxA client for matrix, the decentralized communication protocolxx</summary>
|
||||
<summary xml:lang="zh-CN">分布式通讯协议 Matrix 的客户端</summary>
|
||||
<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="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>
|
||||
<summary xml:lang="fr">Discuter avec vos ami(e)s sur le réseau Matrix</summary>
|
||||
<summary xml:lang="gl">Charle coas súas amizades en Matrix.</summary>
|
||||
<summary xml:lang="it">Conversa con i tuoi contatti su matrix</summary>
|
||||
<summary xml:lang="ka">ესაუბრეთ მეგობრებს Matrix-ზე</summary>
|
||||
<summary xml:lang="ko">Matrix를 사용하여 친구들과 대화하기</summary>
|
||||
<summary xml:lang="nl">Met uw vrienden chatten op matrix</summary>
|
||||
<summary xml:lang="nn">Prat med vennar på Matrix</summary>
|
||||
<summary xml:lang="sl">Klepet z vašimi prijatelji na matrixu</summary>
|
||||
<summary xml:lang="sv">Chatta med dina vänner på Matrix</summary>
|
||||
<summary xml:lang="ta">மேட்ரிக்ஸு மூலம் உங்கள் நண்பர்களிடம் பேசலாம்</summary>
|
||||
<summary xml:lang="tr">Matrix'te arkadaşlarınızla sohbet edin</summary>
|
||||
<summary xml:lang="uk">Спілкуйтеся з вашими друзями у matrix</summary>
|
||||
<summary xml:lang="x-test">xxChat with your friends on matrixxx</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>
|
||||
@@ -92,17 +75,20 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<p xml:lang="en-GB">NeoChat is a client for Matrix, the decentralised communication protocol for instant messaging. It allows you to send text messages, videos and audio files to your family, colleagues and friends. It uses KDE frameworks and most notably Kirigami to provide a convergent experience across multiple platforms.</p>
|
||||
<p xml:lang="eo">NeoChat estas kliento por Matrix, la malcentra komunikoprotokolo por tuja mesaĝado. Ĝi ebligas al vi sendi tekstmesaĝojn, filmetojn kaj sondosierojn al via familio, kolegoj kaj amikoj. Ĝi uzas KDE-framojn kaj precipe Kirigami por disponigi konverĝan sperton tra pluraj platformoj.</p>
|
||||
<p xml:lang="es">NeoChat es un cliente para Matrix, el protocolo de comunicaciones descentralizado para mensajería instantánea. Le permite enviar mensajes de texto, vídeos y archivos de sonido a su familia, compañeros de trabajo y amigos. Usa la infraestructura de KDE y, en particular, Kirigami para proporcionar una experiencia convergente en muchas plataformas.</p>
|
||||
<p xml:lang="eu">NeoChat «Matrix»erako, bat-bateko mezularitzarako komunikazio deszentralizatuko protokolorako, bezero bat da. Zure sendiari, kide eta lagunei testu mezuak, bideo eta audio fitxategiak bidaltzeko aukera ematen dizu. «KDE Frameworks» eta bereziki «Kirigami» erabiltzen ditu plataforma anitzen artean esperientzia konbergente bat eskaintzeko.</p>
|
||||
<p xml:lang="fi">NeoChat on asiakassovellus Matrixille, hajautetulle pikaviestinyhteyskäytännölle. Sillä voi lähettää teksti-, video- ja ääniviestejä perheelle, tutuille ja ystäville. Se käyttää KDE-kehystä ja erityisesti Kirigamia tuottaakseen mukautuvan monialustaisen käyttökokemuksen.</p>
|
||||
<p xml:lang="fr">NeoChat est un client pour le protocole Matrix, un protocole décentralisé de communications pour messagerie instantané. Il vous permet d'envoyer des messages de texte, des vidéos et des fichiers audio à votre famille, vos collègues et vos amis. Il utilise les environnements de développement et plus précisément Kirigami pour fournir une expérience convergente sur plusieurs plate-formes. </p>
|
||||
<p xml:lang="gl">NeoChat é un cliente para Matrix, o protocolo de comunicación descentralizada para mensaxerí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="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="ia">NeoChat es un cliente per Matrix, le protocollo de communication decentralisate per messager instantanee. Illo te permitte inviar messager de texto, files de video e audio a tu familia, collegas e amicos usante. Illo usa KDE frameworks e super toto Kirigamii forni un experientia convergente trans platteforme multiple.</p>
|
||||
<p xml:lang="it">NeoChat è un client per Matrix, il protocollo di comunicazione decentralizzato per la messaggistica istantanea. Ti consente di inviare messaggi di testo, video e file audio a familiari, colleghi e amici. Utilizza i framework KDE e in particolare Kirigami per fornire un'esperienza convergente su più piattaforme.</p>
|
||||
<p xml:lang="ka">NeoChat არის Matrix კლიენტი. ის საშუალებას გაძლევთ გაგზავნოთ ტექსტური შეტყობინებები, ვიდეოები და აუდიო ფაილები თქვენს ოჯახს, კოლეგებსა და მეგობრებს მატრიქსის პროტოკოლის გამოყენებით.</p>
|
||||
<p xml:lang="ko">NeoChat은 분산형 인스턴트 메시징 통신 프로토콜인 Matrix 클라이언트입니다. 가족, 동료, 친구에게 텍스트 메시지, 동영상, 오디오 파일을 전송할 수 있습니다. KDE 프레임워크와 Kirigami를 사용하여 다양한 플랫폼에서 일관적인 사용자 경험을 제공합니다.</p>
|
||||
<p xml:lang="nl">NeoChat is een client voor Matrix, het gedecentraliseerde communicatieprotocol voor instant messages. Het biedt u het verzenden van tekstberichten, video's en geluidsbestanden naar uw familie, collega's en vrienden. Het gebruik KDE frameworks en het meest opmerkelijk Kirigami om een convergente ervaring te leveren op meerdere platforms.</p>
|
||||
<p xml:lang="nn">NeoChat er ein klient for Matrix, ein protokoll for desentralisert kommunikasjon. Du kan utveksla tekst, lyd og videoar med kollegaar, vennar og familie. Programmet brukar KDE Frameworks og Kirigami for å gje ei brukarflate tilpassa ulike plattformer.</p>
|
||||
<p xml:lang="pl">NeoChat jest programem do Matriksa, protokołu rozproszonego porozumiewania się w czasie rzeczywistym. Umożliwia wysyłanie wiadomości tekstowych, filmów oraz dźwięku do twojej rodziny, znajomych oraz przyjaciół. Używa szkieletów KDE i głównie Kirigami, aby zapewnić spójne wrażenia na wielu platformach</p>
|
||||
<p xml:lang="pt">O NeoChat é um cliente do Matrix. O mesmo permite-lhe enviar mensagens de texto, ficheiros de vídeo e áudio para a sua família, colegas e amigos com o protocolo Matrix. Usa as plataformas do KDE, e principalmente o Kirigami, para oferecer uma experiência convergente entre várias plataformas.</p>
|
||||
<p xml:lang="sl">Neochat je odjemalec za Matrix, decentralizirani komunikacijski protokol za takojšnje sporočanje. Omogoča vam pošiljanje besedilnih sporočil, videoposnetkov in zvočnih datotek svoji družini, sodelavcem in prijateljem. Uporablja okvire ogrodje KDE frameworks in predvsem Kirigami za zagotavljanje konvergentne izkušnje na več platformah.</p>
|
||||
<p xml:lang="sv">NeoChat är en klient för Matrix, det decentraliserade kommunikationsprotokollet för direktmeddelanden. Den låter dig skicka textmeddelanden, videor och ljudfiler till din familj, kollegor och vänner. Den använder KDE Ramverk, i synnerhet Kirigami, för att tillhandahålla en konvergent upplevelse på flera plattformar.</p>
|
||||
<p xml:lang="tr">NeoChat, anlık iletileşme için merkezi olmayan iletişim protokolü olan Matrix için bir istemcidir. Ailenize, iş arkadaşlarınıza ve arkadaşlarınıza metin iletiler, videolar ve ses dosyaları göndermenize olanak tanır. Birden çok platformda yakınsak bir deneyim sağlamak için KDE Frameworks ve en önemlilerinden Kirigami'yi kullanır.</p>
|
||||
<p xml:lang="uk">NeoChat — клієнт Matrix, децентралізованого протоколу спілкування для миттєвого обміну повідомленнями. За його допомогою ви можете надсилати текстові повідомлення, відео та звукові файли вашій родин, колегами та друзям. У програмі використано бібліотеки KDE, зокрема Kirigami, для надання однорідного середовища на декількох програмних та апаратних платформах.</p>
|
||||
<p xml:lang="x-test">xxNeoChat is a client for Matrix, the decentralized communication protocol for instant messaging. It allows you to send text messages, videos and audio files to your family, colleagues and friends. It uses KDE frameworks and most notably Kirigami to provide a convergent experience across multiple platforms.xx</p>
|
||||
@@ -111,18 +97,22 @@ to provide a convergent experience across multiple platforms.</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>
|
||||
<p xml:lang="ca-valencia">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 VoIP, fils i alguns aspectes de l'encriptació d'extrem a extrem. Hi ha algunes altres omissions més xicotetes a causa del fet que l'especificació de Matrix està evolucionant constantment, però l'objectiu seguix sent proporcionar suport eventual per a tota l'especificació.</p>
|
||||
<p xml:lang="en-GB">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="eo">NeoChat celas esti plene kapabla aplikaĵo por la Matrix-specifo. Kiel tia, ĉio en la nuna stabila specifo kun la rimarkindaj esceptoj de VoIP, fadenoj kaj kelkaj aspektoj de Fin-al-Fina Ĉifrado estas subtenataj. Estas kelkaj aliaj pli malgrandaj preterlasoj pro la fakto, ke la Matrix-speco konstante evoluas, sed la celo restas provizi finfine subtenon por la tuta specifaĵo.</p>
|
||||
<p xml:lang="es">NeoChat pretende ser una aplicación con todas las funciones para la especificación de Matrix. Como tal, admite todo en la especificación estable actual, con las notables excepciones de VoIP, subprocesos y algunas funciones de cifrado de extremo a extremo. Existen algunas omisiones menos importantes debido al hecho de que la especificación de Matrix está en constante evolución, pero el objetivo sigue siendo brindar compatibilidad final con toda la especificación.</p>
|
||||
<p xml:lang="eu">«NeoChat»ek «Matrix» zehaztapenaren ezaugarri guztiak eskaintzen dituen aplikazio bat izan nahi du. Beraz, egungo zehaztapen egonkorrean dagoen guztiaren euskarria du, VoIP, hariak eta muturren artean zifratzeko salbuespen nabarmenekin. Badira beste ez-betetze txikiago batzuk, «Matrix»en zehaztapena etengabe eboluzioan dagoelako, baina azken helburua zehaztapen osoaren euskarria ematea izaten jarraitzen du.</p>
|
||||
<p xml:lang="fi">NeoChat pyrkii olemaan Matrix-määritelmän täysominaisuuksinen sovellus, joten se tukee kaikkea nykyisessä vakaassa määritelmässä muutamaa huomattavaa poikkeusta lukuun ottamatta (VoIP, säikeet ja jotkin piirteet päästä päähän -salauksessa). Joitakin pienempiäkin puutteita on Matrix-määritelmän jatkuvan kehityksen vuoksi, mutta lopputavoitteena on tarjota määritelmän täysi tuki.</p>
|
||||
<p xml:lang="fr">L'objectif de NeoChat est d'être une application complète pour le protocole Matrix. En tant que tel, tout dans la spécification stable actuelle avec les exceptions notables de VoIP, les processus et certains aspects du chiffrement de bout en bout sont pris en charge. Il y a quelques autres petites omissions en raison du fait que la spécification du protocole Matrix est en constante évolution. Cependant, l'objectif reste de fournir un soutien éventuel pour l'ensemble de la spécification.</p>
|
||||
<p xml:lang="gl">NeoChat pretende ser unha aplicación completa para a especificación de Matrix. Coas excepcións de VoIP, conversas fiadas e algúns aspectos da cifraxe de extremo a extremo, a versión estábel segue as especificacións. Existen algunhas outras pequenas omisións debido ao feito de que Matrix está en continua evolución pero a intención é implementar a especificación completa.</p>
|
||||
<p xml:lang="ia">NeoChat aspira a esser un application plenemente eminente per le specification de Matrix. Tal como omne cosas in le specification currentemente stabile con le exceptiones notabile de VOIP, threads e alcun aspectos del cryptation End-to-End es supportate. Il ha ltere pauc omissiones, debite al facto que le specification de Matrix es in evolution constante ma le aspiration remane a fornir supporto eventual per le integre specification.</p>
|
||||
<p xml:lang="it">NeoChat mira ad essere un'applicazione completa per le specifiche Matrix. Pertanto, sono supportati tutti gli elementi dell'attuale specifica stabile con le notevoli eccezioni di VoIP, conversazioni e alcuni aspetti della cifratura end-to-end. Ci sono alcune altre piccole omissioni dovute al fatto che le specifiche Matrix sono in continua evoluzione, ma l'obiettivo rimane quello di fornire un eventuale supporto per l'intera specifica.</p>
|
||||
<p xml:lang="ka">NeoChat-ი მიზნად ისახავს Matrix სპეციფიკაციის სრული განხორციელება ჰქონდეს. როგორც ასეთი, ყველაფერი მიმდინარე სპეციფიკაციიდან, VoIP-ის, ძაფებისა და გამჭოლი დაშიფვრის ზოგიერთი ასპექტის გარდა, მხარდაჭერილია. შეძლება ასევე იყოს მცირე ლაფსუსებიც იმის გამო, რომ Matrix-ის სპეციფიკაცია მუდმივად ვითარგდება, მაგრამ ჩვენი მიზანი მისი სრული მხარდაჭერაა.</p>
|
||||
<p xml:lang="ko">NeoChat은 Matrix 표준을 따르는 프로그램을 목표로 합니다. 현재 안정 버전의 표준에서 제공하는 기능의 대부분을 지원하며, VoIP, 스레드, 일부 종단간 암호화와 같은 기능은 아직 지원하지 않습니다. Matrix 표준은 계속하여 진화 중이기 때문에 일부 기능이 빠져 있을 수도 있지만 장기적으로는 전체 표준을 지원하는 것이 목표입니다.</p>
|
||||
<p xml:lang="nl">NeoChat richt zich op het volledig bieden van alle mogelijkheden van de Matrix-specificatie. Alles in de huidige stabiele specificatie met merkbare uitzondering van VoIP, gekoppelde discussies en sommige aspecten van eind-tot-eind versleuteling worden ondersteund. Er zijn een paar andere kleinere omissies vanwege het feit dat de Matrix specificatie constant evolueert maar het doel blijft het eventueel bieden van ondersteuning van de gehele specificatie.</p>
|
||||
<p xml:lang="nn">NeoChat har som mål å støtta all funksjonalitet i Matrix-spesifikasjonen. Førebels er alt i den gjeldande stabile spesifikasjonen støtta, med unntak av VoIP, trådar og nokre delar av ende-til-kryptering. Det finst òg andre småting som ikkje er støtta, sidan Matrix-spesifikasjon er i stadig endring, men målet er altså støtte for alt.</p>
|
||||
<p xml:lang="pl">NeoChat w zamyśle ma być pełnowartościową aplikacją wg wytycznych Matriksa. Z tego powodu, wszystko, co jest obecnie w stabilnych wytycznych z pominięciem VoIP, wątków i niektórych części szyfrowania Użytkownik-do-Użytkownika są obecnie obsługiwane. Pominięto też kilka mniejszych rzeczy ze względu na ciągły rozwój wytycznych Matriksa, lecz celem nadal jest zapewnienie obsługi wszystkich wytycznych.</p>
|
||||
<p xml:lang="pt">O NeoChat pretende ser uma aplicação completa para a especificação do Matrix. Como tal, tudo o que existe na especificação estável actual, com as notáveis excepções do VoIP, tópicos e alguns aspectos da Encriptação Ponto-a-Ponto, são suportados. Existem mais algumas omissões, devido ao facto que a norma do Matrix está em constante evolução, mas o objectivo continua a ser oferecer o suporte eventual para a norma por inteiro.</p>
|
||||
<p xml:lang="sl">Neochat cilja, da bi bila popolna aplikacija po specifikaciji Matrixa. Kot takšna vsebuje vse v trenutni stabilni specifikaciji z pomembnimi izjemami pri VoIP, nitih in nekaterih vidikov šifriranja od konca do konca. Obstaja nekaj drugih manjših opustitev zaradi dejstva, da se specifikacija Matrix nenehno razvija, vendar cilj ostaja zagotoviti morebitno podporo celotni specifikaciji.</p>
|
||||
<p xml:lang="sv">NeoChat har som mål att vara ett fullständigt program enligt Matrix-specifikationen. Som sådant stöds allt i den nuvarande stabila specifikationen, med de nämnvärda undantagen VoIP, trådar och några aspekter av kryptering hela vägen. Det finns några ytterligare utelämnanden på grund av att Matrix-specifikationen hela tiden utvecklas, men målet förblir att till slut erbjuda stöd för hela specifikationen.</p>
|
||||
<p xml:lang="tr">NeoChat, Matrix belirtimi için tam özellikli bir uygulama olmayı hedefler. Bu nedenle; VoIP, ileti zincirleri ve Uçtan Uca Şifreleme'nin bazı yönleri gibi dikkate değer istisnalar dışında var olan kararlı belirtimdeki her şey desteklenir. Matrix belirtiminin sürekli gelişmesi nedeniyle birkaç küçük eksiklik daha var; ancak amaç tüm belirtim için nihai destek sağlamak olmayı sürdürüyor.</p>
|
||||
<p xml:lang="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>
|
||||
@@ -131,18 +121,23 @@ to provide a convergent experience across multiple platforms.</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>
|
||||
<p xml:lang="ca-valencia">A causa de la naturalea del desenvolupament de l'especificació de Matrix, NeoChat també implementa nombroses característiques inestables. Actualment són:</p>
|
||||
<p xml:lang="en-GB">Due to the nature of the Matrix specification development NeoChat also supports numerous unstable features. Currently these are:</p>
|
||||
<p xml:lang="eo">Pro la naturo de la Matrix-specifevoluo NeoChat ankaŭ subtenas multajn malstabilajn funkciojn. Nuntempe ĉi tiuj estas:</p>
|
||||
<p xml:lang="es">Debido a la naturaleza del desarrollo de la especificación de Matrix, NeoChat también permite numerosas funciones no estables, como:</p>
|
||||
<p xml:lang="eu">«Matrix» zehaztapenaren garapenaren izaeragatik, «Neo¢hat»ek ezaugarri ezegonkor ugari ere onartzen ditu. Gaur egun hauek:</p>
|
||||
<p xml:lang="fi">Matrix-määritelmän kehittyessä NeoChat tukee myös monia epävakaita ominaisuuksia. Tällä hetkellä näitä ovat:</p>
|
||||
<p xml:lang="fr">En raison de la nature du développement des spécifications du protocole Matrix, NeoChat prend également en charge de nombreuses fonctionnalités instables. Actuellement, ce sont :</p>
|
||||
<p xml:lang="gl">Debido á natureza do desenvolvemento da especificación de Matrix, NeoChat tamén inclúe varias funcionalidades non estábeis:</p>
|
||||
<p xml:lang="ia">Debite al natura del disveloppamento de specification de Matrix NeoChat tamben supporta numerose characteristicas instabile. Currentemente istes es:</p>
|
||||
<p xml:lang="it">A causa della natura dello sviluppo delle specifiche Matrix, NeoChat supporta anche numerose funzionalità instabili. Attualmente queste sono:</p>
|
||||
<p xml:lang="ka">Matrix-ის სპეციფიკაციის განვითარების ბუნების გამო NeoChat-ს ასევე აქვს უამრავი არასტაბილური ფუნქციაც. ახლა ისინია:</p>
|
||||
<p xml:lang="ko">Matrix 표준 개발의 특징으로 인하여 NeoChat은 일부 실험적인 기능을 지원합니다. 현재 지원하는 기능은 다음과 같습니다.</p>
|
||||
<p xml:lang="nl">Vanwege de aard van de ontwikkeling van de Matrix specificatie ondersteunt NeoChat ook talloze onstabiele mogelijkheden. Dit zijn nu:</p>
|
||||
<p xml:lang="nn">På grunn av måten Matrix-spesifikasjonen vert utvikla på, støttar NeoChat òg nokre uferdige funksjonar:</p>
|
||||
<p xml:lang="pl">Ze względu na sposób rozwoju Matriksa, NeoChat obsługuje także kilka niestabilnych możliwości. Obecnie są to:</p>
|
||||
<p xml:lang="pt">Devido à natureza do desenvolvimento da especificação do Matrix, o NeoChat também suporta diversas funcionalidades instáveis. De momento são:</p>
|
||||
<p xml:lang="sl">Zaradi narave razvoja specifikacije Matrixa NeoChat podpira tudi številne nestabilne zmožnosti. Trenutno so to:</p>
|
||||
<p xml:lang="sv">På grund av sättet Matrix-specifikationens utvecklas, stöder NeoChat också ett stor antal instabila funktioner. För närvarande är de:</p>
|
||||
<p xml:lang="ta">மேட்ரிக்ஸு நெறிமுறை வரையறுக்கப்படும் விதத்தின் காரணமாக, பல நிலையற்ற அம்சங்களையும் நியோச்சாட் ஆதரிக்கிறது. தற்போது ஆதரிக்கப்படுபவை:</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>
|
||||
@@ -152,17 +147,22 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<li xml:lang="ca">Enquestes - MSC3381</li>
|
||||
<li xml:lang="ca-valencia">Enquestes - MSC3381</li>
|
||||
<li xml:lang="en-GB">Polls - MSC3381</li>
|
||||
<li xml:lang="eo">Enketoj - MSC3381</li>
|
||||
<li xml:lang="es">Encuestas - MSC3381</li>
|
||||
<li xml:lang="eu">Polls - MSC3381</li>
|
||||
<li xml:lang="fi">Kyselyt – MSC3381</li>
|
||||
<li xml:lang="fr">Sondages - MSC3381</li>
|
||||
<li xml:lang="gl">Enquisas - MSC3381</li>
|
||||
<li xml:lang="gl">Enquisas — MSC3381</li>
|
||||
<li xml:lang="ia">Inquestas - MSC3381</li>
|
||||
<li xml:lang="it">Sondaggi - MSC3381</li>
|
||||
<li xml:lang="ka">Polls - MSC3381</li>
|
||||
<li xml:lang="ko">투표 - MSC3381</li>
|
||||
<li xml:lang="nl">Polls - MSC3381</li>
|
||||
<li xml:lang="nn">Avstemmingar – MSC3381</li>
|
||||
<li xml:lang="pl">Ankiety - MSC3381</li>
|
||||
<li xml:lang="pt">Inquéritos - MSC3381</li>
|
||||
<li xml:lang="sl">Polls - MSC3381</li>
|
||||
<li xml:lang="sv">Polls - MSC3381</li>
|
||||
<li xml:lang="ta">வாக்கெடுப்புகள் - MSC3381</li>
|
||||
<li xml:lang="tr">Anketler - MSC3381</li>
|
||||
<li xml:lang="uk">Опитування - MSC3381</li>
|
||||
@@ -172,17 +172,22 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<li xml:lang="ca">Paquets d'adhesius - MSC2545</li>
|
||||
<li xml:lang="ca-valencia">Paquets d'adhesius - MSC2545</li>
|
||||
<li xml:lang="en-GB">Sticker Packs - MSC2545</li>
|
||||
<li xml:lang="eo">Glumark-Pakoj - MSC2545</li>
|
||||
<li xml:lang="es">Paquetes de pegatinas - MSC2545</li>
|
||||
<li xml:lang="eu">Eranskailu paketeak - MSC2545</li>
|
||||
<li xml:lang="fi">Tarrapakkaukset – MSC2545</li>
|
||||
<li xml:lang="fr">Paquets d'auto-collants - MSC2545</li>
|
||||
<li xml:lang="gl">Paquetes de adhesivos - MSC2545</li>
|
||||
<li xml:lang="gl">Paquetes de adhesivos — MSC2545</li>
|
||||
<li xml:lang="ia">Etiquetta gummate (sticker) -MSC2545</li>
|
||||
<li xml:lang="it">Pacchetti di adesivi - MSC2545</li>
|
||||
<li xml:lang="ka">სტიკერების პაკეტები - MSC2545</li>
|
||||
<li xml:lang="ko">스티커 팩 - MSC2545</li>
|
||||
<li xml:lang="nl">Sticker Packs - MSC2545</li>
|
||||
<li xml:lang="nn">Klistremerke-pakkar – MSC2545</li>
|
||||
<li xml:lang="pl">Paczki naklejek - MSC2545</li>
|
||||
<li xml:lang="pt">Pacotes de Autocolantes - MSC2545</li>
|
||||
<li xml:lang="sl">Sticker Packs - MSC2545</li>
|
||||
<li xml:lang="sv">Sticker Packs - MSC2545</li>
|
||||
<li xml:lang="ta">ஒட்டி தொகுப்புகள் - MSC2545</li>
|
||||
<li xml:lang="tr">Yapışkan Paketleri - MSC2545</li>
|
||||
<li xml:lang="uk">Пакунки наліпок - MSC2545</li>
|
||||
@@ -192,17 +197,22 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<li xml:lang="ca">Esdeveniments d'ubicació - MSC3488</li>
|
||||
<li xml:lang="ca-valencia">Esdeveniments d'ubicació - MSC3488</li>
|
||||
<li xml:lang="en-GB">Location Events - MSC3488</li>
|
||||
<li xml:lang="eo">Lokaj Eventoj - MSC3488</li>
|
||||
<li xml:lang="es">Eventos de ubicación - MSC3488</li>
|
||||
<li xml:lang="eu">Kokaleku-gertaerak - MSC3488</li>
|
||||
<li xml:lang="fi">Sijaintitapahtumat – MSC3488</li>
|
||||
<li xml:lang="fr">Événements de lieu - MSC3488</li>
|
||||
<li xml:lang="gl">Localización de eventos - MSC3488</li>
|
||||
<li xml:lang="gl">Localización de eventos — MSC3488</li>
|
||||
<li xml:lang="ia">Eventos de Location - MSC3488</li>
|
||||
<li xml:lang="it">Località eventi - MSC3488</li>
|
||||
<li xml:lang="ka">მდებარეობის მოვლენები - MSC3488</li>
|
||||
<li xml:lang="ko">위치 이벤트 - MSC3488</li>
|
||||
<li xml:lang="nl">Locatie gebeurtenissen - MSC3488</li>
|
||||
<li xml:lang="nn">Posisjonshendingar – MSC3488</li>
|
||||
<li xml:lang="pl">Wydarzenia w miejscach - MSC3488</li>
|
||||
<li xml:lang="pt">Eventos com Localizações - MSC3488</li>
|
||||
<li xml:lang="sl">Location Events - MSC3488</li>
|
||||
<li xml:lang="sv">Location Events - MSC3488</li>
|
||||
<li xml:lang="ta">இட நிகழ்வுகள் - MSC3488</li>
|
||||
<li xml:lang="tr">Konum Etkinlikleri - MSC3488</li>
|
||||
<li xml:lang="uk">Місцеві зустрічі - MSC3488</li>
|
||||
@@ -261,34 +271,6 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<value key="KDE::windows_store::StoreLogoSquare">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/storelogo-1080x1080.png</value>
|
||||
<value key="KDE::windows_store::Icon">https://invent.kde.org/network/neochat/-/raw/master/icons/300-apps-neochat.png</value>
|
||||
<value key="KDE::windows_store::PromotionalArt16x9">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/promoimage-1920x1080.png</value>
|
||||
<value key="KDE::windows_store::screenshots::1::image">https://cdn.kde.org/screenshots/neochat/NeoChat-Windows-Timeline.png</value>
|
||||
<value key="KDE::windows_store::screenshots::1::caption">Main view with room list, chat, and room information</value>
|
||||
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="ca">Vista principal amb la llista de sales, xats i informació de les sales</value>
|
||||
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="ca-valencia">Vista principal amb la llista de sales, xats i informació de les sales</value>
|
||||
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="es">Vista principal con la lista de salas, chat e información de la sala</value>
|
||||
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="fr">Vue principale avec la liste des salons ainsi que des informations sur les salons et forums de discussions</value>
|
||||
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="ka">მთავარი ხედი სურათების სიით, ჩატით და ოთახის ინფორმაციით</value>
|
||||
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="nl">Hoofdweergave met lijst met rooms, chat en roominformatie</value>
|
||||
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="pt">A área principal com a lista de salas e com informações sobre a conversa e a sala</value>
|
||||
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="sl">Glavni pogled s seznamom sob, klepetom in informacijami o sobah</value>
|
||||
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="ta">அரங்குப்பட்டியல், உரையாடல், மற்றும் அரங்குவிவரங்களைக் கொண்டுள்ள பிரதான காட்சி</value>
|
||||
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="tr">Oda listesini, sohbet penceresini ve oda bilgisini gösteren ana görünüm</value>
|
||||
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="uk">Головна панель із списком кімнат, спілкуванням та даними щодо кімнати</value>
|
||||
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="x-test">xxMain view with room list, chat, and room informationxx</value>
|
||||
<value key="KDE::windows_store::screenshots::2::image">https://cdn.kde.org/screenshots/neochat/NeoChat-Windows-Login.png</value>
|
||||
<value key="KDE::windows_store::screenshots::2::caption">Login screen</value>
|
||||
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="ca">Pantalla d'inici de sessió</value>
|
||||
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="ca-valencia">Pantalla d'inici de sessió</value>
|
||||
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="es">Pantalla de inicio de sesión</value>
|
||||
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="fr">Écran de connexion</value>
|
||||
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="ka">შესვლის ეკრანი</value>
|
||||
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="nl">Aanmeldscherm</value>
|
||||
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="pt">Ecrã de autenticação</value>
|
||||
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="sl">Prijavni zaslon</value>
|
||||
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="ta">நுழைவுத் திரை</value>
|
||||
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="tr">Oturum açma ekranı</value>
|
||||
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="uk">Вікно входу</value>
|
||||
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="x-test">xxLogin screenxx</value>
|
||||
</custom>
|
||||
<launchable type="desktop-id">org.kde.neochat.desktop</launchable>
|
||||
<screenshots>
|
||||
@@ -298,15 +280,79 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<screenshot type="default">
|
||||
<image>https://cdn.kde.org/screenshots/neochat/application.png</image>
|
||||
</screenshot>
|
||||
<screenshot x-kde-os="windows">
|
||||
<image>https://cdn.kde.org/screenshots/neochat/NeoChat-Windows-Timeline.png</image>
|
||||
<caption>Main view with room list, chat, and room information</caption>
|
||||
<caption xml:lang="ar">العرض الرئيسة مع قائمة الغرف والدردشات و معلومات الغرفة</caption>
|
||||
<caption xml:lang="ca">Vista principal amb la llista de sales, xats i informació de les sales</caption>
|
||||
<caption xml:lang="ca-valencia">Vista principal amb la llista de sales, xats i informació de les sales</caption>
|
||||
<caption xml:lang="eo">Ĉefa vido kun ĉambra listo, babilejo kaj ĉambra informo</caption>
|
||||
<caption xml:lang="es">Vista principal con la lista de salas, chat e información de la sala</caption>
|
||||
<caption xml:lang="eu">Ikuspegi nagusia gela-zerrenda, berriketa, eta gelako informazioarekin</caption>
|
||||
<caption xml:lang="fi">Päänäkymä, jossa huoneluettelo, keskustelu ja huoneen tiedot</caption>
|
||||
<caption xml:lang="fr">Vue principale avec la liste des salons ainsi que des informations sur les salons et forums de discussions</caption>
|
||||
<caption xml:lang="gl">Vista principal coa lista de salas, a charla, e información da sala.</caption>
|
||||
<caption xml:lang="it">Vista principale con elenco delle stanze, chat e informazioni sulla stanza</caption>
|
||||
<caption xml:lang="ka">მთავარი ხედი სურათების სიით, ჩატით და ოთახის ინფორმაციით</caption>
|
||||
<caption xml:lang="ko">대화방 목록, 채팅, 대화방 정보가 표시된 주 보기</caption>
|
||||
<caption xml:lang="nl">Hoofdweergave met lijst met rooms, chat en roominformatie</caption>
|
||||
<caption xml:lang="nn">Hovudvising med romliste, pratevindauge og rominformasjon</caption>
|
||||
<caption xml:lang="pl">Główny widok z wykazem pokojów, rozmowami i szczegółami pokojów</caption>
|
||||
<caption xml:lang="pt">A área principal com a lista de salas e com informações sobre a conversa e a sala</caption>
|
||||
<caption xml:lang="sl">Glavni pogled s seznamom sob, klepetom in informacijami o sobah</caption>
|
||||
<caption xml:lang="sv">Huvudvy med rumslista, chatt, och rumsinformation</caption>
|
||||
<caption xml:lang="ta">அரங்குப்பட்டியல், உரையாடல், மற்றும் அரங்குவிவரங்களைக் கொண்டுள்ள பிரதான காட்சி</caption>
|
||||
<caption xml:lang="tr">Oda listesini, sohbet penceresini ve oda bilgisini gösteren ana görünüm</caption>
|
||||
<caption xml:lang="uk">Головна панель із списком кімнат, спілкуванням та даними щодо кімнати</caption>
|
||||
<caption xml:lang="x-test">xxMain view with room list, chat, and room informationxx</caption>
|
||||
</screenshot>
|
||||
<screenshot x-kde-os="windows">
|
||||
<image>https://cdn.kde.org/screenshots/neochat/NeoChat-Windows-Login.png</image>
|
||||
<caption>Login screen</caption>
|
||||
<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="eo">Ensaluta ekrano</caption>
|
||||
<caption xml:lang="es">Pantalla de inicio de sesión</caption>
|
||||
<caption xml:lang="eu">Saio-hasteko pantaila</caption>
|
||||
<caption xml:lang="fi">Kirjautumisnäkymä</caption>
|
||||
<caption xml:lang="fr">Écran de connexion</caption>
|
||||
<caption xml:lang="gl">Pantalla de identificación.</caption>
|
||||
<caption xml:lang="it">Schermata di accesso</caption>
|
||||
<caption xml:lang="ka">შესვლის ეკრანი</caption>
|
||||
<caption xml:lang="ko">로그인 화면</caption>
|
||||
<caption xml:lang="nl">Aanmeldscherm</caption>
|
||||
<caption xml:lang="nn">Innloggingsbilete</caption>
|
||||
<caption xml:lang="pl">Ekran logowania</caption>
|
||||
<caption xml:lang="pt">Ecrã de autenticação</caption>
|
||||
<caption xml:lang="sl">Prijavni zaslon</caption>
|
||||
<caption xml:lang="sv">Inloggningsfönster</caption>
|
||||
<caption xml:lang="ta">நுழைவுத் திரை</caption>
|
||||
<caption xml:lang="tr">Oturum açma ekranı</caption>
|
||||
<caption xml:lang="uk">Вікно входу</caption>
|
||||
<caption xml:lang="x-test">xxLogin screenxx</caption>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<content_rating type="oars-1.1">
|
||||
<content_attribute id="social-chat">intense</content_attribute>
|
||||
</content_rating>
|
||||
<releases>
|
||||
<release version="23.08.0" date="2023-08-24">
|
||||
<url>https://kde.org/announcements/gear/23.08.0/#neochathttpsappskdeorgneochat</url>
|
||||
<description>
|
||||
<p>Apart from a visual overhaul, NeoChat can now display location events and also a map with the location of all the users currently broadcasting their location using Itineray's Matrix integration. Great for locating where your friends are.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="23.04.3" date="2023-07-06"/>
|
||||
<release version="23.04.2" date="2023-06-08"/>
|
||||
<release version="23.04.1" date="2023-05-11"/>
|
||||
<release version="23.04.0" date="2023-04-20">
|
||||
<url>https://kde.org/announcements/gear/23.04.0/#neochathttpsappskdeorgneochat</url>
|
||||
<description>
|
||||
<p>NeoChat improves its design with tweaks that provide a more compact layout and a simpler menu which works better for the collapsed room list.</p>
|
||||
<p>We have also improved the video controls, added a new command /knock <room-id> to send a knock event to a room, and you can now edit a prior message inline, within the chat pane.</p>
|
||||
<p>Other usability improvements include an overhaul of the keyboard navigation and shortcuts like Ctrl+PgUp/PgDn that allow you to skip from room to room.</p>
|
||||
</description>
|
||||
<artifacts>
|
||||
<artifact type="binary" platform="x86_64-windows-msvc">
|
||||
<location>https://download.kde.org/stable/release-service/23.04.0/windows/neochat-23.04.0-512-windows-cl-msvc2019-x86_64.exe</location>
|
||||
|
||||
@@ -93,7 +93,7 @@ Comment[es]=Cliente para el protocolo Matrix
|
||||
Comment[eu]=Matrix protokolorako bezeroa
|
||||
Comment[fi]=Asiakas Matrix-yhteyskäytännölle
|
||||
Comment[fr]=Client pour le protocole « Matrix »
|
||||
Comment[gl]=Cliente para o protocolo Matrix
|
||||
Comment[gl]=Cliente para o protocolo Matrix.
|
||||
Comment[hu]=Kliens a Matrix protokollhoz
|
||||
Comment[ia]=Cliente per le protocollo de Matrix
|
||||
Comment[id]=Klien untuk protokol Matrix
|
||||
|
||||
2702
po/ar/neochat.po
2702
po/ar/neochat.po
File diff suppressed because it is too large
Load Diff
2700
po/az/neochat.po
2700
po/az/neochat.po
File diff suppressed because it is too large
Load Diff
2619
po/ca/neochat.po
2619
po/ca/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2577
po/cs/neochat.po
2577
po/cs/neochat.po
File diff suppressed because it is too large
Load Diff
2595
po/da/neochat.po
2595
po/da/neochat.po
File diff suppressed because it is too large
Load Diff
2649
po/de/neochat.po
2649
po/de/neochat.po
File diff suppressed because it is too large
Load Diff
2684
po/el/neochat.po
2684
po/el/neochat.po
File diff suppressed because it is too large
Load Diff
2652
po/en_GB/neochat.po
2652
po/en_GB/neochat.po
File diff suppressed because it is too large
Load Diff
2615
po/es/neochat.po
2615
po/es/neochat.po
File diff suppressed because it is too large
Load Diff
2874
po/eu/neochat.po
2874
po/eu/neochat.po
File diff suppressed because it is too large
Load Diff
2682
po/fi/neochat.po
2682
po/fi/neochat.po
File diff suppressed because it is too large
Load Diff
2636
po/fr/neochat.po
2636
po/fr/neochat.po
File diff suppressed because it is too large
Load Diff
2666
po/hu/neochat.po
2666
po/hu/neochat.po
File diff suppressed because it is too large
Load Diff
2658
po/ia/neochat.po
2658
po/ia/neochat.po
File diff suppressed because it is too large
Load Diff
2642
po/id/neochat.po
2642
po/id/neochat.po
File diff suppressed because it is too large
Load Diff
2648
po/ie/neochat.po
2648
po/ie/neochat.po
File diff suppressed because it is too large
Load Diff
2637
po/it/neochat.po
2637
po/it/neochat.po
File diff suppressed because it is too large
Load Diff
2483
po/ja/neochat.po
2483
po/ja/neochat.po
File diff suppressed because it is too large
Load Diff
2626
po/ka/neochat.po
2626
po/ka/neochat.po
File diff suppressed because it is too large
Load Diff
4100
po/ko/neochat.po
4100
po/ko/neochat.po
File diff suppressed because it is too large
Load Diff
2493
po/lt/neochat.po
2493
po/lt/neochat.po
File diff suppressed because it is too large
Load Diff
2617
po/nl/neochat.po
2617
po/nl/neochat.po
File diff suppressed because it is too large
Load Diff
1947
po/nn/neochat.po
1947
po/nn/neochat.po
File diff suppressed because it is too large
Load Diff
2689
po/pa/neochat.po
2689
po/pa/neochat.po
File diff suppressed because it is too large
Load Diff
2848
po/pl/neochat.po
2848
po/pl/neochat.po
File diff suppressed because it is too large
Load Diff
2640
po/pt/neochat.po
2640
po/pt/neochat.po
File diff suppressed because it is too large
Load Diff
2708
po/pt_BR/neochat.po
2708
po/pt_BR/neochat.po
File diff suppressed because it is too large
Load Diff
2663
po/ru/neochat.po
2663
po/ru/neochat.po
File diff suppressed because it is too large
Load Diff
2705
po/sk/neochat.po
2705
po/sk/neochat.po
File diff suppressed because it is too large
Load Diff
2619
po/sl/neochat.po
2619
po/sl/neochat.po
File diff suppressed because it is too large
Load Diff
122
po/sv/docs/neochat/man-neochat.1.docbook
Normal file
122
po/sv/docs/neochat/man-neochat.1.docbook
Normal file
@@ -0,0 +1,122 @@
|
||||
<?xml version="1.0" ?>
|
||||
<!DOCTYPE refentry PUBLIC "-//KDE//DTD DocBook XML V4.5-Based Variant V1.1//EN" "dtd/kdedbx45.dtd" [
|
||||
<!ENTITY % Swedish "INCLUDE">
|
||||
]>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
|
||||
SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
-->
|
||||
|
||||
<refentry lang="&language;">
|
||||
<refentryinfo>
|
||||
<title
|
||||
>NeoChat användarmanual</title>
|
||||
<author
|
||||
><firstname
|
||||
>Carl</firstname
|
||||
><surname
|
||||
>Schwan</surname
|
||||
> <contrib
|
||||
>NeoChat manualsida.</contrib
|
||||
> <email
|
||||
>carl@carlschwan.eu</email
|
||||
></author>
|
||||
<date
|
||||
>2022-11-01</date>
|
||||
<releaseinfo
|
||||
>22.09</releaseinfo>
|
||||
<productname
|
||||
>NeoChat</productname>
|
||||
</refentryinfo>
|
||||
|
||||
<refmeta>
|
||||
<refentrytitle>
|
||||
<command
|
||||
>neochat</command>
|
||||
</refentrytitle>
|
||||
<manvolnum
|
||||
>1</manvolnum>
|
||||
</refmeta>
|
||||
|
||||
<refnamediv>
|
||||
<refname
|
||||
>neochat</refname>
|
||||
<refpurpose
|
||||
>Klient för att interagera med meddelandeprotokollet matrix</refpurpose>
|
||||
</refnamediv>
|
||||
<!-- body begins here -->
|
||||
<refsynopsisdiv id='synopsis'>
|
||||
<cmdsynopsis
|
||||
><command
|
||||
>neochat</command
|
||||
> <arg choice="opt"
|
||||
><replaceable
|
||||
>URI</replaceable
|
||||
></arg
|
||||
> </cmdsynopsis>
|
||||
</refsynopsisdiv>
|
||||
|
||||
|
||||
<refsect1 id="description">
|
||||
<title
|
||||
>Beskrivning</title>
|
||||
<para
|
||||
><command
|
||||
>neochat</command
|
||||
> är ett chattprogram för protokollet matrix som fungerar både på skrivbord och mobil. </para>
|
||||
</refsect1>
|
||||
|
||||
<refsect1 id="options"
|
||||
><title
|
||||
>Väljare</title>
|
||||
<variablelist>
|
||||
<varlistentry>
|
||||
<term
|
||||
><option
|
||||
>URI</option
|
||||
></term>
|
||||
<listitem>
|
||||
<para
|
||||
>Matrix webbadress för en användare eller ett rum, t.ex. matrix:u/användare:exempel.org. Det gör att NeoChat försöker öppna det angivna rummet eller konversationen. </para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
</variablelist>
|
||||
</refsect1>
|
||||
|
||||
<refsect1 id="bug">
|
||||
<title
|
||||
>Rapportera fel</title>
|
||||
<para
|
||||
>Rapportera fel och funktionsönskemål på <ulink url="https://bugs.kde.org/enter_bug.cgi?product=NeoChat&component=General"
|
||||
>https://bugs.kde.org/enter_bug.cgi?product=NeoChat&component=General</ulink
|
||||
></para>
|
||||
</refsect1>
|
||||
|
||||
<refsect1>
|
||||
<title
|
||||
>Se också</title>
|
||||
<simplelist>
|
||||
<member
|
||||
>En lista över vanliga frågor om Matrix <ulink url="https://matrix.org/faq/"
|
||||
>https://matrix.org/faq/</ulink
|
||||
> </member>
|
||||
<member
|
||||
>kf5options(7)</member>
|
||||
<member
|
||||
>qt5options(7)</member>
|
||||
</simplelist>
|
||||
</refsect1>
|
||||
|
||||
<refsect1 id="copyright"
|
||||
><title
|
||||
>Copyright</title>
|
||||
<para
|
||||
>Copyright © 2020-2022 Tobias Fella </para>
|
||||
<para
|
||||
>Copyright © 2020-2022 Carl Schwan </para>
|
||||
<para
|
||||
>Licens: GNU General Public Version 3 eller senare <<ulink url="https://wwwgnuorg/licenses/gpl-html"
|
||||
>http://www.gnu.org/licenses/gpl-3.0.html</ulink
|
||||
>></para>
|
||||
</refsect1>
|
||||
</refentry>
|
||||
3946
po/sv/neochat.po
3946
po/sv/neochat.po
File diff suppressed because it is too large
Load Diff
2614
po/ta/neochat.po
2614
po/ta/neochat.po
File diff suppressed because it is too large
Load Diff
2594
po/tok/neochat.po
2594
po/tok/neochat.po
File diff suppressed because it is too large
Load Diff
2620
po/tr/neochat.po
2620
po/tr/neochat.po
File diff suppressed because it is too large
Load Diff
2639
po/uk/neochat.po
2639
po/uk/neochat.po
File diff suppressed because it is too large
Load Diff
2600
po/zh_CN/neochat.po
2600
po/zh_CN/neochat.po
File diff suppressed because it is too large
Load Diff
2483
po/zh_TW/neochat.po
2483
po/zh_TW/neochat.po
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,6 @@
|
||||
|
||||
configure_file(qml/Page/RoomList/RoomDelegate.qml ${CMAKE_CURRENT_BINARY_DIR}/qml/Page/RoomList/RoomDelegate.qml)
|
||||
configure_file(qml/Component/QuickSwitcher.qml ${CMAKE_CURRENT_BINARY_DIR}/qml/Component/QuickSwitcher.qml)
|
||||
configure_file(qml/Dialog/UserDetailDialog.qml ${CMAKE_CURRENT_BINARY_DIR}/qml/Dialog/UserDetailDialog.qml)
|
||||
configure_file(qml/Dialog/PowerLevelDialog.qml ${CMAKE_CURRENT_BINARY_DIR}/qml/Dialog/PowerLevelDialog.qml)
|
||||
configure_file(qml/Component/Timeline/AudioDelegate.qml ${CMAKE_CURRENT_BINARY_DIR}/qml/Component/Timeline/AudioDelegate.qml)
|
||||
configure_file(qml/Component/Timeline/VideoDelegate.qml ${CMAKE_CURRENT_BINARY_DIR}/qml/Component/Timeline/VideoDelegate.qml)
|
||||
@@ -44,8 +43,6 @@ add_library(neochat STATIC
|
||||
roommanager.h
|
||||
neochatroom.cpp
|
||||
neochatroom.h
|
||||
neochatuser.cpp
|
||||
neochatuser.h
|
||||
models/userlistmodel.cpp
|
||||
models/userlistmodel.h
|
||||
models/userfiltermodel.cpp
|
||||
@@ -77,8 +74,6 @@ add_library(neochat STATIC
|
||||
blurhash.h
|
||||
blurhashimageprovider.cpp
|
||||
blurhashimageprovider.h
|
||||
models/collapsestateproxymodel.cpp
|
||||
models/collapsestateproxymodel.h
|
||||
models/mediamessagefiltermodel.cpp
|
||||
models/mediamessagefiltermodel.h
|
||||
urlhelper.cpp
|
||||
@@ -127,6 +122,21 @@ add_library(neochat STATIC
|
||||
locationhelper.h
|
||||
events/pollevent.cpp
|
||||
pollhandler.cpp
|
||||
utils.h
|
||||
registration.cpp
|
||||
neochatconnection.cpp
|
||||
neochatconnection.h
|
||||
jobs/neochatdeactivateaccountjob.cpp
|
||||
jobs/neochatdeactivateaccountjob.h
|
||||
jobs/neochatdeletedevicejob.cpp
|
||||
jobs/neochatdeletedevicejob.h
|
||||
jobs/neochatchangepasswordjob.cpp
|
||||
jobs/neochatchangepasswordjob.h
|
||||
mediasizehelper.cpp
|
||||
mediasizehelper.h
|
||||
eventhandler.cpp
|
||||
enums/delegatetype.h
|
||||
messageformatter.cpp
|
||||
)
|
||||
|
||||
ecm_qt_declare_logging_category(neochat
|
||||
@@ -138,11 +148,23 @@ ecm_qt_declare_logging_category(neochat
|
||||
EXPORT NEOCHAT
|
||||
)
|
||||
|
||||
ecm_qt_declare_logging_category(neochat
|
||||
HEADER "eventhandler_logging.h"
|
||||
IDENTIFIER "EventHandling"
|
||||
CATEGORY_NAME "org.kde.neochat.eventhandler"
|
||||
DEFAULT_SEVERITY Info
|
||||
)
|
||||
|
||||
add_executable(neochat-app
|
||||
main.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/res.generated.qrc
|
||||
)
|
||||
|
||||
if(TARGET Qt::WebView)
|
||||
target_link_libraries(neochat-app PUBLIC Qt::WebView)
|
||||
target_compile_definitions(neochat-app PUBLIC -DHAVE_WEBVIEW)
|
||||
endif()
|
||||
|
||||
target_include_directories(neochat-app PRIVATE ${CMAKE_BINARY_DIR})
|
||||
|
||||
target_link_libraries(neochat-app PRIVATE
|
||||
@@ -157,6 +179,9 @@ if(NOT ANDROID)
|
||||
target_sources(neochat PRIVATE colorschemer.cpp colorschemer.h)
|
||||
if (NOT WIN32 AND NOT APPLE)
|
||||
target_sources(neochat PRIVATE trayicon_sni.cpp trayicon_sni.h)
|
||||
if(QT_MAJOR_VERSION STREQUAL "6")
|
||||
target_link_libraries(neochat PRIVATE KF6::StatusNotifierItem)
|
||||
endif()
|
||||
else()
|
||||
target_sources(neochat PRIVATE trayicon.cpp trayicon.h)
|
||||
endif()
|
||||
@@ -175,7 +200,8 @@ else()
|
||||
endif()
|
||||
|
||||
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR})
|
||||
target_link_libraries(neochat PUBLIC Qt::Core Qt::Quick Qt::Qml Qt::Gui Qt::Multimedia Qt::Network Qt::QuickControls2 KF${QT_MAJOR_VERSION}::I18n KF${QT_MAJOR_VERSION}::Kirigami2 KF${QT_MAJOR_VERSION}::Notifications KF${QT_MAJOR_VERSION}::ConfigCore KF${QT_MAJOR_VERSION}::ConfigGui KF${QT_MAJOR_VERSION}::CoreAddons KF${QT_MAJOR_VERSION}::SonnetCore KF${QT_MAJOR_VERSION}::ItemModels Quotient${QUOTIENT_SUFFIX} cmark::cmark QCoro::Core)
|
||||
target_link_libraries(neochat PUBLIC Qt::Core Qt::Quick Qt::Qml Qt::Gui Qt::Multimedia Qt::Network Qt::QuickControls2 Qt::Xml KF${QT_MAJOR_VERSION}::I18n KF${QT_MAJOR_VERSION}::Kirigami2 KF${QT_MAJOR_VERSION}::Notifications KF${QT_MAJOR_VERSION}::ConfigCore KF${QT_MAJOR_VERSION}::ConfigGui KF${QT_MAJOR_VERSION}::CoreAddons KF${QT_MAJOR_VERSION}::SonnetCore KF${QT_MAJOR_VERSION}::ItemModels KF${QT_MAJOR_VERSION}::SyntaxHighlighting Quotient${QUOTIENT_SUFFIX} cmark::cmark QCoro::Core)
|
||||
|
||||
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
|
||||
|
||||
if(NEOCHAT_FLATPAK)
|
||||
|
||||
@@ -44,7 +44,7 @@ void ActionsHandler::handleNewMessage()
|
||||
if (!m_room->chatBoxAttachmentPath().isEmpty()) {
|
||||
QUrl url(m_room->chatBoxAttachmentPath());
|
||||
auto path = url.isLocalFile() ? url.toLocalFile() : url.toString();
|
||||
m_room->uploadFile(path, m_room->chatBoxText().isEmpty() ? path.mid(path.lastIndexOf('/') + 1) : m_room->chatBoxText());
|
||||
m_room->uploadFile(QUrl(path), m_room->chatBoxText().isEmpty() ? path.mid(path.lastIndexOf(u'/') + 1) : m_room->chatBoxText());
|
||||
m_room->setChatBoxAttachmentPath({});
|
||||
m_room->setChatBoxText({});
|
||||
return;
|
||||
@@ -97,7 +97,7 @@ QString ActionsHandler::handleMentions(QString handledText, const bool &isEdit)
|
||||
void ActionsHandler::handleMessage(const QString &text, QString handledText, const bool &isEdit)
|
||||
{
|
||||
if (NeoChatConfig::allowQuickEdit()) {
|
||||
QRegularExpression sed("^s/([^/]*)/([^/]*)(/g)?$");
|
||||
QRegularExpression sed(QStringLiteral("^s/([^/]*)/([^/]*)(/g)?$"));
|
||||
auto match = sed.match(text);
|
||||
if (match.hasMatch()) {
|
||||
const QString regex = match.captured(1);
|
||||
@@ -113,13 +113,13 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, con
|
||||
} else {
|
||||
originalString = event->plainBody();
|
||||
}
|
||||
if (flags == "/g") {
|
||||
m_room->postHtmlMessage(handledText, originalString.replace(regex, replacement), event->msgtype(), "", event->id());
|
||||
if (flags == "/g"_ls) {
|
||||
m_room->postHtmlMessage(handledText, originalString.replace(regex, replacement), event->msgtype(), {}, event->id());
|
||||
} else {
|
||||
m_room->postHtmlMessage(handledText,
|
||||
originalString.replace(originalString.indexOf(regex), regex.size(), replacement),
|
||||
event->msgtype(),
|
||||
"",
|
||||
{},
|
||||
event->id());
|
||||
}
|
||||
return;
|
||||
@@ -133,7 +133,7 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, con
|
||||
if (handledText.startsWith(QLatin1Char('/'))) {
|
||||
for (const auto &action : ActionsModel::instance().allActions()) {
|
||||
if (handledText.indexOf(action.prefix) == 1
|
||||
&& (handledText.indexOf(" ") == action.prefix.length() + 1 || handledText.length() == action.prefix.length() + 1)) {
|
||||
&& (handledText.indexOf(" "_ls) == action.prefix.length() + 1 || handledText.length() == action.prefix.length() + 1)) {
|
||||
handledText = action.handle(handledText.mid(action.prefix.length() + 1).trimmed(), m_room);
|
||||
if (action.messageType.has_value()) {
|
||||
messageType = *action.messageType;
|
||||
@@ -152,30 +152,30 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, con
|
||||
textHandler.setData(handledText);
|
||||
handledText = textHandler.handleSendText();
|
||||
|
||||
if (handledText.count("<p>") == 1 && handledText.count("</p>") == 1) {
|
||||
handledText.remove("<p>");
|
||||
handledText.remove("</p>");
|
||||
if (handledText.count("<p>"_ls) == 1 && handledText.count("</p>"_ls) == 1) {
|
||||
handledText.remove("<p>"_ls);
|
||||
handledText.remove("</p>"_ls);
|
||||
}
|
||||
|
||||
if (handledText.length() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_room->postMessage(text, handledText, messageType, m_room->chatBoxReplyId(), isEdit ? m_room->chatBoxEditId() : "");
|
||||
m_room->postMessage(text, handledText, messageType, m_room->chatBoxReplyId(), isEdit ? m_room->chatBoxEditId() : QString());
|
||||
}
|
||||
|
||||
void ActionsHandler::checkEffects(const QString &text)
|
||||
{
|
||||
std::optional<QString> effect = std::nullopt;
|
||||
if (text.contains("\u2744")) {
|
||||
if (text.contains(QStringLiteral("\u2744"))) {
|
||||
effect = QLatin1String("snowflake");
|
||||
} else if (text.contains("\u1F386")) {
|
||||
} else if (text.contains(QStringLiteral("\u1F386"))) {
|
||||
effect = QLatin1String("fireworks");
|
||||
} else if (text.contains("\u2F387")) {
|
||||
} else if (text.contains(QStringLiteral("\u2F387"))) {
|
||||
effect = QLatin1String("fireworks");
|
||||
} else if (text.contains("\u1F389")) {
|
||||
} else if (text.contains(QStringLiteral("\u1F389"))) {
|
||||
effect = QLatin1String("confetti");
|
||||
} else if (text.contains("\u1F38A")) {
|
||||
} else if (text.contains(QStringLiteral("\u1F38A"))) {
|
||||
effect = QLatin1String("confetti");
|
||||
}
|
||||
if (effect.has_value()) {
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
#include "neochatroom.h"
|
||||
|
||||
class CustomEmojiModel;
|
||||
class NeoChatRoom;
|
||||
|
||||
/**
|
||||
|
||||
@@ -218,21 +218,21 @@ void ChatDocumentHandler::setRoom(NeoChatRoom *room)
|
||||
void ChatDocumentHandler::complete(int index)
|
||||
{
|
||||
if (m_completionModel->autoCompletionType() == CompletionModel::User) {
|
||||
auto name = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Text).toString();
|
||||
auto id = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Subtitle).toString();
|
||||
auto name = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::DisplayNameRole).toString();
|
||||
auto id = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::SubtitleRole).toString();
|
||||
auto text = getText();
|
||||
auto at = text.lastIndexOf(QLatin1Char('@'), cursorPosition() - 1);
|
||||
QTextCursor cursor(document()->textDocument());
|
||||
cursor.setPosition(at);
|
||||
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
|
||||
cursor.insertText(name % " ");
|
||||
cursor.insertText(name + QStringLiteral(" "));
|
||||
cursor.setPosition(at);
|
||||
cursor.setPosition(cursor.position() + name.size(), QTextCursor::KeepAnchor);
|
||||
cursor.setKeepPositionOnInsert(true);
|
||||
pushMention({cursor, name, 0, 0, id});
|
||||
m_highlighter->rehighlight();
|
||||
} else if (m_completionModel->autoCompletionType() == CompletionModel::Command) {
|
||||
auto command = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedText).toString();
|
||||
auto command = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedTextRole).toString();
|
||||
auto text = getText();
|
||||
auto at = text.lastIndexOf(QLatin1Char('/'));
|
||||
QTextCursor cursor(document()->textDocument());
|
||||
@@ -240,20 +240,20 @@ void ChatDocumentHandler::complete(int index)
|
||||
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
|
||||
cursor.insertText(QStringLiteral("/%1 ").arg(command));
|
||||
} else if (m_completionModel->autoCompletionType() == CompletionModel::Room) {
|
||||
auto alias = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Subtitle).toString();
|
||||
auto alias = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::SubtitleRole).toString();
|
||||
auto text = getText();
|
||||
auto at = text.lastIndexOf(QLatin1Char('#'), cursorPosition() - 1);
|
||||
QTextCursor cursor(document()->textDocument());
|
||||
cursor.setPosition(at);
|
||||
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
|
||||
cursor.insertText(alias % " ");
|
||||
cursor.insertText(alias + QStringLiteral(" "));
|
||||
cursor.setPosition(at);
|
||||
cursor.setPosition(cursor.position() + alias.size(), QTextCursor::KeepAnchor);
|
||||
cursor.setKeepPositionOnInsert(true);
|
||||
pushMention({cursor, alias, 0, 0, alias});
|
||||
m_highlighter->rehighlight();
|
||||
} else if (m_completionModel->autoCompletionType() == CompletionModel::Emoji) {
|
||||
auto shortcode = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedText).toString();
|
||||
auto shortcode = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedTextRole).toString();
|
||||
auto text = getText();
|
||||
auto at = text.lastIndexOf(QLatin1Char(':'));
|
||||
QTextCursor cursor(document()->textDocument());
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#include "models/completionmodel.h"
|
||||
#include "neochatroom.h"
|
||||
|
||||
class QTextDocument;
|
||||
class NeoChatRoom;
|
||||
class SyntaxHighlighter;
|
||||
|
||||
@@ -104,12 +103,12 @@ class ChatDocumentHandler : public QObject
|
||||
/**
|
||||
* @brief The color to highlight user mentions.
|
||||
*/
|
||||
Q_PROPERTY(QColor mentionColor READ mentionColor WRITE setMentionColor NOTIFY mentionColorChanged);
|
||||
Q_PROPERTY(QColor mentionColor READ mentionColor WRITE setMentionColor NOTIFY mentionColorChanged)
|
||||
|
||||
/**
|
||||
* @brief The color to highlight spelling errors.
|
||||
*/
|
||||
Q_PROPERTY(QColor errorColor READ errorColor WRITE setErrorColor NOTIFY errorColorChanged);
|
||||
Q_PROPERTY(QColor errorColor READ errorColor WRITE setErrorColor NOTIFY errorColorChanged)
|
||||
|
||||
public:
|
||||
explicit ChatDocumentHandler(QObject *parent = nullptr);
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QGuiApplication>
|
||||
#include <QImageReader>
|
||||
#include <QNetworkProxy>
|
||||
#include <QQuickTextDocument>
|
||||
#include <QQuickWindow>
|
||||
@@ -33,17 +32,15 @@
|
||||
|
||||
#include <Quotient/accountregistry.h>
|
||||
#include <Quotient/connection.h>
|
||||
#include <Quotient/csapi/content-repo.h>
|
||||
#include <Quotient/csapi/logout.h>
|
||||
#include <Quotient/csapi/profile.h>
|
||||
#include <Quotient/jobs/downloadfilejob.h>
|
||||
#include <Quotient/qt_connection_util.h>
|
||||
#include <Quotient/csapi/notifications.h>
|
||||
#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 "neochatuser.h"
|
||||
#include "notificationsmanager.h"
|
||||
#include "roommanager.h"
|
||||
#include "windowcontroller.h"
|
||||
@@ -60,7 +57,6 @@ Controller::Controller(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
Connection::setRoomType<NeoChatRoom>();
|
||||
Connection::setUserType<NeoChatUser>();
|
||||
|
||||
setApplicationProxy();
|
||||
|
||||
@@ -101,13 +97,11 @@ Controller::Controller(QObject *parent)
|
||||
}
|
||||
#endif
|
||||
|
||||
connect(&m_accountRegistry, &AccountRegistry::accountCountChanged, this, &Controller::activeConnectionIndexChanged);
|
||||
|
||||
static int oldAccountCount = 0;
|
||||
connect(&m_accountRegistry, &AccountRegistry::accountCountChanged, this, [this]() {
|
||||
if (m_accountRegistry.size() > oldAccountCount) {
|
||||
auto connection = m_accountRegistry.accounts()[m_accountRegistry.size() - 1];
|
||||
connect(connection, &Connection::syncDone, this, [connection]() {
|
||||
auto connection = dynamic_cast<NeoChatConnection *>(m_accountRegistry.accounts()[m_accountRegistry.size() - 1]);
|
||||
connect(connection, &NeoChatConnection::syncDone, this, [connection]() {
|
||||
NotificationsManager::instance().handleNotifications(connection);
|
||||
});
|
||||
}
|
||||
@@ -125,43 +119,24 @@ Controller &Controller::instance()
|
||||
return _instance;
|
||||
}
|
||||
|
||||
void Controller::showWindow()
|
||||
void Controller::toggleWindow()
|
||||
{
|
||||
WindowController::instance().showAndRaiseWindow(QString());
|
||||
}
|
||||
|
||||
void Controller::logout(Connection *conn, bool serverSideLogout)
|
||||
{
|
||||
if (!conn) {
|
||||
qCritical() << "Attempt to logout null connection";
|
||||
return;
|
||||
}
|
||||
|
||||
SettingsGroup("Accounts").remove(conn->userId());
|
||||
|
||||
QKeychain::DeletePasswordJob job(qAppName());
|
||||
job.setAutoDelete(true);
|
||||
job.setKey(conn->userId());
|
||||
QEventLoop loop;
|
||||
QKeychain::DeletePasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
|
||||
job.start();
|
||||
loop.exec();
|
||||
|
||||
if (m_accountRegistry.count() > 1) {
|
||||
// Only set the connection if the the account being logged out is currently active
|
||||
if (conn == activeConnection()) {
|
||||
setActiveConnection(m_accountRegistry.accounts()[0]);
|
||||
auto &instance = WindowController::instance();
|
||||
auto window = instance.window();
|
||||
if (window->isVisible()) {
|
||||
if (window->windowStates() & Qt::WindowMinimized) {
|
||||
window->showNormal();
|
||||
window->requestActivate();
|
||||
} else {
|
||||
window->close();
|
||||
}
|
||||
} else {
|
||||
setActiveConnection(nullptr);
|
||||
instance.showAndRaiseWindow({});
|
||||
instance.window()->requestActivate();
|
||||
}
|
||||
if (!serverSideLogout) {
|
||||
return;
|
||||
}
|
||||
conn->logout();
|
||||
}
|
||||
|
||||
void Controller::addConnection(Connection *c)
|
||||
void Controller::addConnection(NeoChatConnection *c)
|
||||
{
|
||||
Q_ASSERT_X(c, __FUNCTION__, "Attempt to add a null connection");
|
||||
|
||||
@@ -169,17 +144,17 @@ void Controller::addConnection(Connection *c)
|
||||
|
||||
c->setLazyLoading(true);
|
||||
|
||||
connect(c, &Connection::syncDone, this, [this, c] {
|
||||
connect(c, &NeoChatConnection::syncDone, this, [this, c] {
|
||||
Q_EMIT syncDone();
|
||||
|
||||
c->sync(30000);
|
||||
c->saveState();
|
||||
});
|
||||
connect(c, &Connection::loggedOut, this, [this, c] {
|
||||
connect(c, &NeoChatConnection::loggedOut, this, [this, c] {
|
||||
dropConnection(c);
|
||||
});
|
||||
|
||||
connect(c, &Connection::requestFailed, this, [this](BaseJob *job) {
|
||||
connect(c, &NeoChatConnection::requestFailed, this, [this](BaseJob *job) {
|
||||
if (job->error() == BaseJob::UserConsentRequired) {
|
||||
Q_EMIT userConsentRequired(job->errorUrl());
|
||||
}
|
||||
@@ -188,20 +163,19 @@ void Controller::addConnection(Connection *c)
|
||||
c->sync();
|
||||
|
||||
Q_EMIT connectionAdded(c);
|
||||
Q_EMIT accountCountChanged();
|
||||
}
|
||||
|
||||
void Controller::dropConnection(Connection *c)
|
||||
void Controller::dropConnection(NeoChatConnection *c)
|
||||
{
|
||||
Q_ASSERT_X(c, __FUNCTION__, "Attempt to drop a null connection");
|
||||
|
||||
m_accountRegistry.drop(c);
|
||||
Q_EMIT connectionDropped(c);
|
||||
Q_EMIT accountCountChanged();
|
||||
}
|
||||
|
||||
void Controller::invokeLogin()
|
||||
{
|
||||
const auto accounts = SettingsGroup("Accounts").childGroups();
|
||||
const auto accounts = SettingsGroup("Accounts"_ls).childGroups();
|
||||
QString id = NeoChatConfig::self()->activeConnection();
|
||||
for (const auto &accountId : accounts) {
|
||||
AccountSettings account{accountId};
|
||||
@@ -215,36 +189,36 @@ void Controller::invokeLogin()
|
||||
AccountSettings account{accountId};
|
||||
QString accessToken;
|
||||
if (accessTokenLoadingJob->error() == QKeychain::Error::NoError) {
|
||||
accessToken = accessTokenLoadingJob->binaryData();
|
||||
accessToken = QString::fromLatin1(accessTokenLoadingJob->binaryData());
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
auto connection = new Connection(account.homeserver());
|
||||
connect(connection, &Connection::connected, this, [this, connection, id] {
|
||||
auto connection = new NeoChatConnection(account.homeserver());
|
||||
connect(connection, &NeoChatConnection::connected, this, [this, connection, id] {
|
||||
connection->loadState();
|
||||
addConnection(connection);
|
||||
if (connection->userId() == id) {
|
||||
setActiveConnection(connection);
|
||||
connectSingleShot(connection, &Connection::syncDone, this, &Controller::initiated);
|
||||
connectSingleShot(connection, &NeoChatConnection::syncDone, this, &Controller::initiated);
|
||||
}
|
||||
});
|
||||
connect(connection, &Connection::loginError, this, [this, connection](const QString &error, const QString &) {
|
||||
if (error == "Unrecognised access token") {
|
||||
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"));
|
||||
logout(connection, false);
|
||||
} else if (error == "Connection closed") {
|
||||
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));
|
||||
logout(connection, true);
|
||||
connection->logout(true);
|
||||
}
|
||||
Q_EMIT initiated();
|
||||
});
|
||||
connect(connection, &Connection::networkError, this, [this](const QString &error, const QString &, int, int) {
|
||||
connect(connection, &NeoChatConnection::networkError, this, [this](const QString &error, const QString &, int, int) {
|
||||
Q_EMIT errorOccured(i18n("Network Error: %1", error));
|
||||
});
|
||||
connection->assumeIdentity(account.userId(), accessToken);
|
||||
@@ -310,100 +284,26 @@ bool Controller::saveAccessTokenToKeyChain(const AccountSettings &account, const
|
||||
return true;
|
||||
}
|
||||
|
||||
void Controller::changeAvatar(Connection *conn, const QUrl &localFile)
|
||||
{
|
||||
auto job = conn->uploadFile(localFile.toLocalFile());
|
||||
connect(job, &BaseJob::success, this, [conn, job] {
|
||||
conn->callApi<SetAvatarUrlJob>(conn->userId(), job->contentUri());
|
||||
});
|
||||
}
|
||||
|
||||
void Controller::markAllMessagesAsRead(Connection *conn)
|
||||
{
|
||||
const auto rooms = conn->allRooms();
|
||||
for (auto room : rooms) {
|
||||
room->markAllMessagesAsRead();
|
||||
}
|
||||
}
|
||||
|
||||
bool Controller::supportSystemTray() const
|
||||
{
|
||||
#ifdef Q_OS_ANDROID
|
||||
return false;
|
||||
#else
|
||||
QString de = getenv("XDG_CURRENT_DESKTOP");
|
||||
auto de = QString::fromLatin1(qgetenv("XDG_CURRENT_DESKTOP"));
|
||||
return de != QStringLiteral("GNOME") && de != QStringLiteral("Pantheon");
|
||||
#endif
|
||||
}
|
||||
|
||||
void Controller::changePassword(Connection *connection, const QString ¤tPassword, const QString &newPassword)
|
||||
{
|
||||
NeochatChangePasswordJob *job = connection->callApi<NeochatChangePasswordJob>(newPassword, false);
|
||||
connect(job, &BaseJob::result, this, [this, job, currentPassword, newPassword, connection] {
|
||||
if (job->error() == 103) {
|
||||
QJsonObject replyData = job->jsonData();
|
||||
QJsonObject authData;
|
||||
authData["session"] = replyData["session"];
|
||||
authData["password"] = currentPassword;
|
||||
authData["type"] = "m.login.password";
|
||||
authData["user"] = connection->user()->id();
|
||||
QJsonObject identifier = {{"type", "m.id.user"}, {"user", connection->user()->id()}};
|
||||
authData["identifier"] = identifier;
|
||||
NeochatChangePasswordJob *innerJob = connection->callApi<NeochatChangePasswordJob>(newPassword, false, authData);
|
||||
connect(innerJob, &BaseJob::success, this, [this]() {
|
||||
Q_EMIT passwordStatus(PasswordStatus::Success);
|
||||
});
|
||||
connect(innerJob, &BaseJob::failure, this, [innerJob, this]() {
|
||||
if (innerJob->jsonData()["errcode"] == "M_FORBIDDEN") {
|
||||
Q_EMIT passwordStatus(PasswordStatus::Wrong);
|
||||
} else {
|
||||
Q_EMIT passwordStatus(PasswordStatus::Other);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool Controller::setAvatar(Connection *connection, const QUrl &avatarSource)
|
||||
{
|
||||
User *localUser = connection->user();
|
||||
QString decoded = avatarSource.path();
|
||||
if (decoded.isEmpty()) {
|
||||
connection->callApi<SetAvatarUrlJob>(localUser->id(), avatarSource);
|
||||
return true;
|
||||
}
|
||||
if (QImageReader(decoded).read().isNull()) {
|
||||
return false;
|
||||
} else {
|
||||
return localUser->setAvatar(decoded);
|
||||
}
|
||||
}
|
||||
|
||||
NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Omittable<QJsonObject> &auth)
|
||||
: BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), "/_matrix/client/r0/account/password")
|
||||
{
|
||||
QJsonObject _data;
|
||||
addParam<>(_data, QStringLiteral("new_password"), newPassword);
|
||||
addParam<IfNotEmpty>(_data, QStringLiteral("logout_devices"), logoutDevices);
|
||||
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
|
||||
setRequestData(_data);
|
||||
}
|
||||
|
||||
int Controller::accountCount() const
|
||||
{
|
||||
return m_accountRegistry.count();
|
||||
}
|
||||
|
||||
void Controller::setQuitOnLastWindowClosed()
|
||||
{
|
||||
#ifndef Q_OS_ANDROID
|
||||
if (NeoChatConfig::self()->systemTray()) {
|
||||
m_trayIcon = new TrayIcon(this);
|
||||
m_trayIcon->show();
|
||||
connect(m_trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
|
||||
connect(m_trayIcon, &TrayIcon::toggleWindow, this, &Controller::toggleWindow);
|
||||
} else {
|
||||
if (m_trayIcon) {
|
||||
disconnect(m_trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
|
||||
disconnect(m_trayIcon, &TrayIcon::toggleWindow, this, &Controller::toggleWindow);
|
||||
delete m_trayIcon;
|
||||
m_trayIcon = nullptr;
|
||||
}
|
||||
@@ -414,7 +314,7 @@ void Controller::setQuitOnLastWindowClosed()
|
||||
#endif
|
||||
}
|
||||
|
||||
Connection *Controller::activeConnection() const
|
||||
NeoChatConnection *Controller::activeConnection() const
|
||||
{
|
||||
if (m_connection.isNull()) {
|
||||
return nullptr;
|
||||
@@ -422,49 +322,43 @@ Connection *Controller::activeConnection() const
|
||||
return m_connection;
|
||||
}
|
||||
|
||||
void Controller::setActiveConnection(Connection *connection)
|
||||
void Controller::setActiveConnection(NeoChatConnection *connection)
|
||||
{
|
||||
if (connection == m_connection) {
|
||||
return;
|
||||
}
|
||||
if (m_connection != nullptr) {
|
||||
disconnect(m_connection, &Connection::syncError, this, nullptr);
|
||||
disconnect(m_connection, &Connection::accountDataChanged, this, nullptr);
|
||||
disconnect(m_connection, &NeoChatConnection::syncError, this, nullptr);
|
||||
disconnect(m_connection, &NeoChatConnection::accountDataChanged, this, nullptr);
|
||||
}
|
||||
m_connection = connection;
|
||||
if (connection != nullptr) {
|
||||
NeoChatConfig::self()->setActiveConnection(connection->userId());
|
||||
connect(connection, &Connection::networkError, this, [this]() {
|
||||
connect(connection, &NeoChatConnection::networkError, this, [this]() {
|
||||
if (!m_isOnline) {
|
||||
return;
|
||||
}
|
||||
m_isOnline = false;
|
||||
Q_EMIT isOnlineChanged(false);
|
||||
});
|
||||
connect(connection, &Connection::syncDone, this, [this] {
|
||||
connect(connection, &NeoChatConnection::syncDone, this, [this] {
|
||||
if (m_isOnline) {
|
||||
return;
|
||||
}
|
||||
m_isOnline = true;
|
||||
Q_EMIT isOnlineChanged(true);
|
||||
});
|
||||
connect(connection, &Connection::requestFailed, this, [](BaseJob *job) {
|
||||
if (dynamic_cast<DownloadFileJob *>(job) && job->jsonData()["errcode"].toString() == "M_TOO_LARGE"_ls) {
|
||||
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."));
|
||||
}
|
||||
});
|
||||
connect(connection, &Connection::accountDataChanged, this, [this](const QString &type) {
|
||||
if (type == QLatin1String("org.kde.neochat.account_label")) {
|
||||
Q_EMIT activeAccountLabelChanged();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
NeoChatConfig::self()->setActiveConnection(QString());
|
||||
}
|
||||
NeoChatConfig::self()->save();
|
||||
Q_EMIT activeConnectionChanged();
|
||||
Q_EMIT activeConnectionIndexChanged();
|
||||
Q_EMIT activeAccountLabelChanged();
|
||||
}
|
||||
|
||||
PushRuleModel *Controller::pushRuleModel() const
|
||||
@@ -477,44 +371,6 @@ void Controller::saveWindowGeometry()
|
||||
WindowController::instance().saveGeometry();
|
||||
}
|
||||
|
||||
NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Omittable<QJsonObject> &auth)
|
||||
: Quotient::BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), QStringLiteral("/_matrix/client/r0/devices/%1").arg(deviceId).toLatin1())
|
||||
{
|
||||
QJsonObject _data;
|
||||
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
|
||||
setRequestData(std::move(_data));
|
||||
}
|
||||
|
||||
void Controller::createRoom(const QString &name, const QString &topic)
|
||||
{
|
||||
auto createRoomJob = m_connection->createRoom(Connection::PublishRoom, "", name, topic, QStringList());
|
||||
connect(createRoomJob, &CreateRoomJob::failure, this, [this, createRoomJob] {
|
||||
Q_EMIT errorOccured(i18n("Room creation failed: %1", createRoomJob->errorString()));
|
||||
});
|
||||
connectSingleShot(this, &Controller::roomAdded, &RoomManager::instance(), &RoomManager::enterRoom, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void Controller::createSpace(const QString &name, const QString &topic)
|
||||
{
|
||||
auto createRoomJob = m_connection->createRoom(Connection::UnpublishRoom,
|
||||
{},
|
||||
name,
|
||||
topic,
|
||||
QStringList(),
|
||||
{},
|
||||
{},
|
||||
false,
|
||||
{},
|
||||
{},
|
||||
QJsonObject{
|
||||
{"type"_ls, "m.space"_ls},
|
||||
});
|
||||
connect(createRoomJob, &CreateRoomJob::failure, this, [this, createRoomJob] {
|
||||
Q_EMIT errorOccured(i18n("Space creation failed: %1", createRoomJob->errorString()));
|
||||
});
|
||||
connectSingleShot(this, &Controller::roomAdded, &RoomManager::instance(), &RoomManager::enterRoom, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
bool Controller::isOnline() const
|
||||
{
|
||||
return m_isOnline;
|
||||
@@ -523,16 +379,16 @@ bool Controller::isOnline() const
|
||||
// TODO: Remove in favor of RoomManager::joinRoom
|
||||
void Controller::joinRoom(const QString &alias)
|
||||
{
|
||||
if (!alias.contains(":")) {
|
||||
if (!alias.contains(":"_ls)) {
|
||||
Q_EMIT errorOccured(i18n("The room id you are trying to join is not valid"));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto knownServer = alias.mid(alias.indexOf(":") + 1);
|
||||
const auto knownServer = alias.mid(alias.indexOf(":"_ls) + 1);
|
||||
RoomManager::instance().joinRoom(m_connection, alias, QStringList{knownServer});
|
||||
}
|
||||
|
||||
void Controller::openOrCreateDirectChat(NeoChatUser *user)
|
||||
void Controller::openOrCreateDirectChat(User *user)
|
||||
{
|
||||
const auto existing = activeConnection()->directChats();
|
||||
|
||||
@@ -582,11 +438,6 @@ bool Controller::hasWindowSystem() const
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Controller::encryptionSupported() const
|
||||
{
|
||||
return Quotient::encryptionSupported();
|
||||
}
|
||||
|
||||
void Controller::forceRefreshTextDocument(QQuickTextDocument *textDocument, QQuickItem *item)
|
||||
{
|
||||
// HACK: Workaround bug QTBUG 93281
|
||||
@@ -631,12 +482,6 @@ int Controller::activeConnectionIndex() const
|
||||
return result - m_accountRegistry.accounts().begin();
|
||||
}
|
||||
|
||||
int Controller::quotientMinorVersion() const
|
||||
{
|
||||
// TODO libQuotient 0.7: Replace with version function from libQuotient
|
||||
return 7;
|
||||
}
|
||||
|
||||
bool Controller::isFlatpak() const
|
||||
{
|
||||
#ifdef NEOCHAT_FLATPAK
|
||||
@@ -646,41 +491,6 @@ bool Controller::isFlatpak() const
|
||||
#endif
|
||||
}
|
||||
|
||||
void Controller::setActiveAccountLabel(const QString &label)
|
||||
{
|
||||
if (!m_connection) {
|
||||
return;
|
||||
}
|
||||
QJsonObject json{
|
||||
{"account_label", label},
|
||||
};
|
||||
m_connection->setAccountData("org.kde.neochat.account_label", json);
|
||||
}
|
||||
|
||||
QString Controller::activeAccountLabel() const
|
||||
{
|
||||
if (!m_connection) {
|
||||
return {};
|
||||
}
|
||||
return m_connection->accountDataJson("org.kde.neochat.account_label")["account_label"].toString();
|
||||
}
|
||||
|
||||
QVariantList Controller::getSupportedRoomVersions(Quotient::Connection *connection)
|
||||
{
|
||||
auto roomVersions = connection->availableRoomVersions();
|
||||
|
||||
QVariantList supportedRoomVersions;
|
||||
for (const Quotient::Connection::SupportedRoomVersion &v : roomVersions) {
|
||||
QVariantMap roomVersionMap;
|
||||
roomVersionMap.insert("id", v.id);
|
||||
roomVersionMap.insert("status", v.status);
|
||||
roomVersionMap.insert("isStable", v.isStable());
|
||||
supportedRoomVersions.append(roomVersionMap);
|
||||
}
|
||||
|
||||
return supportedRoomVersions;
|
||||
}
|
||||
|
||||
AccountRegistry &Controller::accounts()
|
||||
{
|
||||
return m_accountRegistry;
|
||||
|
||||
117
src/controller.h
117
src/controller.h
@@ -9,20 +9,19 @@
|
||||
|
||||
#include <KFormat>
|
||||
|
||||
#include "neochatconnection.h"
|
||||
#include <Quotient/accountregistry.h>
|
||||
#include <Quotient/jobs/basejob.h>
|
||||
#include <Quotient/settings.h>
|
||||
|
||||
class NeoChatRoom;
|
||||
class NeoChatUser;
|
||||
class TrayIcon;
|
||||
class QWindow;
|
||||
class QQuickTextDocument;
|
||||
|
||||
namespace Quotient
|
||||
{
|
||||
class Connection;
|
||||
class Room;
|
||||
class User;
|
||||
}
|
||||
|
||||
namespace QKeychain
|
||||
@@ -42,15 +41,10 @@ class Controller : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
/**
|
||||
* @brief The number of logged in accounts.
|
||||
*/
|
||||
Q_PROPERTY(int accountCount READ accountCount NOTIFY accountCountChanged)
|
||||
|
||||
/**
|
||||
* @brief The current connection for the rest of NeoChat to use.
|
||||
*/
|
||||
Q_PROPERTY(Quotient::Connection *activeConnection READ activeConnection WRITE setActiveConnection NOTIFY activeConnectionChanged)
|
||||
Q_PROPERTY(NeoChatConnection *activeConnection READ activeConnection WRITE setActiveConnection NOTIFY activeConnectionChanged)
|
||||
|
||||
/**
|
||||
* @brief The PushRuleModel that has the active connection's push rules.
|
||||
@@ -62,16 +56,6 @@ class Controller : public QObject
|
||||
*/
|
||||
Q_PROPERTY(int activeConnectionIndex READ activeConnectionIndex NOTIFY activeConnectionIndexChanged)
|
||||
|
||||
/**
|
||||
* @brief The account label for the active account.
|
||||
*
|
||||
* Account labels are a concept specific to NeoChat, allowing accounts to be
|
||||
* labelled, e.g. for "Work", "Private", etc.
|
||||
*
|
||||
* Set to an empty string to remove the label.
|
||||
*/
|
||||
Q_PROPERTY(QString activeAccountLabel READ activeAccountLabel WRITE setActiveAccountLabel NOTIFY activeAccountLabelChanged)
|
||||
|
||||
/**
|
||||
* @brief Whether the OS NeoChat is running on supports sytem tray icons.
|
||||
*/
|
||||
@@ -87,20 +71,6 @@ class Controller : public QObject
|
||||
*/
|
||||
Q_PROPERTY(bool isOnline READ isOnline NOTIFY isOnlineChanged)
|
||||
|
||||
/**
|
||||
* @brief Whether the ecryption support has been enabled.
|
||||
*/
|
||||
Q_PROPERTY(bool encryptionSupported READ encryptionSupported CONSTANT)
|
||||
|
||||
/**
|
||||
* @brief The current minor version number of libQuotient being used.
|
||||
*
|
||||
* This is the only way to gate NeoChat features by libQuotient version in QML.
|
||||
*
|
||||
* @note No major version because libQuotient doesn't have any; All are 0.x.
|
||||
*/
|
||||
Q_PROPERTY(int quotientMinorVersion READ quotientMinorVersion CONSTANT)
|
||||
|
||||
/**
|
||||
* @brief Whether NeoChat is running as a flatpak.
|
||||
*
|
||||
@@ -121,58 +91,28 @@ public:
|
||||
|
||||
static Controller &instance();
|
||||
|
||||
[[nodiscard]] int accountCount() const;
|
||||
|
||||
void setActiveConnection(Quotient::Connection *connection);
|
||||
[[nodiscard]] Quotient::Connection *activeConnection() const;
|
||||
void setActiveConnection(NeoChatConnection *connection);
|
||||
[[nodiscard]] NeoChatConnection *activeConnection() const;
|
||||
|
||||
[[nodiscard]] PushRuleModel *pushRuleModel() const;
|
||||
|
||||
/**
|
||||
* @brief Add a new connection to the account registry.
|
||||
*/
|
||||
void addConnection(Quotient::Connection *c);
|
||||
void addConnection(NeoChatConnection *c);
|
||||
|
||||
/**
|
||||
* @brief Drop a connection from the account registry.
|
||||
*/
|
||||
void dropConnection(Quotient::Connection *c);
|
||||
void dropConnection(NeoChatConnection *c);
|
||||
|
||||
int activeConnectionIndex() const;
|
||||
|
||||
[[nodiscard]] QString activeAccountLabel() const;
|
||||
void setActiveAccountLabel(const QString &label);
|
||||
|
||||
/**
|
||||
* @brief Save an access token to the keychain for the given account.
|
||||
*/
|
||||
bool saveAccessTokenToKeyChain(const Quotient::AccountSettings &account, const QByteArray &accessToken);
|
||||
|
||||
/**
|
||||
* @brief Change the password for an account.
|
||||
*
|
||||
* The function emits a passwordStatus signal with a PasswordStatus value when
|
||||
* complete.
|
||||
*
|
||||
* @sa PasswordStatus, passwordStatus
|
||||
*/
|
||||
Q_INVOKABLE void changePassword(Quotient::Connection *connection, const QString ¤tPassword, const QString &newPassword);
|
||||
|
||||
/**
|
||||
* @brief Change the avatar for an account.
|
||||
*/
|
||||
Q_INVOKABLE bool setAvatar(Quotient::Connection *connection, const QUrl &avatarSource);
|
||||
|
||||
/**
|
||||
* @brief Create new room for a group chat.
|
||||
*/
|
||||
Q_INVOKABLE void createRoom(const QString &name, const QString &topic);
|
||||
|
||||
/**
|
||||
* @brief Create new space.
|
||||
*/
|
||||
Q_INVOKABLE void createSpace(const QString &name, const QString &topic);
|
||||
|
||||
/**
|
||||
* @brief Join a room.
|
||||
*/
|
||||
@@ -183,7 +123,7 @@ public:
|
||||
*
|
||||
* If a direct chat with the user doesn't exist one is created and then joined.
|
||||
*/
|
||||
Q_INVOKABLE void openOrCreateDirectChat(NeoChatUser *user);
|
||||
Q_INVOKABLE void openOrCreateDirectChat(Quotient::User *user);
|
||||
|
||||
[[nodiscard]] bool supportSystemTray() const;
|
||||
|
||||
@@ -194,8 +134,6 @@ public:
|
||||
|
||||
bool isOnline() const;
|
||||
|
||||
bool encryptionSupported() const;
|
||||
|
||||
/**
|
||||
* @brief Sets the QNetworkProxy for the application.
|
||||
*
|
||||
@@ -203,8 +141,6 @@ public:
|
||||
*/
|
||||
Q_INVOKABLE void setApplicationProxy();
|
||||
|
||||
int quotientMinorVersion() const;
|
||||
|
||||
bool isFlatpak() const;
|
||||
|
||||
/**
|
||||
@@ -228,14 +164,12 @@ public:
|
||||
*/
|
||||
Q_INVOKABLE void forceRefreshTextDocument(QQuickTextDocument *textDocument, QQuickItem *item);
|
||||
|
||||
Q_INVOKABLE QVariantList getSupportedRoomVersions(Quotient::Connection *connection);
|
||||
|
||||
Quotient::AccountRegistry &accounts();
|
||||
|
||||
private:
|
||||
explicit Controller(QObject *parent = nullptr);
|
||||
|
||||
QPointer<Quotient::Connection> m_connection;
|
||||
QPointer<NeoChatConnection> m_connection;
|
||||
TrayIcon *m_trayIcon = nullptr;
|
||||
|
||||
QKeychain::ReadPasswordJob *loadAccessTokenFromKeyChain(const Quotient::AccountSettings &account);
|
||||
@@ -252,7 +186,7 @@ private:
|
||||
|
||||
private Q_SLOTS:
|
||||
void invokeLogin();
|
||||
void showWindow();
|
||||
void toggleWindow();
|
||||
void setQuitOnLastWindowClosed();
|
||||
|
||||
Q_SIGNALS:
|
||||
@@ -262,42 +196,17 @@ Q_SIGNALS:
|
||||
/// Error occurred because of server or bug in NeoChat
|
||||
void globalErrorOccured(QString error, QString detail);
|
||||
void syncDone();
|
||||
void connectionAdded(Quotient::Connection *_t1);
|
||||
void connectionDropped(Quotient::Connection *_t1);
|
||||
void accountCountChanged();
|
||||
void connectionAdded(NeoChatConnection *connection);
|
||||
void connectionDropped(NeoChatConnection *connection);
|
||||
void initiated();
|
||||
void notificationClicked(const QString &_t1, const QString &_t2);
|
||||
void quitOnLastWindowClosedChanged();
|
||||
void unreadCountChanged();
|
||||
void activeConnectionChanged();
|
||||
void passwordStatus(Controller::PasswordStatus _t1);
|
||||
void passwordStatus(Controller::PasswordStatus status);
|
||||
void userConsentRequired(QUrl url);
|
||||
void testConnectionResult(const QString &connection, bool usable);
|
||||
void isOnlineChanged(bool isOnline);
|
||||
void keyVerificationRequest(int timeLeft, Quotient::Connection *connection, const QString &transactionId, const QString &deviceId);
|
||||
void keyVerificationStart();
|
||||
void keyVerificationAccept(const QString &commitment);
|
||||
void keyVerificationKey(const QString &sas);
|
||||
void activeConnectionIndexChanged();
|
||||
void roomAdded(NeoChatRoom *room);
|
||||
void activeAccountLabelChanged();
|
||||
|
||||
public Q_SLOTS:
|
||||
void logout(Quotient::Connection *conn, bool serverSideLogout);
|
||||
void changeAvatar(Quotient::Connection *conn, const QUrl &localFile);
|
||||
static void markAllMessagesAsRead(Quotient::Connection *conn);
|
||||
void saveWindowGeometry();
|
||||
};
|
||||
|
||||
// TODO libQuotient 0.7: Drop
|
||||
class NeochatChangePasswordJob : public Quotient::BaseJob
|
||||
{
|
||||
public:
|
||||
explicit NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
|
||||
};
|
||||
|
||||
class NeochatDeleteDeviceJob : public Quotient::BaseJob
|
||||
{
|
||||
public:
|
||||
explicit NeochatDeleteDeviceJob(const QString &deviceId, const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
|
||||
};
|
||||
|
||||
@@ -124,7 +124,7 @@ int DelegateSizeHelper::calculateCurrentPercentageWidth() const
|
||||
int maxPercentWidth = endPercentBigger ? m_endPercentWidth : m_startPercentWidth;
|
||||
int minPercentWidth = endPercentBigger ? m_startPercentWidth : m_endPercentWidth;
|
||||
|
||||
int calcPercentWidth = std::ceil(m * m_parentWidth + c);
|
||||
int calcPercentWidth = std::round(m * m_parentWidth + c);
|
||||
return std::clamp(calcPercentWidth, minPercentWidth, maxPercentWidth);
|
||||
}
|
||||
|
||||
@@ -146,9 +146,9 @@ qreal DelegateSizeHelper::currentWidth() const
|
||||
|
||||
qreal absoluteWidth = m_parentWidth * percentWidth * 0.01;
|
||||
if (m_maxWidth < 0.0) {
|
||||
return std::ceil(absoluteWidth);
|
||||
return std::round(absoluteWidth);
|
||||
} else {
|
||||
return std::ceil(std::min(absoluteWidth, m_maxWidth));
|
||||
return std::round(std::min(absoluteWidth, m_maxWidth));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
43
src/enums/delegatetype.h
Normal file
43
src/enums/delegatetype.h
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
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
/**
|
||||
* @class DelegateType
|
||||
*
|
||||
* This class is designed to define the DelegateType enumeration.
|
||||
*/
|
||||
class DelegateType
|
||||
{
|
||||
Q_GADGET
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief The type of delegate that is needed for the event.
|
||||
*
|
||||
* @note While similar this is not the matrix event or message type. This is
|
||||
* to tell a QML ListView what delegate to show for each event. So while
|
||||
* similar to the spec it is not the same.
|
||||
*/
|
||||
enum Type {
|
||||
Emote, /**< A message that begins with /me. */
|
||||
Notice, /**< A notice event. */
|
||||
Image, /**< A message that is an image. */
|
||||
Audio, /**< A message that is an audio recording. */
|
||||
Video, /**< A message that is a video. */
|
||||
File, /**< A message that is a file. */
|
||||
Message, /**< A text message. */
|
||||
Sticker, /**< A message that is a sticker. */
|
||||
State, /**< A state event in the room. */
|
||||
Encrypted, /**< An encrypted message that cannot be decrypted. */
|
||||
ReadMarker, /**< The local user read marker. */
|
||||
Poll, /**< The initial event for a poll. */
|
||||
Location, /**< A location event. */
|
||||
LiveLocation, /**< The initial event of a shared live location (i.e., the place where this is supposed to be shown in the timeline). */
|
||||
Other, /**< Anything that cannot be classified as another type. */
|
||||
};
|
||||
Q_ENUM(Type);
|
||||
};
|
||||
988
src/eventhandler.cpp
Normal file
988
src/eventhandler.cpp
Normal file
@@ -0,0 +1,988 @@
|
||||
// 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 "eventhandler.h"
|
||||
|
||||
#include <QMovie>
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include <Quotient/eventitem.h>
|
||||
#include <Quotient/events/encryptionevent.h>
|
||||
#include <Quotient/events/reactionevent.h>
|
||||
#include <Quotient/events/redactionevent.h>
|
||||
#include <Quotient/events/roomavatarevent.h>
|
||||
#include <Quotient/events/roomcanonicalaliasevent.h>
|
||||
#include <Quotient/events/roommemberevent.h>
|
||||
#include <Quotient/events/roompowerlevelsevent.h>
|
||||
#include <Quotient/events/simplestateevents.h>
|
||||
#include <Quotient/events/stickerevent.h>
|
||||
#include <Quotient/quotient_common.h>
|
||||
|
||||
#include "eventhandler_logging.h"
|
||||
#include "events/pollevent.h"
|
||||
#include "linkpreviewer.h"
|
||||
#include "models/reactionmodel.h"
|
||||
#include "neochatconfig.h"
|
||||
#include "neochatroom.h"
|
||||
#include "texthandler.h"
|
||||
#include "utils.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
const NeoChatRoom *EventHandler::getRoom() const
|
||||
{
|
||||
return m_room;
|
||||
}
|
||||
|
||||
void EventHandler::setRoom(const NeoChatRoom *room)
|
||||
{
|
||||
if (room == m_room) {
|
||||
return;
|
||||
}
|
||||
m_room = room;
|
||||
}
|
||||
|
||||
const Quotient::Event *EventHandler::getEvent() const
|
||||
{
|
||||
return m_event;
|
||||
}
|
||||
|
||||
void EventHandler::setEvent(const Quotient::RoomEvent *event)
|
||||
{
|
||||
if (event == m_event) {
|
||||
return;
|
||||
}
|
||||
m_event = event;
|
||||
}
|
||||
|
||||
QString EventHandler::getId() const
|
||||
{
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getId called with m_event set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
|
||||
return !m_event->id().isEmpty() ? m_event->id() : m_event->transactionId();
|
||||
}
|
||||
|
||||
DelegateType::Type EventHandler::getDelegateTypeForEvent(const Quotient::RoomEvent *event) const
|
||||
{
|
||||
if (auto e = eventCast<const RoomMessageEvent>(event)) {
|
||||
switch (e->msgtype()) {
|
||||
case MessageEventType::Emote:
|
||||
return DelegateType::Emote;
|
||||
case MessageEventType::Notice:
|
||||
return DelegateType::Notice;
|
||||
case MessageEventType::Image:
|
||||
return DelegateType::Image;
|
||||
case MessageEventType::Audio:
|
||||
return DelegateType::Audio;
|
||||
case MessageEventType::Video:
|
||||
return DelegateType::Video;
|
||||
case MessageEventType::Location:
|
||||
return DelegateType::Location;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (e->hasFileContent()) {
|
||||
return DelegateType::File;
|
||||
}
|
||||
|
||||
return DelegateType::Message;
|
||||
}
|
||||
if (is<const StickerEvent>(*event)) {
|
||||
return DelegateType::Sticker;
|
||||
}
|
||||
if (event->isStateEvent()) {
|
||||
if (event->matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) {
|
||||
return DelegateType::LiveLocation;
|
||||
}
|
||||
return DelegateType::State;
|
||||
}
|
||||
if (is<const EncryptedEvent>(*event)) {
|
||||
return DelegateType::Encrypted;
|
||||
}
|
||||
if (is<PollStartEvent>(*event)) {
|
||||
const auto pollEvent = eventCast<const PollStartEvent>(event);
|
||||
if (pollEvent->isRedacted()) {
|
||||
return DelegateType::Message;
|
||||
}
|
||||
return DelegateType::Poll;
|
||||
}
|
||||
|
||||
return DelegateType::Other;
|
||||
}
|
||||
|
||||
DelegateType::Type EventHandler::getDelegateType() const
|
||||
{
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getDelegateType called with m_event set to nullptr.";
|
||||
return DelegateType::Other;
|
||||
}
|
||||
|
||||
return getDelegateTypeForEvent(m_event);
|
||||
}
|
||||
|
||||
QVariantMap EventHandler::getAuthor(bool isPending) const
|
||||
{
|
||||
if (m_room == nullptr) {
|
||||
qCWarning(EventHandling) << "getAuthor called with m_room set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
// If we have a room we can return an empty user by handing nullptr to m_room->getUser.
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getAuthor called with m_event set to nullptr. Returning empty user.";
|
||||
return m_room->getUser(nullptr);
|
||||
}
|
||||
|
||||
const auto author = isPending ? m_room->localUser() : m_room->user(m_event->senderId());
|
||||
return m_room->getUser(author);
|
||||
}
|
||||
|
||||
QString EventHandler::getAuthorDisplayName(bool isPending) const
|
||||
{
|
||||
if (m_room == nullptr) {
|
||||
qCWarning(EventHandling) << "getAuthorDisplayName called with m_room set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getAuthorDisplayName called with m_event set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
|
||||
if (is<RoomMemberEvent>(*m_event) && !m_event->unsignedJson()[QStringLiteral("prev_content")][QStringLiteral("displayname")].isNull()
|
||||
&& m_event->stateKey() == m_event->senderId()) {
|
||||
auto previousDisplayName = m_event->unsignedJson()[QStringLiteral("prev_content")][QStringLiteral("displayname")].toString().toHtmlEscaped();
|
||||
if (previousDisplayName.isEmpty()) {
|
||||
previousDisplayName = m_event->senderId();
|
||||
}
|
||||
return previousDisplayName;
|
||||
} else {
|
||||
const auto author = isPending ? m_room->localUser() : m_room->user(m_event->senderId());
|
||||
return m_room->htmlSafeMemberName(author->id());
|
||||
}
|
||||
}
|
||||
|
||||
QDateTime EventHandler::getTime(bool isPending, QDateTime lastUpdated) const
|
||||
{
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getTime called with m_event set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
if (isPending && lastUpdated == QDateTime()) {
|
||||
qCWarning(EventHandling) << "a value must be provided for lastUpdated for a pending event.";
|
||||
return {};
|
||||
}
|
||||
|
||||
return isPending ? lastUpdated : m_event->originTimestamp();
|
||||
}
|
||||
|
||||
QString EventHandler::getTimeString(bool relative, QLocale::FormatType format, bool isPending, QDateTime lastUpdated) const
|
||||
{
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getTime called with m_event set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
if (isPending && lastUpdated == QDateTime()) {
|
||||
qCWarning(EventHandling) << "a value must be provided for lastUpdated for a pending event.";
|
||||
return {};
|
||||
}
|
||||
|
||||
auto ts = getTime(isPending, lastUpdated);
|
||||
if (ts.isValid()) {
|
||||
if (relative) {
|
||||
return m_format.formatRelativeDate(ts.toLocalTime().date(), format);
|
||||
} else {
|
||||
return QLocale().toString(ts.toLocalTime().time(), format);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool EventHandler::isHighlighted()
|
||||
{
|
||||
if (m_room == nullptr) {
|
||||
qCWarning(EventHandling) << "isHighlighted called with m_room set to nullptr.";
|
||||
return false;
|
||||
}
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "isHighlighted called with m_event set to nullptr.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return !m_room->isDirectChat() && m_room->isEventHighlighted(m_event);
|
||||
}
|
||||
|
||||
bool EventHandler::isHidden()
|
||||
{
|
||||
if (m_event->isStateEvent() && !NeoChatConfig::self()->showStateEvent()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (auto roomMemberEvent = eventCast<const RoomMemberEvent>(m_event)) {
|
||||
if ((roomMemberEvent->isJoin() || roomMemberEvent->isLeave()) && !NeoChatConfig::self()->showLeaveJoinEvent()) {
|
||||
return true;
|
||||
} else if (roomMemberEvent->isRename() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::self()->showRename()) {
|
||||
return true;
|
||||
} else if (roomMemberEvent->isAvatarUpdate() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave()
|
||||
&& !NeoChatConfig::self()->showAvatarUpdate()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_event->isStateEvent() && eventCast<const StateEvent>(m_event)->repeatsState()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// isReplacement?
|
||||
if (auto e = eventCast<const RoomMessageEvent>(m_event)) {
|
||||
if (!e->replacedEvent().isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (is<RedactionEvent>(*m_event) || is<ReactionEvent>(*m_event)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (auto e = eventCast<const RoomMessageEvent>(m_event)) {
|
||||
if (!e->replacedEvent().isEmpty() && e->replacedEvent() != e->id()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_room->connection()->isIgnored(m_room->user(m_event->senderId()))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// hide ending live location beacons
|
||||
if (m_event->isStateEvent() && m_event->matrixType() == "org.matrix.msc3672.beacon_info"_ls && !m_event->contentJson()["live"_ls].toBool()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
QString EventHandler::getRichBody(bool stripNewlines) const
|
||||
{
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getRichBody called with m_event set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
return getBody(m_event, Qt::RichText, stripNewlines);
|
||||
}
|
||||
|
||||
QString EventHandler::getPlainBody(bool stripNewlines) const
|
||||
{
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getPlainBody called with m_event set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
return getBody(m_event, Qt::PlainText, stripNewlines);
|
||||
}
|
||||
|
||||
QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat format, bool stripNewlines) const
|
||||
{
|
||||
if (event->isRedacted()) {
|
||||
auto reason = event->redactedBecause()->reason();
|
||||
return (reason.isEmpty()) ? i18n("<i>[This message was deleted]</i>") : i18n("<i>[This message was deleted: %1]</i>", reason);
|
||||
}
|
||||
|
||||
const bool prettyPrint = (format == Qt::RichText);
|
||||
|
||||
return switchOnType(
|
||||
*event,
|
||||
[this, format, stripNewlines](const RoomMessageEvent &event) {
|
||||
return getMessageBody(event, format, stripNewlines);
|
||||
},
|
||||
[](const StickerEvent &e) {
|
||||
return e.body();
|
||||
},
|
||||
[this, prettyPrint](const RoomMemberEvent &e) {
|
||||
// FIXME: Rewind to the name that was at the time of this event
|
||||
auto subjectName = m_room->htmlSafeMemberName(e.userId());
|
||||
if (e.membership() == Membership::Leave) {
|
||||
if (e.prevContent() && e.prevContent()->displayName) {
|
||||
subjectName = sanitized(*e.prevContent()->displayName).toHtmlEscaped();
|
||||
}
|
||||
}
|
||||
|
||||
if (prettyPrint) {
|
||||
subjectName = QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a>")
|
||||
.arg(e.userId(), Utils::getUserColor(m_room->user(e.userId())->hueF()).name(), subjectName);
|
||||
}
|
||||
|
||||
// The below code assumes senderName output in AuthorRole
|
||||
switch (e.membership()) {
|
||||
case Membership::Invite:
|
||||
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();
|
||||
}
|
||||
return text;
|
||||
}
|
||||
Q_FALLTHROUGH();
|
||||
case Membership::Join: {
|
||||
QString text{};
|
||||
// Part 1: invites and joins
|
||||
if (e.repeatsState()) {
|
||||
text = i18n("joined the room (repeated)");
|
||||
} else if (e.changesMembership()) {
|
||||
text = e.membership() == Membership::Invite ? i18n("invited %1 to the room", subjectName) : i18n("joined the room");
|
||||
}
|
||||
if (!text.isEmpty()) {
|
||||
if (!e.reason().isEmpty()) {
|
||||
text += i18n(": %1", e.reason().toHtmlEscaped());
|
||||
}
|
||||
return text;
|
||||
}
|
||||
// Part 2: profile changes of joined members
|
||||
if (e.isRename()) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
if (e.isAvatarUpdate()) {
|
||||
if (!text.isEmpty()) {
|
||||
text += i18n(" and ");
|
||||
}
|
||||
if (!e.newAvatarUrl()) {
|
||||
text += i18nc("their refers to a singular user", "cleared their avatar");
|
||||
} else if (!e.prevContent()->avatarUrl) {
|
||||
text += i18n("set an avatar");
|
||||
} else {
|
||||
text += i18nc("their refers to a singular user", "updated their avatar");
|
||||
}
|
||||
}
|
||||
if (text.isEmpty()) {
|
||||
text = i18nc("<user> changed nothing", "changed nothing");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
case Membership::Leave:
|
||||
if (e.prevContent() && e.prevContent()->membership == Membership::Invite) {
|
||||
return (e.senderId() != e.userId()) ? i18n("withdrew %1's invitation", subjectName) : i18n("rejected the invitation");
|
||||
}
|
||||
|
||||
if (e.prevContent() && e.prevContent()->membership == Membership::Ban) {
|
||||
return (e.senderId() != e.userId()) ? i18n("unbanned %1", subjectName) : i18n("self-unbanned");
|
||||
}
|
||||
return (e.senderId() != e.userId())
|
||||
? i18n("has put %1 out of the room: %2", subjectName, e.contentJson()["reason"_ls].toString().toHtmlEscaped())
|
||||
: i18n("left the room");
|
||||
case Membership::Ban:
|
||||
if (e.senderId() != e.userId()) {
|
||||
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());
|
||||
}
|
||||
} else {
|
||||
return i18n("self-banned from the room");
|
||||
}
|
||||
case Membership::Knock: {
|
||||
QString reason(e.contentJson()["reason"_ls].toString().toHtmlEscaped());
|
||||
return reason.isEmpty() ? i18n("requested an invite") : i18n("requested an invite with reason: %1", reason);
|
||||
}
|
||||
default:;
|
||||
}
|
||||
return i18n("made something unknown");
|
||||
},
|
||||
[](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, stripNewlines](const RoomTopicEvent &e) {
|
||||
return (e.topic().isEmpty()) ? i18n("cleared the topic")
|
||||
: i18n("set the topic to: %1",
|
||||
prettyPrint ? Quotient::prettyPrint(e.topic())
|
||||
: stripNewlines ? e.topic().replace(u'\n', u' ')
|
||||
: e.topic());
|
||||
},
|
||||
[](const RoomAvatarEvent &) {
|
||||
return i18n("changed the room avatar");
|
||||
},
|
||||
[](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());
|
||||
},
|
||||
[](const RoomPowerLevelsEvent &) {
|
||||
return i18nc("'power level' means permission level", "changed the power levels for this room");
|
||||
},
|
||||
[](const StateEvent &e) {
|
||||
if (e.matrixType() == QLatin1String("m.room.server_acl")) {
|
||||
return i18n("changed the server access control lists for this room");
|
||||
}
|
||||
if (e.matrixType() == QLatin1String("im.vector.modular.widgets")) {
|
||||
if (e.fullJson()["unsigned"_ls]["prev_content"_ls].toObject().isEmpty()) {
|
||||
return i18nc("[User] added <name> widget", "added %1 widget", e.contentJson()["name"_ls].toString());
|
||||
}
|
||||
if (e.contentJson().isEmpty()) {
|
||||
return i18nc("[User] removed <name> widget", "removed %1 widget", e.fullJson()["unsigned"_ls]["prev_content"_ls]["name"_ls].toString());
|
||||
}
|
||||
return i18nc("[User] configured <name> widget", "configured %1 widget", e.contentJson()["name"_ls].toString());
|
||||
}
|
||||
if (e.matrixType() == "org.matrix.msc3672.beacon_info"_ls) {
|
||||
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());
|
||||
},
|
||||
[](const PollStartEvent &e) {
|
||||
return e.question();
|
||||
},
|
||||
i18n("Unknown event"));
|
||||
}
|
||||
|
||||
QString EventHandler::getMessageBody(const RoomMessageEvent &event, Qt::TextFormat format, bool stripNewlines) const
|
||||
{
|
||||
TextHandler textHandler;
|
||||
|
||||
if (event.hasFileContent()) {
|
||||
auto fileCaption = event.content()->fileInfo()->originalName;
|
||||
if (fileCaption.isEmpty()) {
|
||||
fileCaption = event.plainBody();
|
||||
} else if (event.content()->fileInfo()->originalName != event.plainBody()) {
|
||||
fileCaption = event.plainBody() + " | "_ls + fileCaption;
|
||||
}
|
||||
textHandler.setData(fileCaption);
|
||||
return !fileCaption.isEmpty() ? textHandler.handleRecievePlainText(Qt::PlainText, stripNewlines) : i18n("a file");
|
||||
}
|
||||
|
||||
QString body;
|
||||
if (event.hasTextContent() && event.content()) {
|
||||
body = static_cast<const MessageEventContent::TextContent *>(event.content())->body;
|
||||
} else {
|
||||
body = event.plainBody();
|
||||
}
|
||||
|
||||
textHandler.setData(body);
|
||||
|
||||
Qt::TextFormat inputFormat;
|
||||
if (event.mimeType().name() == "text/plain"_ls) {
|
||||
inputFormat = Qt::PlainText;
|
||||
} else {
|
||||
inputFormat = Qt::RichText;
|
||||
}
|
||||
|
||||
if (format == Qt::RichText) {
|
||||
return textHandler.handleRecieveRichText(inputFormat, m_room, &event, stripNewlines);
|
||||
} else {
|
||||
return textHandler.handleRecievePlainText(inputFormat, stripNewlines);
|
||||
}
|
||||
}
|
||||
|
||||
QString EventHandler::getGenericBody() const
|
||||
{
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getGenericBody called with m_event set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
if (m_event->isRedacted()) {
|
||||
return i18n("<i>[This message was deleted]</i>");
|
||||
}
|
||||
|
||||
return switchOnType(
|
||||
*m_event,
|
||||
[](const RoomMessageEvent &e) {
|
||||
Q_UNUSED(e)
|
||||
return i18n("sent a message");
|
||||
},
|
||||
[](const StickerEvent &e) {
|
||||
Q_UNUSED(e)
|
||||
return i18n("sent a sticker");
|
||||
},
|
||||
[](const RoomMemberEvent &e) {
|
||||
switch (e.membership()) {
|
||||
case Membership::Invite:
|
||||
if (e.repeatsState()) {
|
||||
return i18n("reinvited someone to the room");
|
||||
}
|
||||
Q_FALLTHROUGH();
|
||||
case Membership::Join: {
|
||||
QString text{};
|
||||
// Part 1: invites and joins
|
||||
if (e.repeatsState()) {
|
||||
text = i18n("joined the room (repeated)");
|
||||
} else if (e.changesMembership()) {
|
||||
text = e.membership() == Membership::Invite ? i18n("invited someone to the room") : i18n("joined the room");
|
||||
}
|
||||
if (!text.isEmpty()) {
|
||||
return text;
|
||||
}
|
||||
// Part 2: profile changes of joined members
|
||||
if (e.isRename()) {
|
||||
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");
|
||||
}
|
||||
}
|
||||
if (e.isAvatarUpdate()) {
|
||||
if (!text.isEmpty()) {
|
||||
text += i18n(" and ");
|
||||
}
|
||||
if (!e.newAvatarUrl()) {
|
||||
text += i18nc("their refers to a singular user", "cleared their avatar");
|
||||
} else if (!e.prevContent()->avatarUrl) {
|
||||
text += i18n("set an avatar");
|
||||
} else {
|
||||
text += i18nc("their refers to a singular user", "updated their avatar");
|
||||
}
|
||||
}
|
||||
if (text.isEmpty()) {
|
||||
text = i18nc("<user> changed nothing", "changed nothing");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
case Membership::Leave:
|
||||
if (e.prevContent() && e.prevContent()->membership == Membership::Invite) {
|
||||
return (e.senderId() != e.userId()) ? i18n("withdrew a user's invitation") : i18n("rejected the invitation");
|
||||
}
|
||||
|
||||
if (e.prevContent() && e.prevContent()->membership == Membership::Ban) {
|
||||
return (e.senderId() != e.userId()) ? i18n("unbanned a user") : i18n("self-unbanned");
|
||||
}
|
||||
return (e.senderId() != e.userId()) ? i18n("put a user out of the room") : i18n("left the room");
|
||||
case Membership::Ban:
|
||||
if (e.senderId() != e.userId()) {
|
||||
return i18n("banned a user from the room");
|
||||
} else {
|
||||
return i18n("self-banned from the room");
|
||||
}
|
||||
case Membership::Knock: {
|
||||
return i18n("requested an invite");
|
||||
}
|
||||
default:;
|
||||
}
|
||||
return i18n("made something unknown");
|
||||
},
|
||||
[](const RoomCanonicalAliasEvent &e) {
|
||||
return (e.alias().isEmpty()) ? i18n("cleared the room main alias") : i18n("set the room main alias");
|
||||
},
|
||||
[](const RoomNameEvent &e) {
|
||||
return (e.name().isEmpty()) ? i18n("cleared the room name") : i18n("set the room name");
|
||||
},
|
||||
[](const RoomTopicEvent &e) {
|
||||
return (e.topic().isEmpty()) ? i18n("cleared the topic") : i18n("set the topic");
|
||||
},
|
||||
[](const RoomAvatarEvent &) {
|
||||
return i18n("changed the room avatar");
|
||||
},
|
||||
[](const EncryptionEvent &) {
|
||||
return i18n("activated End-to-End Encryption");
|
||||
},
|
||||
[](const RoomCreateEvent &e) {
|
||||
return e.isUpgrade() ? i18n("upgraded the room version") : i18n("created the room");
|
||||
},
|
||||
[](const RoomPowerLevelsEvent &) {
|
||||
return i18nc("'power level' means permission level", "changed the power levels for this room");
|
||||
},
|
||||
[](const StateEvent &e) {
|
||||
if (e.matrixType() == QLatin1String("m.room.server_acl")) {
|
||||
return i18n("changed the server access control lists for this room");
|
||||
}
|
||||
if (e.matrixType() == QLatin1String("im.vector.modular.widgets")) {
|
||||
if (e.fullJson()["unsigned"_ls]["prev_content"_ls].toObject().isEmpty()) {
|
||||
return i18n("added a widget");
|
||||
}
|
||||
if (e.contentJson().isEmpty()) {
|
||||
return i18n("removed a widget");
|
||||
}
|
||||
return i18n("configured a widget");
|
||||
}
|
||||
return i18n("updated the state");
|
||||
},
|
||||
[](const PollStartEvent &e) {
|
||||
Q_UNUSED(e);
|
||||
return i18n("started a poll");
|
||||
},
|
||||
i18n("Unknown event"));
|
||||
}
|
||||
|
||||
QVariantMap EventHandler::getMediaInfo() const
|
||||
{
|
||||
return getMediaInfoForEvent(m_event);
|
||||
}
|
||||
|
||||
QVariantMap EventHandler::getMediaInfoForEvent(const Quotient::RoomEvent *event) const
|
||||
{
|
||||
QString eventId = event->id();
|
||||
|
||||
// Get the file info for the event.
|
||||
const EventContent::FileInfo *fileInfo;
|
||||
if (event->is<RoomMessageEvent>()) {
|
||||
auto roomMessageEvent = eventCast<const RoomMessageEvent>(event);
|
||||
if (!roomMessageEvent->hasFileContent()) {
|
||||
return {};
|
||||
}
|
||||
fileInfo = roomMessageEvent->content()->fileInfo();
|
||||
} else if (event->is<StickerEvent>()) {
|
||||
auto stickerEvent = eventCast<const StickerEvent>(event);
|
||||
fileInfo = &stickerEvent->image();
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
|
||||
return getMediaInfoFromFileInfo(fileInfo, eventId);
|
||||
}
|
||||
|
||||
QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail) const
|
||||
{
|
||||
QVariantMap mediaInfo;
|
||||
|
||||
// Get the mxc URL for the media.
|
||||
if (!fileInfo->url().isValid() || eventId.isEmpty()) {
|
||||
mediaInfo["source"_ls] = QUrl();
|
||||
} else {
|
||||
QUrl source = m_room->makeMediaUrl(eventId, fileInfo->url());
|
||||
|
||||
if (source.isValid() && source.scheme() == QStringLiteral("mxc")) {
|
||||
mediaInfo["source"_ls] = source;
|
||||
} else {
|
||||
mediaInfo["source"_ls] = QUrl();
|
||||
}
|
||||
}
|
||||
|
||||
auto mimeType = fileInfo->mimeType;
|
||||
// Add the MIME type for the media if available.
|
||||
mediaInfo["mimeType"_ls] = mimeType.name();
|
||||
|
||||
// Add the MIME type icon if available.
|
||||
mediaInfo["mimeIcon"_ls] = mimeType.iconName();
|
||||
|
||||
// Add media size if available.
|
||||
mediaInfo["size"_ls] = fileInfo->payloadSize;
|
||||
|
||||
// Add parameter depending on media type.
|
||||
if (mimeType.name().contains(QStringLiteral("image"))) {
|
||||
if (auto castInfo = static_cast<const EventContent::ImageContent *>(fileInfo)) {
|
||||
mediaInfo["width"_ls] = castInfo->imageSize.width();
|
||||
mediaInfo["height"_ls] = castInfo->imageSize.height();
|
||||
|
||||
// TODO: Images in certain formats (e.g. WebP) will be erroneously marked as animated, even if they are static.
|
||||
mediaInfo["animated"_ls] = QMovie::supportedFormats().contains(mimeType.preferredSuffix().toUtf8());
|
||||
|
||||
if (!isThumbnail) {
|
||||
QVariantMap tempInfo;
|
||||
auto thumbnailInfo = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true);
|
||||
if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) {
|
||||
tempInfo = thumbnailInfo;
|
||||
} else {
|
||||
QString blurhash = castInfo->originalInfoJson["xyz.amorgan.blurhash"_ls].toString();
|
||||
if (blurhash.isEmpty()) {
|
||||
tempInfo["source"_ls] = QUrl();
|
||||
} else {
|
||||
tempInfo["source"_ls] = QUrl("image://blurhash/"_ls + blurhash);
|
||||
}
|
||||
}
|
||||
mediaInfo["tempInfo"_ls] = tempInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mimeType.name().contains(QStringLiteral("video"))) {
|
||||
if (auto castInfo = static_cast<const EventContent::VideoContent *>(fileInfo)) {
|
||||
mediaInfo["width"_ls] = castInfo->imageSize.width();
|
||||
mediaInfo["height"_ls] = castInfo->imageSize.height();
|
||||
mediaInfo["duration"_ls] = castInfo->duration;
|
||||
|
||||
if (!isThumbnail) {
|
||||
QVariantMap tempInfo;
|
||||
auto thumbnailInfo = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true);
|
||||
if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) {
|
||||
tempInfo = thumbnailInfo;
|
||||
} else {
|
||||
QString blurhash = castInfo->originalInfoJson["xyz.amorgan.blurhash"_ls].toString();
|
||||
if (blurhash.isEmpty()) {
|
||||
tempInfo["source"_ls] = QUrl();
|
||||
} else {
|
||||
tempInfo["source"_ls] = QUrl("image://blurhash/"_ls + blurhash);
|
||||
}
|
||||
}
|
||||
mediaInfo["tempInfo"_ls] = tempInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mimeType.name().contains(QStringLiteral("audio"))) {
|
||||
if (auto castInfo = static_cast<const EventContent::AudioContent *>(fileInfo)) {
|
||||
mediaInfo["duration"_ls] = castInfo->duration;
|
||||
}
|
||||
}
|
||||
|
||||
return mediaInfo;
|
||||
}
|
||||
|
||||
QSharedPointer<LinkPreviewer> EventHandler::getLinkPreviewer() const
|
||||
{
|
||||
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 {};
|
||||
}
|
||||
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
|
||||
{
|
||||
return !m_event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString().isEmpty();
|
||||
}
|
||||
|
||||
QString EventHandler::getReplyId() const
|
||||
{
|
||||
return m_event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString();
|
||||
}
|
||||
|
||||
DelegateType::Type EventHandler::getReplyDelegateType() const
|
||||
{
|
||||
auto replyEvent = m_room->getReplyForEvent(*m_event);
|
||||
if (replyEvent == nullptr) {
|
||||
return DelegateType::Other;
|
||||
}
|
||||
return getDelegateTypeForEvent(replyEvent);
|
||||
}
|
||||
|
||||
QVariantMap EventHandler::getReplyAuthor() const
|
||||
{
|
||||
if (m_room == nullptr) {
|
||||
qCWarning(EventHandling) << "getReplyAuthor called with m_room set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
// If we have a room we can return an empty user by handing nullptr to m_room->getUser.
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getReplyAuthor called with m_event set to nullptr. Returning empty user.";
|
||||
return m_room->getUser(nullptr);
|
||||
}
|
||||
|
||||
auto replyPtr = m_room->getReplyForEvent(*m_event);
|
||||
|
||||
if (replyPtr) {
|
||||
auto replyUser = m_room->user(replyPtr->senderId());
|
||||
return m_room->getUser(replyUser);
|
||||
} else {
|
||||
return m_room->getUser(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
QString EventHandler::getReplyRichBody(bool stripNewlines) const
|
||||
{
|
||||
if (m_room == nullptr) {
|
||||
qCWarning(EventHandling) << "getReplyRichBody called with m_room set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getReplyRichBody called with m_event set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
|
||||
auto replyEvent = m_room->getReplyForEvent(*m_event);
|
||||
if (replyEvent == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return getBody(replyEvent, Qt::RichText, stripNewlines);
|
||||
}
|
||||
|
||||
QString EventHandler::getReplyPlainBody(bool stripNewlines) const
|
||||
{
|
||||
if (m_room == nullptr) {
|
||||
qCWarning(EventHandling) << "getReplyPlainBody called with m_room set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getReplyPlainBody called with m_event set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
|
||||
auto replyEvent = m_room->getReplyForEvent(*m_event);
|
||||
if (replyEvent == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return getBody(replyEvent, Qt::PlainText, stripNewlines);
|
||||
}
|
||||
|
||||
QVariantMap EventHandler::getReplyMediaInfo() const
|
||||
{
|
||||
if (m_room == nullptr) {
|
||||
qCWarning(EventHandling) << "getReplyMediaInfo called with m_room set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getReplyMediaInfo called with m_event set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
|
||||
auto replyPtr = m_room->getReplyForEvent(*m_event);
|
||||
if (!replyPtr) {
|
||||
return {};
|
||||
}
|
||||
return getMediaInfoForEvent(replyPtr);
|
||||
}
|
||||
|
||||
float EventHandler::getLatitude() const
|
||||
{
|
||||
const auto geoUri = m_event->contentJson()["geo_uri"_ls].toString();
|
||||
if (geoUri.isEmpty()) {
|
||||
return -100.0; // latitude runs from -90deg to +90deg so -100 is out of range.
|
||||
}
|
||||
const auto latitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[0];
|
||||
return latitude.toFloat();
|
||||
}
|
||||
|
||||
float EventHandler::getLongitude() const
|
||||
{
|
||||
const auto geoUri = m_event->contentJson()["geo_uri"_ls].toString();
|
||||
if (geoUri.isEmpty()) {
|
||||
return -200.0; // longitude runs from -180deg to +180deg so -200 is out of range.
|
||||
}
|
||||
const auto latitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[1];
|
||||
return latitude.toFloat();
|
||||
}
|
||||
|
||||
QString EventHandler::getLocationAssetType() const
|
||||
{
|
||||
const auto assetType = m_event->contentJson()["org.matrix.msc3488.asset"_ls].toObject()["type"_ls].toString();
|
||||
if (assetType.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
return assetType;
|
||||
}
|
||||
|
||||
bool EventHandler::hasReadMarkers() const
|
||||
{
|
||||
auto userIds = m_room->userIdsAtEvent(m_event->id());
|
||||
userIds.remove(m_room->localUser()->id());
|
||||
return userIds.size() > 0;
|
||||
}
|
||||
|
||||
QVariantList EventHandler::getReadMarkers(int maxMarkers) const
|
||||
{
|
||||
auto userIds_temp = m_room->userIdsAtEvent(m_event->id());
|
||||
userIds_temp.remove(m_room->localUser()->id());
|
||||
|
||||
auto userIds = userIds_temp.values();
|
||||
if (userIds.count() > maxMarkers) {
|
||||
userIds = userIds.mid(0, maxMarkers);
|
||||
}
|
||||
|
||||
QVariantList users;
|
||||
users.reserve(userIds.size());
|
||||
for (const auto &userId : userIds) {
|
||||
auto user = m_room->user(userId);
|
||||
users += m_room->getUser(user);
|
||||
}
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
QString EventHandler::getNumberExcessReadMarkers(int maxMarkers) const
|
||||
{
|
||||
auto userIds = m_room->userIdsAtEvent(m_event->id());
|
||||
userIds.remove(m_room->localUser()->id());
|
||||
|
||||
if (userIds.count() > maxMarkers) {
|
||||
return QStringLiteral("+ ") + QString::number(userIds.count() - maxMarkers);
|
||||
} else {
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
QString EventHandler::getReadMarkersString() const
|
||||
{
|
||||
auto userIds = m_room->userIdsAtEvent(m_event->id());
|
||||
userIds.remove(m_room->localUser()->id());
|
||||
|
||||
/**
|
||||
* The string ends up in the form
|
||||
* "x users: user1DisplayName, user2DisplayName, etc."
|
||||
*/
|
||||
QString readMarkersString = i18np("1 user: ", "%1 users: ", userIds.size());
|
||||
for (const auto &userId : userIds) {
|
||||
auto user = m_room->user(userId);
|
||||
readMarkersString += user->displayname(m_room) + i18nc("list separator", ", ");
|
||||
}
|
||||
readMarkersString.chop(2);
|
||||
return readMarkersString;
|
||||
}
|
||||
400
src/eventhandler.h
Normal file
400
src/eventhandler.h
Normal file
@@ -0,0 +1,400 @@
|
||||
// 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 <KFormat>
|
||||
|
||||
#include <Quotient/eventitem.h>
|
||||
#include <Quotient/events/roomevent.h>
|
||||
#include <Quotient/events/roommessageevent.h>
|
||||
|
||||
#include "enums/delegatetype.h"
|
||||
|
||||
class LinkPreviewer;
|
||||
class NeoChatRoom;
|
||||
class ReactionModel;
|
||||
|
||||
/**
|
||||
* @class EventHandler
|
||||
*
|
||||
* This class is designed to handle a Quotient::RoomEvent allowing data to be extracted
|
||||
* in a form ready for the NeoChat UI.
|
||||
*
|
||||
* To use this properly both the room and the event should be set (and the event should
|
||||
* be from the given room).
|
||||
*
|
||||
* @note EventHandler will always try to return something even when not properly
|
||||
* initialised, this is usually the best empty value it can create with available
|
||||
* information. This is to minimize warnings from QML especially during startup
|
||||
* and room changes.
|
||||
*/
|
||||
class EventHandler : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Return the current room the EventHandler is using.
|
||||
*/
|
||||
const NeoChatRoom *getRoom() const;
|
||||
|
||||
/**
|
||||
* @brief Set the current room the EventHandler to using.
|
||||
*/
|
||||
void setRoom(const NeoChatRoom *room);
|
||||
|
||||
/**
|
||||
* @brief Return the current event the EventHandler is using.
|
||||
*/
|
||||
const Quotient::Event *getEvent() const;
|
||||
|
||||
/**
|
||||
* @brief Set the current event the EventHandler to using.
|
||||
*/
|
||||
void setEvent(const Quotient::RoomEvent *event);
|
||||
|
||||
/**
|
||||
* @brief Return the Matrix ID of the event.
|
||||
*/
|
||||
QString getId() const;
|
||||
|
||||
/**
|
||||
* @brief Return the DelegateType of the event.
|
||||
*
|
||||
* @note While similar this is not the matrix event or message type. This is
|
||||
* to tell a QML ListView what delegate to show for each event. So while
|
||||
* similar to the spec it is not the same.
|
||||
*/
|
||||
DelegateType::Type getDelegateType() const;
|
||||
|
||||
/**
|
||||
* @brief Get the author of the event in context of the room.
|
||||
*
|
||||
* This is different to getting a Quotient::User object
|
||||
* as neither of those can provide details like the displayName or avatarMediaId
|
||||
* without the room context as these can vary from room to room. This function
|
||||
* uses the room context and outputs the result as QVariantMap.
|
||||
*
|
||||
* An empty QVariantMap will be returned if the EventHandler hasn't had the room
|
||||
* intialised. An empty user (i.e. a QVariantMap with all the correct keys
|
||||
* but empty values) will be returned if the room has been set but not an event.
|
||||
*
|
||||
* @param isPending if the event is pending, i.e. has not been confirmed by
|
||||
* the server.
|
||||
*
|
||||
* @return a QVariantMap for the user with the following properties:
|
||||
* - isLocalUser - Whether the user is the local user.
|
||||
* - id - The matrix ID of the user.
|
||||
* - displayName - Display name in the context of this room.
|
||||
* - avatarSource - The mxc URL for the user's avatar in the current room.
|
||||
* - avatarMediaId - Avatar id in the context of this room.
|
||||
* - color - Color for the user.
|
||||
* - object - The Quotient::User object for the user.
|
||||
*
|
||||
* @sa Quotient::User
|
||||
*/
|
||||
QVariantMap getAuthor(bool isPending = false) const;
|
||||
|
||||
/**
|
||||
* @brief Get the display name of the event author.
|
||||
*
|
||||
* This method is separate from getAuthor() and special in that it will return
|
||||
* the old display name of the author if the current event is one that caused it
|
||||
* to change. This allows for scenarios where the UI wishes to notify that a
|
||||
* user's display name has changed and what it changed from.
|
||||
*
|
||||
* @param isPending whether the event is pending as this cannot be derived from
|
||||
* just the event object.
|
||||
*/
|
||||
QString getAuthorDisplayName(bool isPending = false) const;
|
||||
|
||||
/**
|
||||
* @brief Return a QDateTime object for the event timestamp.
|
||||
*/
|
||||
QDateTime getTime(bool isPending = false, QDateTime lastUpdated = {}) const;
|
||||
|
||||
/**
|
||||
* @brief Return a QString for the event timestamp.
|
||||
*
|
||||
* This is intended to return a string that is read for display in the UI without
|
||||
* any further manipulation required.
|
||||
*
|
||||
* @param relative whether the string is realtive to the current date, i.e.
|
||||
* Yesterday or Wednesday, etc.
|
||||
* @param format the QLocale::FormatType to use.
|
||||
* @param isPending whether the event is pending as this cannot be derived from
|
||||
* just the event object.
|
||||
* @param lastUpdated the time the event was last updated locally as this cannot be
|
||||
* obtained from the event.
|
||||
*/
|
||||
QString getTimeString(bool relative, QLocale::FormatType format = QLocale::ShortFormat, bool isPending = false, QDateTime lastUpdated = {}) const;
|
||||
|
||||
/**
|
||||
* @brief Whether the event should be highlighted in the timeline.
|
||||
*
|
||||
* @note Messages in direct chats are never highlighted.
|
||||
*/
|
||||
bool isHighlighted();
|
||||
|
||||
/**
|
||||
* @brief Whether the event should be hidden in the timeline.
|
||||
*
|
||||
* This could be for numerous reasons, e.g. if it's a replacement event, if the
|
||||
* user has hidden all state events or if the sender has been ignored by the local
|
||||
* user.
|
||||
*/
|
||||
bool isHidden();
|
||||
|
||||
/**
|
||||
* @brief Output a string for the message content ready for display in a rich text field.
|
||||
*
|
||||
* The output string is dependant upon the event type and the desired output format.
|
||||
*
|
||||
* For most messages this is the body content of the message. For media messages
|
||||
* this will be the caption and for state events it will be a string specific
|
||||
* to that event with some dynamic details about the event added.
|
||||
*
|
||||
* E.g. For a room topic state event the text will be:
|
||||
* "set the topic to: <new topic text>"
|
||||
*
|
||||
* @param stripNewlines whether the output should have new lines in it.
|
||||
*/
|
||||
QString getRichBody(bool stripNewlines = false) const;
|
||||
|
||||
/**
|
||||
* @brief Output a string for the message content ready for display in a plain text field.
|
||||
*
|
||||
* The output string is dependant upon the event type and the desired output format.
|
||||
*
|
||||
* For most messages this is the body content of the message. For media messages
|
||||
* this will be the caption and for state events it will be a string specific
|
||||
* to that event with some dynamic details about the event added.
|
||||
*
|
||||
* E.g. For a room topic state event the text will be:
|
||||
* "set the topic to: <new topic text>"
|
||||
*
|
||||
* @param stripNewlines whether the output should have new lines in it.
|
||||
*/
|
||||
QString getPlainBody(bool stripNewlines = false) const;
|
||||
|
||||
/**
|
||||
* @brief Output a generic string for the message content ready for display.
|
||||
*
|
||||
* The output string is dependant upon the event type.
|
||||
*
|
||||
* Unlike EventHandler::getRichBody or EventHandler::getPlainBody the string
|
||||
* is the same for all events of the same type.
|
||||
*
|
||||
* E.g. For a message the text will be:
|
||||
* "sent a message"
|
||||
*
|
||||
* @sa getRichBody(), getPlainBody()
|
||||
*/
|
||||
QString getGenericBody() const;
|
||||
|
||||
/**
|
||||
* @brief Return the media info for the event.
|
||||
*
|
||||
* An empty QVariantMap will be returned for any event that doesn't have any
|
||||
* media info.
|
||||
*
|
||||
* @return This should consist of the following:
|
||||
* - source - The mxc URL for the media.
|
||||
* - mimeType - The MIME type of the media (should be image/xxx for this delegate).
|
||||
* - mimeIcon - The MIME icon name (should be image-xxx).
|
||||
* - size - The file size in bytes.
|
||||
* - width - The width in pixels of the audio media.
|
||||
* - height - The height in pixels of the audio media.
|
||||
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
bool hasReply() const;
|
||||
|
||||
/**
|
||||
* @brief Return the Matrix ID of the event replied to.
|
||||
*/
|
||||
QString getReplyId() const;
|
||||
|
||||
/**
|
||||
* @brief Return the DelegateType of the event replied to.
|
||||
*
|
||||
* @note While similar this is not the matrix event or message type. This is
|
||||
* to tell a QML ListView what delegate to show for each event. So while
|
||||
* similar to the spec it is not the same.
|
||||
*/
|
||||
DelegateType::Type getReplyDelegateType() const;
|
||||
|
||||
/**
|
||||
* @brief Get the author of the event replied to in context of the room.
|
||||
*
|
||||
* This is different to getting a Quotient::User object
|
||||
* as neither of those can provide details like the displayName or avatarMediaId
|
||||
* without the room context as these can vary from room to room. This function
|
||||
* uses the room context and outputs the result as QVariantMap.
|
||||
*
|
||||
* An empty QVariantMap will be returned if the EventHandler hasn't had the room
|
||||
* intialised. An empty user (i.e. a QVariantMap with all the correct keys
|
||||
* but empty values) will be returned if the room has been set but not an event.
|
||||
*
|
||||
* @return a QVariantMap for the user with the following properties:
|
||||
* - isLocalUser - Whether the user is the local user.
|
||||
* - id - The matrix ID of the user.
|
||||
* - displayName - Display name in the context of this room.
|
||||
* - avatarSource - The mxc URL for the user's avatar in the current room.
|
||||
* - avatarMediaId - Avatar id in the context of this room.
|
||||
* - color - Color for the user.
|
||||
* - object - The Quotient::User object for the user.
|
||||
*
|
||||
* @sa Quotient::User
|
||||
*/
|
||||
QVariantMap getReplyAuthor() const;
|
||||
|
||||
/**
|
||||
* @brief Output a string for the message content of the event replied to ready
|
||||
* for display in a rich text field.
|
||||
*
|
||||
* The output string is dependant upon the event type and the desired output format.
|
||||
*
|
||||
* For most messages this is the body content of the message. For media messages
|
||||
* this will be the caption and for state events it will be a string specific
|
||||
* to that event with some dynamic details about the event added.
|
||||
*
|
||||
* E.g. For a room topic state event the text will be:
|
||||
* "set the topic to: <new topic text>"
|
||||
*
|
||||
* @param stripNewlines whether the output should have new lines in it.
|
||||
*/
|
||||
QString getReplyRichBody(bool stripNewlines = false) const;
|
||||
|
||||
/**
|
||||
* @brief Output a string for the message content of the event replied to ready
|
||||
* for display in a plain text field.
|
||||
*
|
||||
* The output string is dependant upon the event type and the desired output format.
|
||||
*
|
||||
* For most messages this is the body content of the message. For media messages
|
||||
* this will be the caption and for state events it will be a string specific
|
||||
* to that event with some dynamic details about the event added.
|
||||
*
|
||||
* E.g. For a room topic state event the text will be:
|
||||
* "set the topic to: <new topic text>"
|
||||
*
|
||||
* @param stripNewlines whether the output should have new lines in it.
|
||||
*/
|
||||
QString getReplyPlainBody(bool stripNewlines = false) const;
|
||||
|
||||
/**
|
||||
* @brief Return the media info for the event replied to.
|
||||
*
|
||||
* An empty QVariantMap will be returned for any event that doesn't have any
|
||||
* media info.
|
||||
*
|
||||
* @return This should consist of the following:
|
||||
* - source - The mxc URL for the media.
|
||||
* - mimeType - The MIME type of the media (should be image/xxx for this delegate).
|
||||
* - mimeIcon - The MIME icon name (should be image-xxx).
|
||||
* - size - The file size in bytes.
|
||||
* - width - The width in pixels of the audio media.
|
||||
* - height - The height in pixels of the audio media.
|
||||
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
|
||||
*/
|
||||
QVariantMap getReplyMediaInfo() const;
|
||||
|
||||
/**
|
||||
* @brief Return the latitude for the event.
|
||||
*
|
||||
* Returns -100.0 if the event doesn't have a location (latitudes are in the
|
||||
* range -90deg to +90deg so -100 is out of range).
|
||||
*/
|
||||
float getLatitude() const;
|
||||
|
||||
/**
|
||||
* @brief Return the longitude for the event.
|
||||
*
|
||||
* Returns -200.0 if the event doesn't have a location (latitudes are in the
|
||||
* range -180deg to +180deg so -200 is out of range).
|
||||
*/
|
||||
float getLongitude() const;
|
||||
|
||||
/**
|
||||
* @brief Return the type of location marker for the event.
|
||||
*/
|
||||
QString getLocationAssetType() const;
|
||||
|
||||
/**
|
||||
* @brief Whether the event has any read marker for other users.
|
||||
*/
|
||||
bool hasReadMarkers() const;
|
||||
|
||||
/**
|
||||
* @brief Returns a list of user read marker for the event.
|
||||
*
|
||||
* @param maxMarkers the maximum number of users to return. Usually the number
|
||||
* of user read makers shown is limited to not clutter the UI.
|
||||
* This needs to be the same as used in getNumberExcessReadMarkers
|
||||
* so that the markers line up with the number displayed, i.e.
|
||||
* the number of users shown plus the excess number will be
|
||||
* the total number of other user read markers at an event.
|
||||
*/
|
||||
QVariantList getReadMarkers(int maxMarkers = 5) const;
|
||||
|
||||
/**
|
||||
* @brief Returns the number of excess user read markers for the event.
|
||||
*
|
||||
* This returns a string in the form "+ x" ready for use in the UI.
|
||||
*
|
||||
* @param maxMarkers the maximum number of markers shown in the UI. This needs to
|
||||
* be the same as used in getReadMarkers so that the value lines
|
||||
* up with the number displayed, i.e. the number of users shown
|
||||
* plus the excess number will be the total number of other user
|
||||
* read markers at an event.
|
||||
*/
|
||||
QString getNumberExcessReadMarkers(int maxMarkers = 5) const;
|
||||
|
||||
/**
|
||||
* @brief Returns a string with the names of the read markers at the event.
|
||||
*
|
||||
* This is in the form "x users: name 1, name 2, ...".
|
||||
*/
|
||||
QString getReadMarkersString() const;
|
||||
|
||||
private:
|
||||
const NeoChatRoom *m_room = nullptr;
|
||||
const Quotient::RoomEvent *m_event = nullptr;
|
||||
|
||||
KFormat m_format;
|
||||
|
||||
DelegateType::Type getDelegateTypeForEvent(const Quotient::RoomEvent *event) const;
|
||||
|
||||
QString getBody(const Quotient::RoomEvent *event, Qt::TextFormat format, bool stripNewlines) const;
|
||||
QString getMessageBody(const Quotient::RoomMessageEvent &event, Qt::TextFormat format, bool stripNewlines) const;
|
||||
|
||||
QVariantMap getMediaInfoForEvent(const Quotient::RoomEvent *event) const;
|
||||
QVariantMap getMediaInfoFromFileInfo(const Quotient::EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail = false) const;
|
||||
};
|
||||
@@ -10,29 +10,29 @@ ImagePackEventContent::ImagePackEventContent(const QJsonObject &json)
|
||||
{
|
||||
if (json.contains(QStringLiteral("pack"))) {
|
||||
pack = ImagePackEventContent::Pack{
|
||||
fromJson<Omittable<QString>>(json["pack"].toObject()["display_name"]),
|
||||
fromJson<Omittable<QUrl>>(json["pack"].toObject()["avatar_url"]),
|
||||
fromJson<Omittable<QStringList>>(json["pack"].toObject()["usage"]),
|
||||
fromJson<Omittable<QString>>(json["pack"].toObject()["attribution"]),
|
||||
fromJson<Omittable<QString>>(json["pack"_ls].toObject()["display_name"_ls]),
|
||||
fromJson<Omittable<QUrl>>(json["pack"_ls].toObject()["avatar_url"_ls]),
|
||||
fromJson<Omittable<QStringList>>(json["pack"_ls].toObject()["usage"_ls]),
|
||||
fromJson<Omittable<QString>>(json["pack"_ls].toObject()["attribution"_ls]),
|
||||
};
|
||||
} else {
|
||||
pack = none;
|
||||
}
|
||||
|
||||
const auto &keys = json["images"].toObject().keys();
|
||||
const auto &keys = json["images"_ls].toObject().keys();
|
||||
for (const auto &k : keys) {
|
||||
Omittable<EventContent::ImageInfo> info;
|
||||
if (json["images"][k].toObject().contains(QStringLiteral("info"))) {
|
||||
info = EventContent::ImageInfo(QUrl(json["images"][k]["url"].toString()), json["images"][k]["info"].toObject(), k);
|
||||
if (json["images"_ls][k].toObject().contains(QStringLiteral("info"))) {
|
||||
info = EventContent::ImageInfo(QUrl(json["images"_ls][k]["url"_ls].toString()), json["images"_ls][k]["info"_ls].toObject(), k);
|
||||
} else {
|
||||
info = none;
|
||||
}
|
||||
images += ImagePackImage{
|
||||
k,
|
||||
fromJson<QUrl>(json["images"][k]["url"].toString()),
|
||||
fromJson<Omittable<QString>>(json["images"][k]["body"]),
|
||||
fromJson<QUrl>(json["images"_ls][k]["url"_ls].toString()),
|
||||
fromJson<Omittable<QString>>(json["images"_ls][k]["body"_ls]),
|
||||
info,
|
||||
fromJson<Omittable<QStringList>>(json["images"][k]["usage"]),
|
||||
fromJson<Omittable<QStringList>>(json["images"_ls][k]["usage"_ls]),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -42,20 +42,20 @@ void ImagePackEventContent::fillJson(QJsonObject *o) const
|
||||
if (pack) {
|
||||
QJsonObject packJson;
|
||||
if (pack->displayName) {
|
||||
packJson["display_name"] = *pack->displayName;
|
||||
packJson["display_name"_ls] = *pack->displayName;
|
||||
}
|
||||
if (pack->usage) {
|
||||
QJsonArray usageJson;
|
||||
for (const auto &usage : *pack->usage) {
|
||||
usageJson += usage;
|
||||
}
|
||||
packJson["usage"] = usageJson;
|
||||
packJson["usage"_ls] = usageJson;
|
||||
}
|
||||
if (pack->avatarUrl) {
|
||||
packJson["avatar_url"] = pack->avatarUrl->toString();
|
||||
packJson["avatar_url"_ls] = pack->avatarUrl->toString();
|
||||
}
|
||||
if (pack->attribution) {
|
||||
packJson["attribution"] = *pack->attribution;
|
||||
packJson["attribution"_ls] = *pack->attribution;
|
||||
}
|
||||
(*o)["pack"_ls] = packJson;
|
||||
}
|
||||
@@ -63,16 +63,16 @@ void ImagePackEventContent::fillJson(QJsonObject *o) const
|
||||
QJsonObject imagesJson;
|
||||
for (const auto &image : images) {
|
||||
QJsonObject imageJson;
|
||||
imageJson["url"] = image.url.toString();
|
||||
imageJson["url"_ls] = image.url.toString();
|
||||
if (image.body) {
|
||||
imageJson["body"] = *image.body;
|
||||
imageJson["body"_ls] = *image.body;
|
||||
}
|
||||
if (image.usage) {
|
||||
QJsonArray usageJson;
|
||||
for (const auto &usage : *image.usage) {
|
||||
usageJson += usage;
|
||||
}
|
||||
imageJson["usage"] = usageJson;
|
||||
imageJson["usage"_ls] = usageJson;
|
||||
}
|
||||
imagesJson[image.shortcode] = imageJson;
|
||||
}
|
||||
|
||||
@@ -12,12 +12,12 @@ PollStartEvent::PollStartEvent(const QJsonObject &obj)
|
||||
|
||||
int PollStartEvent::maxSelections() const
|
||||
{
|
||||
return contentJson()["org.matrix.msc3381.poll.start"]["max_selections"].toInt();
|
||||
return contentJson()["org.matrix.msc3381.poll.start"_ls]["max_selections"_ls].toInt();
|
||||
}
|
||||
|
||||
QString PollStartEvent::question() const
|
||||
{
|
||||
return contentJson()["org.matrix.msc3381.poll.start"]["question"]["body"].toString();
|
||||
return contentJson()["org.matrix.msc3381.poll.start"_ls]["question"_ls]["body"_ls].toString();
|
||||
}
|
||||
|
||||
PollResponseEvent::PollResponseEvent(const QJsonObject &obj)
|
||||
@@ -32,7 +32,7 @@ PollEndEvent::PollEndEvent(const QJsonObject &obj)
|
||||
|
||||
PollResponseEvent::PollResponseEvent(const QString &pollStartEventId, QStringList responses)
|
||||
: RoomEvent(basicJson(TypeId,
|
||||
{{"org.matrix.msc3381.poll.response", QJsonObject{{"answers", QJsonArray::fromStringList(responses)}}},
|
||||
{"m.relates_to", QJsonObject{{"rel_type", "m.reference"}, {"event_id", pollStartEventId}}}}))
|
||||
{{"org.matrix.msc3381.poll.response"_ls, QJsonObject{{"answers"_ls, QJsonArray::fromStringList(responses)}}},
|
||||
{"m.relates_to"_ls, QJsonObject{{"rel_type"_ls, "m.reference"_ls}, {"event_id"_ls, pollStartEventId}}}}))
|
||||
{
|
||||
}
|
||||
|
||||
16
src/jobs/neochatchangepasswordjob.cpp
Normal file
16
src/jobs/neochatchangepasswordjob.cpp
Normal file
@@ -0,0 +1,16 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "neochatchangepasswordjob.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Omittable<QJsonObject> &auth)
|
||||
: BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), "/_matrix/client/r0/account/password")
|
||||
{
|
||||
QJsonObject _data;
|
||||
addParam<>(_data, QStringLiteral("new_password"), newPassword);
|
||||
addParam<IfNotEmpty>(_data, QStringLiteral("logout_devices"), logoutDevices);
|
||||
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
|
||||
setRequestData(_data);
|
||||
}
|
||||
13
src/jobs/neochatchangepasswordjob.h
Normal file
13
src/jobs/neochatchangepasswordjob.h
Normal file
@@ -0,0 +1,13 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Quotient/jobs/basejob.h>
|
||||
#include <Quotient/omittable.h>
|
||||
|
||||
class NeochatChangePasswordJob : public Quotient::BaseJob
|
||||
{
|
||||
public:
|
||||
explicit NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
|
||||
};
|
||||
14
src/jobs/neochatdeactivateaccountjob.cpp
Normal file
14
src/jobs/neochatdeactivateaccountjob.cpp
Normal file
@@ -0,0 +1,14 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "neochatdeactivateaccountjob.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
NeoChatDeactivateAccountJob::NeoChatDeactivateAccountJob(const Omittable<QJsonObject> &auth)
|
||||
: BaseJob(HttpVerb::Post, QStringLiteral("DisableDeviceJob"), "_matrix/client/v3/account/deactivate")
|
||||
{
|
||||
QJsonObject data;
|
||||
addParam<IfNotEmpty>(data, QStringLiteral("auth"), auth);
|
||||
setRequestData(data);
|
||||
}
|
||||
13
src/jobs/neochatdeactivateaccountjob.h
Normal file
13
src/jobs/neochatdeactivateaccountjob.h
Normal file
@@ -0,0 +1,13 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Quotient/jobs/basejob.h>
|
||||
#include <Quotient/omittable.h>
|
||||
|
||||
class NeoChatDeactivateAccountJob : public Quotient::BaseJob
|
||||
{
|
||||
public:
|
||||
explicit NeoChatDeactivateAccountJob(const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
|
||||
};
|
||||
14
src/jobs/neochatdeletedevicejob.cpp
Normal file
14
src/jobs/neochatdeletedevicejob.cpp
Normal file
@@ -0,0 +1,14 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "neochatdeletedevicejob.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Omittable<QJsonObject> &auth)
|
||||
: BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), QStringLiteral("/_matrix/client/r0/devices/%1").arg(deviceId).toLatin1())
|
||||
{
|
||||
QJsonObject _data;
|
||||
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
|
||||
setRequestData(std::move(_data));
|
||||
}
|
||||
13
src/jobs/neochatdeletedevicejob.h
Normal file
13
src/jobs/neochatdeletedevicejob.h
Normal file
@@ -0,0 +1,13 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Quotient/jobs/basejob.h>
|
||||
#include <Quotient/omittable.h>
|
||||
|
||||
class NeochatDeleteDeviceJob : public Quotient::BaseJob
|
||||
{
|
||||
public:
|
||||
explicit NeochatDeleteDeviceJob(const QString &deviceId, const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
|
||||
};
|
||||
@@ -8,17 +8,22 @@
|
||||
#include <Quotient/connection.h>
|
||||
#include <Quotient/csapi/content-repo.h>
|
||||
|
||||
#include "neochatconfig.h"
|
||||
#include "neochatroom.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
LinkPreviewer::LinkPreviewer(QObject *parent, NeoChatRoom *room, const QUrl &url)
|
||||
LinkPreviewer::LinkPreviewer(QObject *parent, const NeoChatRoom *room, const QUrl &url)
|
||||
: QObject(parent)
|
||||
, m_currentRoom(room)
|
||||
, m_loaded(false)
|
||||
, m_url(url)
|
||||
{
|
||||
loadUrlPreview();
|
||||
if (m_currentRoom) {
|
||||
connect(m_currentRoom, &NeoChatRoom::urlPreviewEnabledChanged, this, &LinkPreviewer::loadUrlPreview);
|
||||
}
|
||||
connect(NeoChatConfig::self(), &NeoChatConfig::ShowLinkPreviewChanged, this, &LinkPreviewer::loadUrlPreview);
|
||||
}
|
||||
|
||||
bool LinkPreviewer::loaded() const
|
||||
@@ -57,19 +62,22 @@ void LinkPreviewer::setUrl(QUrl url)
|
||||
|
||||
void LinkPreviewer::loadUrlPreview()
|
||||
{
|
||||
if (!m_currentRoom || !NeoChatConfig::showLinkPreview() || !m_currentRoom->urlPreviewEnabled()) {
|
||||
return;
|
||||
}
|
||||
if (m_url.scheme() == QStringLiteral("https")) {
|
||||
m_loaded = false;
|
||||
Q_EMIT loadedChanged();
|
||||
|
||||
auto conn = m_currentRoom->connection();
|
||||
GetUrlPreviewJob *job = conn->callApi<GetUrlPreviewJob>(m_url.toString());
|
||||
GetUrlPreviewJob *job = conn->callApi<GetUrlPreviewJob>(m_url);
|
||||
|
||||
connect(job, &BaseJob::success, this, [this, job, conn]() {
|
||||
const auto json = job->jsonData();
|
||||
m_title = json["og:title"].toString().trimmed();
|
||||
m_description = json["og:description"].toString().trimmed().replace("\n", " ");
|
||||
m_title = json["og:title"_ls].toString().trimmed();
|
||||
m_description = json["og:description"_ls].toString().trimmed().replace("\n"_ls, " "_ls);
|
||||
|
||||
auto imageUrl = QUrl(json["og:image"].toString());
|
||||
auto imageUrl = QUrl(json["og:image"_ls].toString());
|
||||
if (imageUrl.isValid() && imageUrl.scheme() == QStringLiteral("mxc")) {
|
||||
m_imageSource = conn->makeMediaUrl(imageUrl);
|
||||
} else {
|
||||
@@ -85,4 +93,9 @@ void LinkPreviewer::loadUrlPreview()
|
||||
}
|
||||
}
|
||||
|
||||
bool LinkPreviewer::empty() const
|
||||
{
|
||||
return m_url.isEmpty();
|
||||
}
|
||||
|
||||
#include "moc_linkpreviewer.cpp"
|
||||
|
||||
@@ -44,8 +44,15 @@ class LinkPreviewer : public QObject
|
||||
*/
|
||||
Q_PROPERTY(QUrl imageSource READ imageSource NOTIFY imageSourceChanged)
|
||||
|
||||
/**
|
||||
* @brief Whether the there is a link to preview.
|
||||
*
|
||||
* A linkPreviwer is empty if the URL is empty.
|
||||
*/
|
||||
Q_PROPERTY(bool empty READ empty NOTIFY emptyChanged)
|
||||
|
||||
public:
|
||||
explicit LinkPreviewer(QObject *parent = nullptr, NeoChatRoom *room = nullptr, const QUrl &url = {});
|
||||
explicit LinkPreviewer(QObject *parent = nullptr, const NeoChatRoom *room = nullptr, const QUrl &url = {});
|
||||
|
||||
[[nodiscard]] QUrl url() const;
|
||||
void setUrl(QUrl);
|
||||
@@ -53,9 +60,10 @@ public:
|
||||
[[nodiscard]] QString title() const;
|
||||
[[nodiscard]] QString description() const;
|
||||
[[nodiscard]] QUrl imageSource() const;
|
||||
[[nodiscard]] bool empty() const;
|
||||
|
||||
private:
|
||||
NeoChatRoom *m_currentRoom = nullptr;
|
||||
const NeoChatRoom *m_currentRoom = nullptr;
|
||||
|
||||
bool m_loaded;
|
||||
QString m_title = QString();
|
||||
@@ -71,5 +79,6 @@ Q_SIGNALS:
|
||||
void descriptionChanged();
|
||||
void imageSourceChanged();
|
||||
void urlChanged();
|
||||
void emptyChanged();
|
||||
};
|
||||
Q_DECLARE_METATYPE(LinkPreviewer *)
|
||||
|
||||
@@ -87,32 +87,32 @@ public:
|
||||
QMutexLocker locker(&mutex);
|
||||
QByteArray buf;
|
||||
QTextStream str(&buf);
|
||||
str << QDateTime::currentDateTime().toString(Qt::ISODate) << " [";
|
||||
str << QDateTime::currentDateTime().toString(Qt::ISODate) << QStringLiteral(" [");
|
||||
switch (type) {
|
||||
case QtDebugMsg:
|
||||
str << "DEBUG";
|
||||
str << QStringLiteral("DEBUG");
|
||||
break;
|
||||
case QtInfoMsg:
|
||||
str << "INFO ";
|
||||
str << QStringLiteral("INFO ");
|
||||
break;
|
||||
case QtWarningMsg:
|
||||
str << "WARN ";
|
||||
str << QStringLiteral("WARN ");
|
||||
break;
|
||||
case QtFatalMsg:
|
||||
str << "FATAL";
|
||||
str << QStringLiteral("FATAL");
|
||||
break;
|
||||
case QtCriticalMsg:
|
||||
str << "CRITICAL";
|
||||
str << QStringLiteral("CRITICAL");
|
||||
break;
|
||||
}
|
||||
str << "] " << context.category << ": ";
|
||||
str << QStringLiteral("] ") << context.category << QStringLiteral(": ");
|
||||
if (context.file && *context.file && context.line) {
|
||||
str << context.file << ":" << context.line << ": ";
|
||||
str << context.file << QStringLiteral(":") << context.line << QStringLiteral(": ");
|
||||
}
|
||||
if (context.function && *context.function) {
|
||||
str << context.function << ": ";
|
||||
str << context.function << QStringLiteral(": ");
|
||||
}
|
||||
str << message << "\n";
|
||||
str << message << QStringLiteral("\n");
|
||||
str.flush();
|
||||
file.write(buf.constData(), buf.size());
|
||||
file.flush();
|
||||
@@ -129,18 +129,18 @@ public:
|
||||
if (file.isOpen()) {
|
||||
file.close();
|
||||
}
|
||||
auto filePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QDir::separator() + appName;
|
||||
const auto &filePath = QStringLiteral("%1%2%3").arg(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), QDir::separator(), appName);
|
||||
|
||||
QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QDir::separator());
|
||||
auto entryList = dir.entryList({appName + QStringLiteral(".*")});
|
||||
std::sort(entryList.begin(), entryList.end(), [](const auto &left, const auto &right) {
|
||||
auto leftIndex = left.split(".").last().toInt();
|
||||
auto rightIndex = right.split(".").last().toInt();
|
||||
auto leftIndex = left.split(QStringLiteral(".")).last().toInt();
|
||||
auto rightIndex = right.split(QStringLiteral(".")).last().toInt();
|
||||
return leftIndex > rightIndex;
|
||||
});
|
||||
for (const auto &entry : entryList) {
|
||||
bool ok = false;
|
||||
const auto index = entry.split(".").last().toInt(&ok);
|
||||
const auto index = entry.split(QStringLiteral(".")).last().toInt(&ok);
|
||||
if (!ok) {
|
||||
continue;
|
||||
}
|
||||
@@ -151,8 +151,8 @@ public:
|
||||
file.remove();
|
||||
continue;
|
||||
}
|
||||
const QString newName = filePath + QStringLiteral(".%1").arg(index + 1);
|
||||
const bool success = file.copy(newName);
|
||||
const auto &newName = QStringLiteral("%1.%2").arg(filePath, QString::number(index + 1));
|
||||
const auto success = file.copy(newName);
|
||||
if (success) {
|
||||
file.remove();
|
||||
} else {
|
||||
@@ -216,7 +216,7 @@ void initLogging()
|
||||
oldCategoryFilter = QLoggingCategory::installFilter(filter);
|
||||
oldHandler = qInstallMessageHandler(messageHandler);
|
||||
sInstance->setOrigHandler(oldHandler);
|
||||
sInstance->setName("neochat.log");
|
||||
sInstance->setName(QStringLiteral("neochat.log"));
|
||||
}
|
||||
|
||||
#include "logger.moc"
|
||||
|
||||
@@ -22,7 +22,7 @@ Login::Login(QObject *parent)
|
||||
void Login::init()
|
||||
{
|
||||
m_homeserverReachable = false;
|
||||
m_connection = new Connection();
|
||||
m_connection = new NeoChatConnection();
|
||||
m_matrixId = QString();
|
||||
m_password = QString();
|
||||
m_deviceName = QStringLiteral("NeoChat %1 %2 %3 %4")
|
||||
@@ -33,12 +33,12 @@ void Login::init()
|
||||
|
||||
connect(this, &Login::matrixIdChanged, this, [this]() {
|
||||
setHomeserverReachable(false);
|
||||
QRegularExpression validator("^\\@?[a-zA-Z0-9\\._=\\-/]+\\:[a-zA-Z0-9\\-]+(\\.[a-zA-Z0-9\\-]+)*(\\:[0-9]+)?$");
|
||||
QRegularExpression validator(QStringLiteral("^\\@?[a-zA-Z0-9\\._=\\-/]+\\:[a-zA-Z0-9\\-]+(\\.[a-zA-Z0-9\\-]+)*(\\:[0-9]+)?$"));
|
||||
if (!validator.match(m_matrixId).hasMatch()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_matrixId == "@") {
|
||||
if (m_matrixId == QLatin1Char('@')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ void Login::init()
|
||||
m_testing = true;
|
||||
Q_EMIT testingChanged();
|
||||
if (!m_connection) {
|
||||
m_connection = new Connection();
|
||||
m_connection = new NeoChatConnection();
|
||||
}
|
||||
m_connection->resolveServer(m_matrixId);
|
||||
connectSingleShot(m_connection, &Connection::loginFlowsChanged, this, [this]() {
|
||||
@@ -87,7 +87,11 @@ void Login::init()
|
||||
Q_EMIT isLoggingInChanged();
|
||||
});
|
||||
connect(m_connection, &Connection::loginError, this, [this](QString error, const QString &) {
|
||||
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
|
||||
if (error == QStringLiteral("Invalid username or password")) {
|
||||
setInvalidPassword(true);
|
||||
} else {
|
||||
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
|
||||
}
|
||||
m_isLoggingIn = false;
|
||||
Q_EMIT isLoggingInChanged();
|
||||
});
|
||||
@@ -120,8 +124,8 @@ QString Login::matrixId() const
|
||||
void Login::setMatrixId(const QString &matrixId)
|
||||
{
|
||||
m_matrixId = matrixId;
|
||||
if (!m_matrixId.startsWith('@')) {
|
||||
m_matrixId.prepend('@');
|
||||
if (!m_matrixId.startsWith(QLatin1Char('@'))) {
|
||||
m_matrixId.prepend(QLatin1Char('@'));
|
||||
}
|
||||
Q_EMIT matrixIdChanged();
|
||||
}
|
||||
@@ -133,6 +137,7 @@ QString Login::password() const
|
||||
|
||||
void Login::setPassword(const QString &password)
|
||||
{
|
||||
setInvalidPassword(false);
|
||||
m_password = password;
|
||||
Q_EMIT passwordChanged();
|
||||
}
|
||||
@@ -155,7 +160,7 @@ void Login::login()
|
||||
|
||||
// Some servers do not have a .well_known file. So we login via the username part from the mxid,
|
||||
// rather than with the full mxid, as that would lead to an invalid user.
|
||||
auto username = m_matrixId.mid(1, m_matrixId.indexOf(":") - 1);
|
||||
auto username = m_matrixId.mid(1, m_matrixId.indexOf(QLatin1Char(':')) - 1);
|
||||
m_connection->loginWithPassword(username, m_password, m_deviceName, QString());
|
||||
}
|
||||
|
||||
@@ -199,4 +204,15 @@ bool Login::isLoggedIn() const
|
||||
return m_isLoggedIn;
|
||||
}
|
||||
|
||||
void Login::setInvalidPassword(bool invalid)
|
||||
{
|
||||
m_invalidPassword = invalid;
|
||||
Q_EMIT isInvalidPasswordChanged();
|
||||
}
|
||||
|
||||
bool Login::isInvalidPassword() const
|
||||
{
|
||||
return m_invalidPassword;
|
||||
}
|
||||
|
||||
#include "moc_login.cpp"
|
||||
|
||||
17
src/login.h
17
src/login.h
@@ -6,10 +6,7 @@
|
||||
#include <QObject>
|
||||
#include <QUrl>
|
||||
|
||||
namespace Quotient
|
||||
{
|
||||
class Connection;
|
||||
}
|
||||
class NeoChatConnection;
|
||||
|
||||
/**
|
||||
* @class Login
|
||||
@@ -73,6 +70,11 @@ class Login : public QObject
|
||||
*/
|
||||
Q_PROPERTY(bool isLoggedIn READ isLoggedIn NOTIFY isLoggedInChanged)
|
||||
|
||||
/**
|
||||
* @brief Whether the password (or the username) is invalid.
|
||||
*/
|
||||
Q_PROPERTY(bool isInvalidPassword READ isInvalidPassword NOTIFY isInvalidPasswordChanged)
|
||||
|
||||
public:
|
||||
explicit Login(QObject *parent = nullptr);
|
||||
|
||||
@@ -100,6 +102,9 @@ public:
|
||||
|
||||
bool isLoggedIn() const;
|
||||
|
||||
bool isInvalidPassword() const;
|
||||
void setInvalidPassword(bool invalid);
|
||||
|
||||
Q_INVOKABLE void login();
|
||||
Q_INVOKABLE void loginWithSso();
|
||||
|
||||
@@ -116,6 +121,7 @@ Q_SIGNALS:
|
||||
void testingChanged();
|
||||
void isLoggingInChanged();
|
||||
void isLoggedInChanged();
|
||||
void isInvalidPasswordChanged();
|
||||
|
||||
private:
|
||||
void setHomeserverReachable(bool reachable);
|
||||
@@ -126,9 +132,10 @@ private:
|
||||
QString m_deviceName;
|
||||
bool m_supportsSso = false;
|
||||
bool m_supportsPassword = false;
|
||||
Quotient::Connection *m_connection = nullptr;
|
||||
NeoChatConnection *m_connection = nullptr;
|
||||
QUrl m_ssoUrl;
|
||||
bool m_testing = false;
|
||||
bool m_isLoggingIn = false;
|
||||
bool m_isLoggedIn = false;
|
||||
bool m_invalidPassword = false;
|
||||
};
|
||||
|
||||
84
src/main.cpp
84
src/main.cpp
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <QCommandLineParser>
|
||||
#include <QIcon>
|
||||
#include <QNetworkDiskCache>
|
||||
#include <QNetworkProxyFactory>
|
||||
#include <QObject>
|
||||
#include <QQmlApplicationEngine>
|
||||
@@ -17,6 +18,10 @@
|
||||
#include <QApplication>
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_WEBVIEW
|
||||
#include <QtWebView>
|
||||
#endif
|
||||
|
||||
#include <KAboutData>
|
||||
#ifdef HAVE_KDBUSADDONS
|
||||
#include <KDBusService>
|
||||
@@ -29,10 +34,11 @@
|
||||
|
||||
#include "neochat-version.h"
|
||||
|
||||
#include <Quotient/keyverificationsession.h>
|
||||
#include <Quotient/accountregistry.h>
|
||||
#include <Quotient/keyverificationsession.h>
|
||||
#include <Quotient/networkaccessmanager.h>
|
||||
#include <Quotient/room.h>
|
||||
#include <Quotient/user.h>
|
||||
#include <Quotient/util.h>
|
||||
|
||||
#include "actionshandler.h"
|
||||
@@ -41,14 +47,15 @@
|
||||
#include "clipboard.h"
|
||||
#include "controller.h"
|
||||
#include "delegatesizehelper.h"
|
||||
#include "enums/delegatetype.h"
|
||||
#include "filetypesingleton.h"
|
||||
#include "linkpreviewer.h"
|
||||
#include "locationhelper.h"
|
||||
#include "logger.h"
|
||||
#include "login.h"
|
||||
#include "matriximageprovider.h"
|
||||
#include "mediasizehelper.h"
|
||||
#include "models/accountemoticonmodel.h"
|
||||
#include "models/collapsestateproxymodel.h"
|
||||
#include "models/customemojimodel.h"
|
||||
#include "models/devicesmodel.h"
|
||||
#include "models/devicesproxymodel.h"
|
||||
@@ -75,8 +82,8 @@
|
||||
#include "models/userlistmodel.h"
|
||||
#include "models/webshortcutmodel.h"
|
||||
#include "neochatconfig.h"
|
||||
#include "neochatconnection.h"
|
||||
#include "neochatroom.h"
|
||||
#include "neochatuser.h"
|
||||
#include "notificationsmanager.h"
|
||||
#include "pollhandler.h"
|
||||
#include "roommanager.h"
|
||||
@@ -89,24 +96,36 @@
|
||||
#endif
|
||||
#include "models/completionmodel.h"
|
||||
#include "models/statemodel.h"
|
||||
#include "neochatuser.h"
|
||||
|
||||
#ifdef HAVE_RUNNER
|
||||
#include "runner.h"
|
||||
#include <QDBusConnection>
|
||||
#endif
|
||||
#include "registration.h"
|
||||
|
||||
#ifdef Q_OS_WINDOWS
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
#include "messageformatter.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
class NetworkAccessManagerFactory : public QQmlNetworkAccessManagerFactory
|
||||
{
|
||||
QNetworkAccessManager *create(QObject *) override
|
||||
{
|
||||
return NetworkAccessManager::instance();
|
||||
auto nam = NetworkAccessManager::instance();
|
||||
nam->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
|
||||
nam->enableStrictTransportSecurityStore(true, QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1String("/hsts/"));
|
||||
nam->setStrictTransportSecurityEnabled(true);
|
||||
|
||||
auto namDiskCache = new QNetworkDiskCache(nam);
|
||||
namDiskCache->setCacheDirectory(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1String("/nam/"));
|
||||
nam->setCache(namDiskCache);
|
||||
|
||||
return nam;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -129,11 +148,19 @@ int main(int argc, char *argv[])
|
||||
|
||||
QNetworkProxyFactory::setUseSystemConfiguration(true);
|
||||
|
||||
#ifdef HAVE_WEBVIEW
|
||||
QtWebView::initialize();
|
||||
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
|
||||
#if QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
|
||||
QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGLRhi);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
QGuiApplication app(argc, argv);
|
||||
QQuickStyle::setStyle(QStringLiteral("org.kde.breeze"));
|
||||
#else
|
||||
QIcon::setFallbackThemeName("breeze");
|
||||
QIcon::setFallbackThemeName("breeze"_ls);
|
||||
QApplication app(argc, argv);
|
||||
// Default to org.kde.desktop style unless the user forces another style
|
||||
if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) {
|
||||
@@ -155,7 +182,7 @@ int main(int argc, char *argv[])
|
||||
#endif
|
||||
KLocalizedString::setApplicationDomain("neochat");
|
||||
|
||||
QGuiApplication::setOrganizationName("KDE");
|
||||
QGuiApplication::setOrganizationName("KDE"_ls);
|
||||
|
||||
KAboutData about(QStringLiteral("neochat"),
|
||||
i18n("NeoChat"),
|
||||
@@ -192,7 +219,8 @@ int main(int argc, char *argv[])
|
||||
#ifdef NEOCHAT_FLATPAK
|
||||
// Copy over the included FontConfig configuration to the
|
||||
// app's config dir:
|
||||
QFile::copy("/app/etc/fonts/conf.d/99-noto-mono-color-emoji.conf", "/var/config/fontconfig/conf.d/99-noto-mono-color-emoji.conf");
|
||||
QFile::copy(QStringLiteral("/app/etc/fonts/conf.d/99-noto-mono-color-emoji.conf"),
|
||||
QStringLiteral("/var/config/fontconfig/conf.d/99-noto-mono-color-emoji.conf"));
|
||||
#endif
|
||||
|
||||
Clipboard clipboard;
|
||||
@@ -202,6 +230,12 @@ int main(int argc, char *argv[])
|
||||
Login *login = new Login();
|
||||
UrlHelper urlHelper;
|
||||
|
||||
MessageFormatter formatter;
|
||||
// formatter.formatInternal("<p>hrrejoire</p>\n<pre><code class=\"language-js\">var i = 0; i++; function\n</code></pre>\n<p>rekore</p>\n", new
|
||||
// QTextDocument);
|
||||
|
||||
// return 0;
|
||||
|
||||
#ifdef HAVE_COLORSCHEME
|
||||
ColorSchemer colorScheme;
|
||||
qmlRegisterSingletonInstance<ColorSchemer>("org.kde.neochat", 1, 0, "ColorSchemer", &colorScheme);
|
||||
@@ -210,6 +244,7 @@ int main(int argc, char *argv[])
|
||||
}
|
||||
#endif
|
||||
|
||||
qmlRegisterSingletonInstance<MessageFormatter>("org.kde.neochat", 1, 0, "MessageFormatter", &formatter);
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Controller", &Controller::instance());
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "NotificationsManager", &NotificationsManager::instance());
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Clipboard", &clipboard);
|
||||
@@ -222,6 +257,7 @@ int main(int argc, char *argv[])
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "AccountRegistry", &Controller::instance().accounts());
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "SpaceHierarchyCache", &SpaceHierarchyCache::instance());
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "CustomEmojiModel", &CustomEmojiModel::instance());
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Registration", &Registration::instance());
|
||||
qmlRegisterType<ActionsHandler>("org.kde.neochat", 1, 0, "ActionsHandler");
|
||||
qmlRegisterType<ChatDocumentHandler>("org.kde.neochat", 1, 0, "ChatDocumentHandler");
|
||||
qmlRegisterType<RoomListModel>("org.kde.neochat", 1, 0, "RoomListModel");
|
||||
@@ -229,7 +265,6 @@ int main(int argc, char *argv[])
|
||||
qmlRegisterType<UserListModel>("org.kde.neochat", 1, 0, "UserListModel");
|
||||
qmlRegisterType<MessageEventModel>("org.kde.neochat", 1, 0, "MessageEventModel");
|
||||
qmlRegisterType<ReactionModel>("org.kde.neochat", 1, 0, "ReactionModel");
|
||||
qmlRegisterType<CollapseStateProxyModel>("org.kde.neochat", 1, 0, "CollapseStateProxyModel");
|
||||
qmlRegisterType<MediaMessageFilterModel>("org.kde.neochat", 1, 0, "MediaMessageFilterModel");
|
||||
qmlRegisterType<MessageFilterModel>("org.kde.neochat", 1, 0, "MessageFilterModel");
|
||||
qmlRegisterType<UserFilterModel>("org.kde.neochat", 1, 0, "UserFilterModel");
|
||||
@@ -254,34 +289,33 @@ int main(int argc, char *argv[])
|
||||
qmlRegisterType<AccountEmoticonModel>("org.kde.neochat", 1, 0, "AccountEmoticonModel");
|
||||
qmlRegisterType<EmoticonFilterModel>("org.kde.neochat", 1, 0, "EmoticonFilterModel");
|
||||
qmlRegisterType<DelegateSizeHelper>("org.kde.neochat", 1, 0, "DelegateSizeHelper");
|
||||
qmlRegisterUncreatableType<RoomMessageEvent>("org.kde.neochat", 1, 0, "RoomMessageEvent", "ENUM");
|
||||
qmlRegisterUncreatableType<PushNotificationKind>("org.kde.neochat", 1, 0, "PushNotificationKind", "ENUM");
|
||||
qmlRegisterUncreatableType<PushNotificationSection>("org.kde.neochat", 1, 0, "PushNotificationSection", "ENUM");
|
||||
qmlRegisterUncreatableType<PushNotificationState>("org.kde.neochat", 1, 0, "PushNotificationState", "ENUM");
|
||||
qmlRegisterUncreatableType<PushNotificationAction>("org.kde.neochat", 1, 0, "PushNotificationAction", "ENUM");
|
||||
qmlRegisterUncreatableType<NeoChatRoomType>("org.kde.neochat", 1, 0, "NeoChatRoomType", "ENUM");
|
||||
qmlRegisterUncreatableType<NeoChatUser>("org.kde.neochat", 1, 0, "NeoChatUser", {});
|
||||
qmlRegisterType<MediaSizeHelper>("org.kde.neochat", 1, 0, "MediaSizeHelper");
|
||||
qmlRegisterUncreatableType<DelegateType>("org.kde.neochat", 1, 0, "DelegateType", "ENUM"_ls);
|
||||
qmlRegisterUncreatableType<PushNotificationKind>("org.kde.neochat", 1, 0, "PushNotificationKind", "ENUM"_ls);
|
||||
qmlRegisterUncreatableType<PushNotificationSection>("org.kde.neochat", 1, 0, "PushNotificationSection", "ENUM"_ls);
|
||||
qmlRegisterUncreatableType<PushNotificationState>("org.kde.neochat", 1, 0, "PushNotificationState", "ENUM"_ls);
|
||||
qmlRegisterUncreatableType<PushNotificationAction>("org.kde.neochat", 1, 0, "PushNotificationAction", "ENUM"_ls);
|
||||
qmlRegisterUncreatableType<NeoChatRoomType>("org.kde.neochat", 1, 0, "NeoChatRoomType", "ENUM"_ls);
|
||||
qmlRegisterUncreatableType<User>("org.kde.neochat", 1, 0, "User", {});
|
||||
qmlRegisterUncreatableType<NeoChatRoom>("org.kde.neochat", 1, 0, "NeoChatRoom", {});
|
||||
qmlRegisterUncreatableType<NeoChatConnection>("org.kde.neochat", 1, 0, "NeoChatConnection", {});
|
||||
|
||||
qRegisterMetaType<User *>("User*");
|
||||
qRegisterMetaType<User *>("const User*");
|
||||
qRegisterMetaType<User *>("const Quotient::User*");
|
||||
qRegisterMetaType<Room *>("Room*");
|
||||
qRegisterMetaType<Connection *>("Connection*");
|
||||
qRegisterMetaType<MessageEventType>("MessageEventType");
|
||||
qRegisterMetaType<NeoChatRoom *>("NeoChatRoom*");
|
||||
qRegisterMetaType<NeoChatUser *>("NeoChatUser*");
|
||||
qRegisterMetaType<User *>("User*");
|
||||
qRegisterMetaType<GetRoomEventsJob *>("GetRoomEventsJob*");
|
||||
qRegisterMetaType<QMimeType>("QMimeType");
|
||||
#ifdef Quotient_E2EE_ENABLED
|
||||
qRegisterMetaType<KeyVerificationSession *>("KeyVerificationSession*");
|
||||
qmlRegisterUncreatableType<KeyVerificationSession>("org.kde.neochat", 1, 0, "KeyVerificationSession", {});
|
||||
qRegisterMetaType<QVector<EmojiEntry>>("QVector<EmojiEntry>");
|
||||
#endif
|
||||
qmlRegisterSingletonType("org.kde.neochat", 1, 0, "About", [](QQmlEngine *engine, QJSEngine *) -> QJSValue {
|
||||
return engine->toScriptValue(KAboutData::applicationData());
|
||||
});
|
||||
qmlRegisterSingletonType(QUrl("qrc:/OsmLocationPlugin.qml"), "org.kde.neochat", 1, 0, "OsmLocationPlugin");
|
||||
qmlRegisterSingletonType(QUrl("qrc:/OsmLocationPlugin.qml"_ls), "org.kde.neochat", 1, 0, "OsmLocationPlugin");
|
||||
qmlRegisterSingletonType("org.kde.neochat", 1, 0, "LocationHelper", [](QQmlEngine *engine, QJSEngine *) -> QJSValue {
|
||||
return engine->toScriptValue(LocationHelper());
|
||||
});
|
||||
@@ -324,13 +358,13 @@ int main(int argc, char *argv[])
|
||||
QCommandLineParser parser;
|
||||
parser.setApplicationDescription(i18n("Client for the matrix communication protocol"));
|
||||
parser.addPositionalArgument(QStringLiteral("urls"), i18n("Supports matrix: url scheme"));
|
||||
parser.addOption(QCommandLineOption("ignore-ssl-errors", i18n("Ignore all SSL Errors, e.g., unsigned certificates.")));
|
||||
parser.addOption(QCommandLineOption("ignore-ssl-errors"_ls, i18n("Ignore all SSL Errors, e.g., unsigned certificates.")));
|
||||
|
||||
about.setupCommandLine(&parser);
|
||||
parser.process(app);
|
||||
about.processCommandLine(&parser);
|
||||
|
||||
if (parser.isSet("ignore-ssl-errors")) {
|
||||
if (parser.isSet("ignore-ssl-errors"_ls)) {
|
||||
QObject::connect(NetworkAccessManager::instance(), &QNetworkAccessManager::sslErrors, NetworkAccessManager::instance(), [](QNetworkReply *reply) {
|
||||
reply->ignoreSslErrors();
|
||||
});
|
||||
@@ -344,13 +378,13 @@ int main(int argc, char *argv[])
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (parser.positionalArguments().length() > 0) {
|
||||
if (!parser.positionalArguments().isEmpty()) {
|
||||
RoomManager::instance().setUrlArgument(parser.positionalArguments()[0]);
|
||||
}
|
||||
|
||||
#ifdef HAVE_RUNNER
|
||||
Runner runner;
|
||||
QDBusConnection::sessionBus().registerObject("/RoomRunner", &runner, QDBusConnection::ExportScriptableContents);
|
||||
QDBusConnection::sessionBus().registerObject("/RoomRunner"_ls, &runner, QDBusConnection::ExportScriptableContents);
|
||||
#endif
|
||||
|
||||
QWindow *window = windowFromEngine(&engine);
|
||||
|
||||
@@ -25,18 +25,24 @@ ThumbnailResponse::ThumbnailResponse(QString id, QSize size)
|
||||
mediaId,
|
||||
QString::number(requestedSize.width()),
|
||||
QString::number(requestedSize.height())))
|
||||
, errorStr("Image request hasn't started")
|
||||
, errorStr("Image request hasn't started"_ls)
|
||||
{
|
||||
if (requestedSize.isEmpty()) {
|
||||
requestedSize.setHeight(100);
|
||||
requestedSize.setWidth(100);
|
||||
}
|
||||
if (mediaId.count('/') != 1) {
|
||||
errorStr = i18n("Media id '%1' doesn't follow server/mediaId pattern", mediaId);
|
||||
Q_EMIT finished();
|
||||
return;
|
||||
if (mediaId.count(QLatin1Char('/')) != 1) {
|
||||
if (mediaId.startsWith(QLatin1Char('/'))) {
|
||||
mediaId = mediaId.mid(1);
|
||||
} else {
|
||||
errorStr = i18n("Media id '%1' doesn't follow server/mediaId pattern", mediaId);
|
||||
Q_EMIT finished();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
mediaId = mediaId.split(QLatin1Char('?'))[0];
|
||||
|
||||
QImage cachedImage;
|
||||
if (cachedImage.load(localFile)) {
|
||||
image = cachedImage;
|
||||
|
||||
@@ -10,11 +10,6 @@
|
||||
|
||||
#include <QReadWriteLock>
|
||||
|
||||
namespace Quotient
|
||||
{
|
||||
class Connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @class ThumbnailResponse
|
||||
*
|
||||
@@ -34,7 +29,7 @@ private Q_SLOTS:
|
||||
void prepareResult();
|
||||
|
||||
private:
|
||||
const QString mediaId;
|
||||
QString mediaId;
|
||||
QSize requestedSize;
|
||||
const QString localFile;
|
||||
Quotient::MediaThumbnailJob *job = nullptr;
|
||||
|
||||
163
src/mediasizehelper.cpp
Normal file
163
src/mediasizehelper.cpp
Normal file
@@ -0,0 +1,163 @@
|
||||
// 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 "mediasizehelper.h"
|
||||
|
||||
#include "neochatconfig.h"
|
||||
|
||||
MediaSizeHelper::MediaSizeHelper(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::contentMaxWidth() const
|
||||
{
|
||||
return m_contentMaxWidth;
|
||||
}
|
||||
|
||||
void MediaSizeHelper::setContentMaxWidth(qreal contentMaxWidth)
|
||||
{
|
||||
if (contentMaxWidth < 0.0 || qFuzzyCompare(contentMaxWidth, 0.0)) {
|
||||
m_contentMaxWidth = -1.0;
|
||||
Q_EMIT contentMaxWidthChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
return;
|
||||
}
|
||||
if (qFuzzyCompare(contentMaxWidth, m_contentMaxWidth)) {
|
||||
return;
|
||||
}
|
||||
m_contentMaxWidth = contentMaxWidth;
|
||||
Q_EMIT contentMaxWidthChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::contentMaxHeight() const
|
||||
{
|
||||
return m_contentMaxHeight;
|
||||
}
|
||||
|
||||
void MediaSizeHelper::setContentMaxHeight(qreal contentMaxHeight)
|
||||
{
|
||||
if (contentMaxHeight < 0.0 || qFuzzyCompare(contentMaxHeight, 0.0)) {
|
||||
m_contentMaxHeight = -1.0;
|
||||
Q_EMIT contentMaxHeightChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
return;
|
||||
}
|
||||
if (qFuzzyCompare(contentMaxHeight, m_contentMaxHeight)) {
|
||||
return;
|
||||
}
|
||||
m_contentMaxHeight = contentMaxHeight;
|
||||
Q_EMIT contentMaxHeightChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::mediaWidth() const
|
||||
{
|
||||
return m_mediaWidth;
|
||||
}
|
||||
|
||||
void MediaSizeHelper::setMediaWidth(qreal mediaWidth)
|
||||
{
|
||||
if (mediaWidth < 0.0 || qFuzzyCompare(mediaWidth, 0.0)) {
|
||||
m_mediaWidth = -1.0;
|
||||
Q_EMIT mediaWidthChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
return;
|
||||
}
|
||||
if (qFuzzyCompare(mediaWidth, m_mediaWidth)) {
|
||||
return;
|
||||
}
|
||||
m_mediaWidth = mediaWidth;
|
||||
Q_EMIT mediaWidthChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::mediaHeight() const
|
||||
{
|
||||
return m_mediaHeight;
|
||||
}
|
||||
|
||||
void MediaSizeHelper::setMediaHeight(qreal mediaHeight)
|
||||
{
|
||||
if (mediaHeight < 0.0 || qFuzzyCompare(mediaHeight, 0.0)) {
|
||||
m_mediaHeight = -1.0;
|
||||
Q_EMIT mediaHeightChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
return;
|
||||
}
|
||||
if (qFuzzyCompare(mediaHeight, m_mediaHeight)) {
|
||||
return;
|
||||
}
|
||||
m_mediaHeight = mediaHeight;
|
||||
Q_EMIT mediaHeightChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::resolvedMediaWidth() const
|
||||
{
|
||||
if (m_mediaWidth > 0.0) {
|
||||
return m_mediaWidth;
|
||||
}
|
||||
return widthLimit();
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::resolvedMediaHeight() const
|
||||
{
|
||||
if (m_mediaHeight > 0.0) {
|
||||
return m_mediaHeight;
|
||||
}
|
||||
return widthLimit() / 16.0 * 9.0;
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::aspectRatio() const
|
||||
{
|
||||
return resolvedMediaWidth() / resolvedMediaHeight();
|
||||
}
|
||||
|
||||
bool MediaSizeHelper::limitWidth() const
|
||||
{
|
||||
// If actual data isn't available we'll be using a placeholder that is width
|
||||
// limited so return true.
|
||||
if (m_mediaWidth < 0.0 || m_mediaHeight < 0.0) {
|
||||
return true;
|
||||
}
|
||||
return m_mediaWidth >= m_mediaHeight;
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::widthLimit() const
|
||||
{
|
||||
if (m_contentMaxWidth < 0.0) {
|
||||
return NeoChatConfig::self()->mediaMaxWidth();
|
||||
}
|
||||
return std::min(m_contentMaxWidth, qreal(NeoChatConfig::self()->mediaMaxWidth()));
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::heightLimit() const
|
||||
{
|
||||
if (m_contentMaxHeight < 0.0) {
|
||||
return NeoChatConfig::self()->mediaMaxHeight();
|
||||
}
|
||||
return std::min(m_contentMaxHeight, qreal(NeoChatConfig::self()->mediaMaxHeight()));
|
||||
}
|
||||
|
||||
QSize MediaSizeHelper::currentSize() const
|
||||
{
|
||||
if (limitWidth()) {
|
||||
qreal width = std::min(widthLimit(), resolvedMediaWidth());
|
||||
qreal height = width / aspectRatio();
|
||||
if (height > heightLimit()) {
|
||||
return QSize(qRound(heightLimit() * aspectRatio()), qRound(heightLimit()));
|
||||
}
|
||||
return QSize(qRound(width), qRound(height));
|
||||
} else {
|
||||
qreal height = std::min(heightLimit(), resolvedMediaHeight());
|
||||
qreal width = height * aspectRatio();
|
||||
if (width > widthLimit()) {
|
||||
return QSize(qRound(widthLimit()), qRound(widthLimit() / aspectRatio()));
|
||||
}
|
||||
return QSize(qRound(width), qRound(height));
|
||||
}
|
||||
}
|
||||
|
||||
#include "moc_mediasizehelper.cpp"
|
||||
103
src/mediasizehelper.h
Normal file
103
src/mediasizehelper.h
Normal file
@@ -0,0 +1,103 @@
|
||||
// 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 <QSize>
|
||||
|
||||
/**
|
||||
* @class MediaSizeHelper
|
||||
*
|
||||
* A class to help calculate the current width of a media item within a chat delegate.
|
||||
*
|
||||
* The only realistic way to guarantee that a media item (e.g. an image or video)
|
||||
* is the correct size in QML is to calculate the size manually.
|
||||
*
|
||||
* The rules for this component work as follows:
|
||||
* - The output will always try to keep the media size if no limits are breached.
|
||||
* - If no media width is set, the current size will be a placeholder at a 16:9 ratio
|
||||
* calcualated from either the configured max width or the contentMaxWidth, whichever
|
||||
* is smaller (if the contentMaxWidth isn't set, the configured max width is used).
|
||||
* - The aspect ratio of the media will always be maintained if set (otherwise 16:9).
|
||||
* - The current size will never be larger than any of the limits in either direction.
|
||||
* - If any limit is breached the image size will be reduced while maintaining aspect
|
||||
* ration, i.e. no stretching or squashing. This can mean that the width or height
|
||||
* is reduced even if that parameter doesn't breach the limit itself.
|
||||
*/
|
||||
class MediaSizeHelper : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
/**
|
||||
* @brief The maximum width (in px) the media can be.
|
||||
*
|
||||
* This is the upper limit placed upon the media by the delegate.
|
||||
*/
|
||||
Q_PROPERTY(qreal contentMaxWidth READ contentMaxWidth WRITE setContentMaxWidth NOTIFY contentMaxWidthChanged)
|
||||
|
||||
/**
|
||||
* @brief The maximum height (in px) the media can be.
|
||||
*
|
||||
* This is the upper limit placed upon the media by the delegate.
|
||||
*/
|
||||
Q_PROPERTY(qreal contentMaxHeight READ contentMaxHeight WRITE setContentMaxHeight NOTIFY contentMaxHeightChanged)
|
||||
|
||||
/**
|
||||
* @brief The base width (in px) of the media.
|
||||
*/
|
||||
Q_PROPERTY(qreal mediaWidth READ mediaWidth WRITE setMediaWidth NOTIFY mediaWidthChanged)
|
||||
|
||||
/**
|
||||
* @brief The base height (in px) of the media.
|
||||
*/
|
||||
Q_PROPERTY(qreal mediaHeight READ mediaHeight WRITE setMediaHeight NOTIFY mediaHeightChanged)
|
||||
|
||||
/**
|
||||
* @brief The size (in px) of the component based on the current input.
|
||||
*
|
||||
* Will always try to return a value even if some of the inputs are not set to
|
||||
* account for being called before the parameters are intialised. For any parameters
|
||||
* not set these will just be left out of the calcs.
|
||||
*
|
||||
* If no input values are provided a default placeholder value will be returned.
|
||||
*/
|
||||
Q_PROPERTY(QSize currentSize READ currentSize NOTIFY currentSizeChanged)
|
||||
|
||||
public:
|
||||
explicit MediaSizeHelper(QObject *parent = nullptr);
|
||||
|
||||
qreal contentMaxWidth() const;
|
||||
void setContentMaxWidth(qreal contentMaxWidth);
|
||||
|
||||
qreal contentMaxHeight() const;
|
||||
void setContentMaxHeight(qreal contentMaxHeight);
|
||||
|
||||
qreal mediaWidth() const;
|
||||
void setMediaWidth(qreal mediaWidth);
|
||||
|
||||
qreal mediaHeight() const;
|
||||
void setMediaHeight(qreal mediaHeight);
|
||||
|
||||
QSize currentSize() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void contentMaxWidthChanged();
|
||||
void contentMaxHeightChanged();
|
||||
void mediaWidthChanged();
|
||||
void mediaHeightChanged();
|
||||
void currentSizeChanged();
|
||||
|
||||
private:
|
||||
qreal m_contentMaxWidth = -1.0;
|
||||
qreal m_contentMaxHeight = -1.0;
|
||||
qreal m_mediaWidth = -1.0;
|
||||
qreal m_mediaHeight = -1.0;
|
||||
|
||||
qreal resolvedMediaWidth() const;
|
||||
qreal resolvedMediaHeight() const;
|
||||
qreal aspectRatio() const;
|
||||
bool limitWidth() const;
|
||||
qreal widthLimit() const;
|
||||
qreal heightLimit() const;
|
||||
};
|
||||
133
src/messageformatter.cpp
Normal file
133
src/messageformatter.cpp
Normal file
@@ -0,0 +1,133 @@
|
||||
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "messageformatter.h"
|
||||
|
||||
#include <QDomDocument>
|
||||
#include <QGuiApplication>
|
||||
#include <QPalette>
|
||||
#include <QQmlContext>
|
||||
#include <QQmlProperty>
|
||||
#include <QTextCursor>
|
||||
#include <QTextDocumentFragment>
|
||||
|
||||
#include <KSyntaxHighlighting/definition.h>
|
||||
#include <KSyntaxHighlighting/repository.h>
|
||||
#include <KSyntaxHighlighting/syntaxhighlighter.h>
|
||||
#include <KSyntaxHighlighting/theme.h>
|
||||
|
||||
QTextDocumentFragment copyTextLayoutFrom(QTextDocument *document)
|
||||
{
|
||||
QTextCursor sourceCursor(document);
|
||||
sourceCursor.select(QTextCursor::Document);
|
||||
|
||||
QTextDocument helper;
|
||||
|
||||
// copy the content fragment from the source document into our helper document
|
||||
QTextCursor curs(&helper);
|
||||
curs.insertFragment(sourceCursor.selection());
|
||||
curs.select(QTextCursor::Document);
|
||||
|
||||
// not sure why, but fonts get lost. since this is for codeblocks, we can
|
||||
// just force the mono font. anyone copying this code would probably want
|
||||
// to fix the problem proper if it's not also for codeblocks.
|
||||
const auto fixedFont = QFontDatabase::systemFont(QFontDatabase::FixedFont);
|
||||
|
||||
const int docStart = sourceCursor.selectionStart();
|
||||
const int docEnd = helper.characterCount() - 1;
|
||||
|
||||
// since the copied fragment above lost the qsyntaxhighlighter stuff,
|
||||
// we gotta go through the qtextlayout and apply those styles to the
|
||||
// document
|
||||
const auto end = document->findBlock(sourceCursor.selectionEnd()).next();
|
||||
for (auto current = document->findBlock(docStart); current.isValid() && current != end; current = current.next()) {
|
||||
const auto layout = current.layout();
|
||||
|
||||
// iterate through the formats, applying them to our document
|
||||
for (const auto &span : layout->formats()) {
|
||||
const int start = current.position() + span.start - docStart;
|
||||
const int end = start + span.length;
|
||||
|
||||
curs.setPosition(qMax(start, 0));
|
||||
curs.setPosition(qMin(end, docEnd), QTextCursor::KeepAnchor);
|
||||
|
||||
auto fmt = span.format;
|
||||
fmt.setFont(fixedFont);
|
||||
|
||||
curs.setCharFormat(fmt);
|
||||
}
|
||||
}
|
||||
|
||||
return QTextDocumentFragment(&helper);
|
||||
}
|
||||
|
||||
QTextDocumentFragment highlight(const QString &code, const QString &language)
|
||||
{
|
||||
using namespace KSyntaxHighlighting;
|
||||
|
||||
static Repository repo;
|
||||
|
||||
auto theme = repo.themeForPalette(QGuiApplication::palette());
|
||||
auto definition = repo.definitionForFileName(QLatin1String("file.") + language);
|
||||
|
||||
QTextDocument doku(code);
|
||||
|
||||
QScopedPointer<SyntaxHighlighter> highlighter(new SyntaxHighlighter(&doku));
|
||||
highlighter->setTheme(theme);
|
||||
highlighter->setDefinition(definition);
|
||||
|
||||
return copyTextLayoutFrom(&doku);
|
||||
}
|
||||
|
||||
bool extractCodeBlock(QTextCursor cursor, QDomElement element)
|
||||
{
|
||||
const auto codeNode = element.firstChild();
|
||||
if (!codeNode.isNull()) {
|
||||
const auto code = codeNode.toElement();
|
||||
if (!code.isNull() && code.tagName() == QLatin1String("code")) {
|
||||
QString lang;
|
||||
auto langClass = code.attribute(QLatin1String("class"), QLatin1String("none"));
|
||||
if (langClass != QLatin1String("none") && langClass.startsWith(QLatin1String("language-"))) {
|
||||
lang = langClass.remove(0, 9);
|
||||
}
|
||||
|
||||
if (!lang.isNull()) {
|
||||
cursor.insertFragment(highlight(code.text(), lang));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QString MessageFormatter::formatInternal(const QString &messageBody, QTextDocument *document)
|
||||
{
|
||||
QTextCursor curs(document);
|
||||
QDomDocument doc(QLatin1String("htmlement"));
|
||||
doc.setContent(QStringLiteral("<div>%1</div>").arg(messageBody));
|
||||
QDomElement docElem = doc.documentElement();
|
||||
QDomNode n = docElem.firstChild();
|
||||
while (!n.isNull()) {
|
||||
QDomElement e = n.toElement();
|
||||
if (!e.isNull()) {
|
||||
if (e.tagName() != QLatin1String("pre") || !extractCodeBlock(curs, e)) {
|
||||
QString outText;
|
||||
QTextStream out(&outText);
|
||||
e.save(out, 0);
|
||||
curs.insertHtml(outText);
|
||||
}
|
||||
}
|
||||
n = n.nextSibling();
|
||||
}
|
||||
|
||||
Q_EMIT document->contentsChanged();
|
||||
return document->toHtml();
|
||||
}
|
||||
|
||||
QString MessageFormatter::format(const QString &messageBody, QQuickTextDocument *doc, QQuickItem *item)
|
||||
{
|
||||
QColor linkColor = QQmlProperty(item, QLatin1String("Kirigami.Theme.linkColor"), qmlContext(item)).read().value<QColor>();
|
||||
|
||||
return formatInternal(messageBody, doc->textDocument());
|
||||
}
|
||||
13
src/messageformatter.h
Normal file
13
src/messageformatter.h
Normal file
@@ -0,0 +1,13 @@
|
||||
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <QQuickItem>
|
||||
#include <QQuickTextDocument>
|
||||
|
||||
class MessageFormatter : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
Q_INVOKABLE QString format(const QString &messageBody, QQuickTextDocument *doc, QQuickItem *item);
|
||||
Q_INVOKABLE QString formatInternal(const QString &messageBody, QTextDocument *doc);
|
||||
};
|
||||
@@ -138,7 +138,7 @@ QCoro::Task<void> AccountEmoticonModel::doSetEmoticonImage(int index, QUrl sourc
|
||||
if (job->error() != BaseJob::NoError) {
|
||||
co_return;
|
||||
}
|
||||
m_images->images[index].url = job->contentUri().toString();
|
||||
m_images->images[index].url = job->contentUri();
|
||||
m_images->images[index].info = none;
|
||||
QJsonObject data;
|
||||
m_images->fillJson(&data);
|
||||
|
||||
@@ -13,8 +13,6 @@
|
||||
|
||||
#include <Quotient/connection.h>
|
||||
|
||||
class ImagePacksModel;
|
||||
|
||||
/**
|
||||
* @class AccountEmoticonModel
|
||||
*
|
||||
|
||||
@@ -14,9 +14,10 @@
|
||||
using Action = ActionsModel::Action;
|
||||
using namespace Quotient;
|
||||
|
||||
QStringList rainbowColors{"#ff2b00", "#ff5500", "#ff8000", "#ffaa00", "#ffd500", "#ffff00", "#d4ff00", "#aaff00", "#80ff00", "#55ff00", "#2bff00", "#00ff00",
|
||||
"#00ff2b", "#00ff55", "#00ff80", "#00ffaa", "#00ffd5", "#00ffff", "#00d4ff", "#00aaff", "#007fff", "#0055ff", "#002bff", "#0000ff",
|
||||
"#2a00ff", "#5500ff", "#7f00ff", "#aa00ff", "#d400ff", "#ff00ff", "#ff00d4", "#ff00aa", "#ff0080", "#ff0055", "#ff002b", "#ff0000"};
|
||||
QStringList rainbowColors{"#ff2b00"_ls, "#ff5500"_ls, "#ff8000"_ls, "#ffaa00"_ls, "#ffd500"_ls, "#ffff00"_ls, "#d4ff00"_ls, "#aaff00"_ls, "#80ff00"_ls,
|
||||
"#55ff00"_ls, "#2bff00"_ls, "#00ff00"_ls, "#00ff2b"_ls, "#00ff55"_ls, "#00ff80"_ls, "#00ffaa"_ls, "#00ffd5"_ls, "#00ffff"_ls,
|
||||
"#00d4ff"_ls, "#00aaff"_ls, "#007fff"_ls, "#0055ff"_ls, "#002bff"_ls, "#0000ff"_ls, "#2a00ff"_ls, "#5500ff"_ls, "#7f00ff"_ls,
|
||||
"#aa00ff"_ls, "#d400ff"_ls, "#ff00ff"_ls, "#ff00d4"_ls, "#ff00aa"_ls, "#ff0080"_ls, "#ff0055"_ls, "#ff002b"_ls, "#ff0000"_ls};
|
||||
|
||||
auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room) {
|
||||
if (text.isEmpty()) {
|
||||
@@ -260,7 +261,7 @@ QVector<ActionsModel::Action> actions{
|
||||
}
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Knocking room <roomname>.", "Knocking room %1.", text));
|
||||
auto connection = Controller::instance().activeConnection();
|
||||
const auto knownServer = roomName.mid(roomName.indexOf(":") + 1);
|
||||
const auto knownServer = roomName.mid(roomName.indexOf(":"_ls) + 1);
|
||||
if (parts.length() >= 2) {
|
||||
RoomManager::instance().knockRoom(connection, roomName, parts[1], QStringList{knownServer});
|
||||
} else {
|
||||
@@ -450,7 +451,7 @@ QVector<ActionsModel::Action> actions{
|
||||
i18nc("You are not allowed to ban <username> from this room.", "You are not allowed to ban %1 from this room.", parts[0]));
|
||||
return QString();
|
||||
}
|
||||
room->ban(parts[0], parts.size() > 1 ? parts.mid(1).join(" ") : QString());
|
||||
room->ban(parts[0], parts.size() > 1 ? parts.mid(1).join(QLatin1Char(' ')) : QString());
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> was banned from this room.", "%1 was banned from this room.", parts[0]));
|
||||
return QString();
|
||||
},
|
||||
@@ -527,7 +528,7 @@ QVector<ActionsModel::Action> actions{
|
||||
i18nc("You are not allowed to kick <username> from this room", "You are not allowed to kick %1 from this room.", parts[0]));
|
||||
return QString();
|
||||
}
|
||||
room->kickMember(parts[0], parts.size() > 1 ? parts.mid(1).join(" ") : QString());
|
||||
room->kickMember(parts[0], parts.size() > 1 ? parts.mid(1).join(QLatin1Char(' ')) : QString());
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> was kicked from this room.", "%1 was kicked from this room.", parts[0]));
|
||||
return QString();
|
||||
},
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "collapsestateproxymodel.h"
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
bool CollapseStateProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
|
||||
{
|
||||
Q_UNUSED(source_parent);
|
||||
return sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::DelegateTypeRole)
|
||||
!= MessageEventModel::DelegateType::State // If this is not a state, show it
|
||||
|| (source_row < sourceModel()->rowCount() - 1
|
||||
&& sourceModel()->data(sourceModel()->index(source_row + 1, 0), MessageEventModel::DelegateTypeRole)
|
||||
!= MessageEventModel::DelegateType::State) // If this is the first state in a block, show it. TODO hidden events?
|
||||
|| sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::ShowSectionRole).toBool(); // If it's a new day, show it
|
||||
}
|
||||
|
||||
QVariant CollapseStateProxyModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (role == AggregateDisplayRole) {
|
||||
return aggregateEventToString(mapToSource(index).row());
|
||||
} else if (role == StateEventsRole) {
|
||||
return stateEventsList(mapToSource(index).row());
|
||||
} else if (role == AuthorListRole) {
|
||||
return authorList(mapToSource(index).row());
|
||||
} else if (role == ExcessAuthorsRole) {
|
||||
return excessAuthors(mapToSource(index).row());
|
||||
}
|
||||
return sourceModel()->data(mapToSource(index), role);
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> CollapseStateProxyModel::roleNames() const
|
||||
{
|
||||
auto roles = sourceModel()->roleNames();
|
||||
roles[AggregateDisplayRole] = "aggregateDisplay";
|
||||
roles[StateEventsRole] = "stateEvents";
|
||||
roles[AuthorListRole] = "authorList";
|
||||
roles[ExcessAuthorsRole] = "excessAuthors";
|
||||
return roles;
|
||||
}
|
||||
|
||||
QString CollapseStateProxyModel::aggregateEventToString(int sourceRow) const
|
||||
{
|
||||
QStringList parts;
|
||||
QVariantList uniqueAuthors;
|
||||
for (int i = sourceRow; i >= 0; i--) {
|
||||
parts += sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::GenericDisplayRole).toString();
|
||||
QVariant nextAuthor = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole);
|
||||
if (!uniqueAuthors.contains(nextAuthor)) {
|
||||
uniqueAuthors.append(nextAuthor);
|
||||
}
|
||||
if (i > 0
|
||||
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
|
||||
!= MessageEventModel::DelegateType::State // If it's not a state event
|
||||
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
||||
)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
parts.sort(); // Sort them so that all identical events can be collected.
|
||||
if (!parts.isEmpty()) {
|
||||
QStringList chunks;
|
||||
while (!parts.isEmpty()) {
|
||||
chunks += QString();
|
||||
int count = 1;
|
||||
auto part = parts.takeFirst();
|
||||
chunks.last() += part;
|
||||
while (!parts.isEmpty() && parts.first() == part) {
|
||||
parts.removeFirst();
|
||||
count++;
|
||||
}
|
||||
if (count > 1 && uniqueAuthors.length() == 1) {
|
||||
chunks.last() += i18ncp("n times", " %1 time ", " %1 times ", count);
|
||||
}
|
||||
}
|
||||
chunks.removeDuplicates();
|
||||
QString text = "<style>a {text-decoration: none;}</style>"; // There can be links in the event text so make sure all are styled.
|
||||
// The author text is either "n users" if > 1 user or the matrix.to link to a single user.
|
||||
QString userText = uniqueAuthors.length() > 1 ? i18ncp("n users", " %1 user ", " %1 users ", uniqueAuthors.length())
|
||||
: QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a> ")
|
||||
.arg(uniqueAuthors[0].toMap()["id"].toString(),
|
||||
uniqueAuthors[0].toMap()["color"].toString(),
|
||||
uniqueAuthors[0].toMap()["displayName"].toString());
|
||||
text += userText;
|
||||
text += chunks.takeFirst();
|
||||
|
||||
if (chunks.size() > 0) {
|
||||
while (chunks.size() > 1) {
|
||||
text += i18nc("[action 1], [action 2 and/or action 3]", ", ");
|
||||
text += chunks.takeFirst();
|
||||
}
|
||||
text += uniqueAuthors.length() > 1 ? i18nc("[action 1, action 2] or [action 3]", " or ") : i18nc("[action 1, action 2] and [action 3]", " and ");
|
||||
text += chunks.takeFirst();
|
||||
}
|
||||
return text;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
QVariantList CollapseStateProxyModel::stateEventsList(int sourceRow) const
|
||||
{
|
||||
QVariantList stateEvents;
|
||||
for (int i = sourceRow; i >= 0; i--) {
|
||||
auto nextState = QVariantMap{
|
||||
{"author", sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole)},
|
||||
{"authorDisplayName", sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorDisplayNameRole).toString()},
|
||||
{"text", sourceModel()->data(sourceModel()->index(i, 0), Qt::DisplayRole).toString()},
|
||||
};
|
||||
stateEvents.append(nextState);
|
||||
if (i > 0
|
||||
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
|
||||
!= MessageEventModel::DelegateType::State // If it's not a state event
|
||||
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
||||
)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return stateEvents;
|
||||
}
|
||||
|
||||
QVariantList CollapseStateProxyModel::authorList(int sourceRow) const
|
||||
{
|
||||
QVariantList uniqueAuthors;
|
||||
for (int i = sourceRow; i >= 0; i--) {
|
||||
QVariant nextAvatar = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole);
|
||||
if (!uniqueAuthors.contains(nextAvatar)) {
|
||||
uniqueAuthors.append(nextAvatar);
|
||||
}
|
||||
if (i > 0
|
||||
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
|
||||
!= MessageEventModel::DelegateType::State // If it's not a state event
|
||||
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
||||
)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (uniqueAuthors.count() > 5) {
|
||||
uniqueAuthors = uniqueAuthors.mid(0, 5);
|
||||
}
|
||||
return uniqueAuthors;
|
||||
}
|
||||
|
||||
QString CollapseStateProxyModel::excessAuthors(int row) const
|
||||
{
|
||||
QVariantList uniqueAuthors;
|
||||
for (int i = row; i >= 0; i--) {
|
||||
QVariant nextAvatar = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole);
|
||||
if (!uniqueAuthors.contains(nextAvatar)) {
|
||||
uniqueAuthors.append(nextAvatar);
|
||||
}
|
||||
if (i > 0
|
||||
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
|
||||
!= MessageEventModel::DelegateType::State // If it's not a state event
|
||||
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
||||
)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int excessAuthors;
|
||||
if (uniqueAuthors.count() > 5) {
|
||||
excessAuthors = uniqueAuthors.count() - 5;
|
||||
} else {
|
||||
excessAuthors = 0;
|
||||
}
|
||||
QString excessAuthorsString;
|
||||
if (excessAuthors == 0) {
|
||||
return QString();
|
||||
} else {
|
||||
return QStringLiteral("+ %1").arg(excessAuthors);
|
||||
}
|
||||
}
|
||||
|
||||
#include "moc_collapsestateproxymodel.cpp"
|
||||
@@ -1,80 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "messageeventmodel.h"
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
/**
|
||||
* @class CollapseStateProxyModel
|
||||
*
|
||||
* This model aggregates multiple sequential state events into a single entry.
|
||||
*
|
||||
* Events are only aggregated if they happened on the same day.
|
||||
*
|
||||
* @sa MessageEventModel
|
||||
*/
|
||||
class CollapseStateProxyModel : public QSortFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum Roles {
|
||||
AggregateDisplayRole = MessageEventModel::LastRole + 1, /**< Single line aggregation of all the state events. */
|
||||
StateEventsRole, /**< List of state events in the aggregated state. */
|
||||
AuthorListRole, /**< List of the first 5 unique authors of the aggregated state event. */
|
||||
ExcessAuthorsRole, /**< The number of unique authors beyond the first 5. */
|
||||
LastRole, // Keep this last
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Whether a row should be shown out or not.
|
||||
*
|
||||
* @sa QSortFilterProxyModel::filterAcceptsRow
|
||||
*/
|
||||
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QSortFilterProxyModel::data
|
||||
*/
|
||||
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa Roles, QAbstractProxyModel::roleNames()
|
||||
*/
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Aggregation of the text of consecutive state events starting at row.
|
||||
*
|
||||
* If state events happen on different days they will be split into two aggregate
|
||||
* events.
|
||||
*/
|
||||
[[nodiscard]] QString aggregateEventToString(int row) const;
|
||||
|
||||
/**
|
||||
* @brief Return a list of consecutive state events starting at row.
|
||||
*
|
||||
* If state events happen on different days they will be split into two aggregate
|
||||
* events.
|
||||
*/
|
||||
[[nodiscard]] QVariantList stateEventsList(int row) const;
|
||||
|
||||
/**
|
||||
* @brief List of the first 5 unique authors for the aggregate state events starting at row.
|
||||
*/
|
||||
[[nodiscard]] QVariantList authorList(int row) const;
|
||||
|
||||
/**
|
||||
* @brief The number of unique authors beyond the first 5 for the aggregate state events starting at row.
|
||||
*/
|
||||
[[nodiscard]] QString excessAuthors(int row) const;
|
||||
};
|
||||
@@ -53,54 +53,55 @@ QVariant CompletionModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
auto filterIndex = m_filterModel->index(index.row(), 0);
|
||||
if (m_autoCompletionType == User) {
|
||||
if (role == Text) {
|
||||
if (role == DisplayNameRole) {
|
||||
return m_filterModel->data(filterIndex, UserListModel::DisplayNameRole);
|
||||
}
|
||||
if (role == Subtitle) {
|
||||
if (role == SubtitleRole) {
|
||||
return m_filterModel->data(filterIndex, UserListModel::UserIdRole);
|
||||
}
|
||||
if (role == Icon) {
|
||||
if (role == IconNameRole) {
|
||||
return m_filterModel->data(filterIndex, UserListModel::AvatarRole);
|
||||
}
|
||||
}
|
||||
|
||||
if (m_autoCompletionType == Command) {
|
||||
if (role == Text) {
|
||||
return m_filterModel->data(filterIndex, ActionsModel::Prefix).toString() + QStringLiteral(" ")
|
||||
+ m_filterModel->data(filterIndex, ActionsModel::Parameters).toString();
|
||||
if (role == DisplayNameRole) {
|
||||
return QStringLiteral("%1 %2").arg(m_filterModel->data(filterIndex, ActionsModel::Prefix).toString(),
|
||||
m_filterModel->data(filterIndex, ActionsModel::Parameters).toString());
|
||||
}
|
||||
if (role == Subtitle) {
|
||||
if (role == SubtitleRole) {
|
||||
return m_filterModel->data(filterIndex, ActionsModel::Description);
|
||||
}
|
||||
if (role == Icon) {
|
||||
if (role == IconNameRole) {
|
||||
return QStringLiteral("invalid");
|
||||
}
|
||||
if (role == ReplacedText) {
|
||||
if (role == ReplacedTextRole) {
|
||||
return m_filterModel->data(filterIndex, ActionsModel::Prefix);
|
||||
}
|
||||
}
|
||||
if (m_autoCompletionType == Room) {
|
||||
if (role == Text) {
|
||||
if (role == DisplayNameRole) {
|
||||
return m_filterModel->data(filterIndex, RoomListModel::DisplayNameRole);
|
||||
}
|
||||
if (role == Subtitle) {
|
||||
if (role == SubtitleRole) {
|
||||
return m_filterModel->data(filterIndex, RoomListModel::CanonicalAliasRole);
|
||||
}
|
||||
if (role == Icon) {
|
||||
return m_filterModel->data(filterIndex, RoomListModel::AvatarRole);
|
||||
if (role == IconNameRole) {
|
||||
return QStringLiteral("mxc://%1")
|
||||
.arg(m_roomListModel->connection()->makeMediaUrl(m_filterModel->data(filterIndex, RoomListModel::AvatarRole).toUrl()).toString());
|
||||
}
|
||||
}
|
||||
if (m_autoCompletionType == Emoji) {
|
||||
if (role == Text) {
|
||||
if (role == DisplayNameRole) {
|
||||
return m_filterModel->data(filterIndex, CustomEmojiModel::DisplayRole);
|
||||
}
|
||||
if (role == Icon) {
|
||||
if (role == IconNameRole) {
|
||||
return m_filterModel->data(filterIndex, CustomEmojiModel::MxcUrl);
|
||||
}
|
||||
if (role == ReplacedText) {
|
||||
if (role == ReplacedTextRole) {
|
||||
return m_filterModel->data(filterIndex, CustomEmojiModel::ReplacedTextRole);
|
||||
}
|
||||
if (role == Subtitle) {
|
||||
if (role == SubtitleRole) {
|
||||
return m_filterModel->data(filterIndex, EmojiModel::DescriptionRole);
|
||||
}
|
||||
}
|
||||
@@ -111,10 +112,10 @@ QVariant CompletionModel::data(const QModelIndex &index, int role) const
|
||||
QHash<int, QByteArray> CompletionModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{Text, "text"},
|
||||
{Subtitle, "subtitle"},
|
||||
{Icon, "icon"},
|
||||
{ReplacedText, "replacedText"},
|
||||
{DisplayNameRole, "displayName"},
|
||||
{SubtitleRole, "subtitle"},
|
||||
{IconNameRole, "iconName"},
|
||||
{ReplacedTextRole, "replacedText"},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -40,12 +40,12 @@ class CompletionModel : public QAbstractListModel
|
||||
*
|
||||
* @sa AutoCompletionType
|
||||
*/
|
||||
Q_PROPERTY(AutoCompletionType autoCompletionType READ autoCompletionType NOTIFY autoCompletionTypeChanged);
|
||||
Q_PROPERTY(AutoCompletionType autoCompletionType READ autoCompletionType NOTIFY autoCompletionTypeChanged)
|
||||
|
||||
/**
|
||||
* @brief The RoomListModel to be used for room completions.
|
||||
*/
|
||||
Q_PROPERTY(RoomListModel *roomListModel READ roomListModel WRITE setRoomListModel NOTIFY roomListModelChanged);
|
||||
Q_PROPERTY(RoomListModel *roomListModel READ roomListModel WRITE setRoomListModel NOTIFY roomListModelChanged)
|
||||
|
||||
public:
|
||||
/**
|
||||
@@ -64,10 +64,10 @@ public:
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum Roles {
|
||||
Text = Qt::DisplayRole, /**< The main text to show. */
|
||||
Subtitle, /**< The subtitle text to show. */
|
||||
Icon, /**< The icon to show. */
|
||||
ReplacedText, /**< The text to replace the input text with for the completion. */
|
||||
DisplayNameRole = Qt::DisplayRole, /**< The main text to show. */
|
||||
SubtitleRole, /**< The subtitle text to show. */
|
||||
IconNameRole, /**< The icon to show. */
|
||||
ReplacedTextRole, /**< The text to replace the input text with for the completion. */
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
|
||||
|
||||
@@ -11,6 +11,11 @@ bool CompletionProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &so
|
||||
if (m_filterText.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sourceModel()->data(sourceModel()->index(sourceRow, 0), filterRole()).toString().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (sourceModel()->data(sourceModel()->index(sourceRow, 0), filterRole()).toString().startsWith(m_filterText, Qt::CaseInsensitive)
|
||||
&& !m_fullText.startsWith(sourceModel()->data(sourceModel()->index(sourceRow, 0), filterRole()).toString()))
|
||||
|| (m_secondaryFilterRole != -1
|
||||
|
||||
@@ -21,14 +21,14 @@ void CustomEmojiModel::fetchEmojis()
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes");
|
||||
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes"_ls);
|
||||
if (data == nullptr) {
|
||||
return;
|
||||
}
|
||||
QJsonObject emojis = data->contentJson()["images"].toObject();
|
||||
QJsonObject emojis = data->contentJson()["images"_ls].toObject();
|
||||
|
||||
// TODO: Remove with stable migration
|
||||
const auto legacyEmojis = data->contentJson()["emoticons"].toObject();
|
||||
const auto legacyEmojis = data->contentJson()["emoticons"_ls].toObject();
|
||||
for (const auto &emoji : legacyEmojis.keys()) {
|
||||
if (!emojis.contains(emoji)) {
|
||||
emojis[emoji] = legacyEmojis[emoji];
|
||||
@@ -41,9 +41,9 @@ void CustomEmojiModel::fetchEmojis()
|
||||
for (const auto &emoji : emojis.keys()) {
|
||||
const auto &data = emojis[emoji];
|
||||
|
||||
const auto e = emoji.startsWith(":") ? emoji : (QStringLiteral(":") + emoji + QStringLiteral(":"));
|
||||
const auto e = emoji.startsWith(":"_ls) ? emoji : (QStringLiteral(":") + emoji + QStringLiteral(":"));
|
||||
|
||||
m_emojis << CustomEmoji{e, data.toObject()["url"].toString(), QRegularExpression(e)};
|
||||
m_emojis << CustomEmoji{e, data.toObject()["url"_ls].toString(), QRegularExpression(e)};
|
||||
}
|
||||
|
||||
endResetModel();
|
||||
@@ -56,19 +56,19 @@ void CustomEmojiModel::addEmoji(const QString &name, const QUrl &location)
|
||||
auto job = Controller::instance().activeConnection()->uploadFile(location.toLocalFile());
|
||||
|
||||
connect(job, &BaseJob::success, this, [name, location, job] {
|
||||
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes");
|
||||
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes"_ls);
|
||||
auto json = data != nullptr ? data->contentJson() : QJsonObject();
|
||||
auto emojiData = json["images"].toObject();
|
||||
auto emojiData = json["images"_ls].toObject();
|
||||
|
||||
QString url;
|
||||
url = job->contentUri().toString();
|
||||
|
||||
QImage image(location.toLocalFile());
|
||||
QJsonObject imageInfo;
|
||||
imageInfo["w"] = image.width();
|
||||
imageInfo["h"] = image.height();
|
||||
imageInfo["mimetype"] = QMimeDatabase().mimeTypeForFile(location.toLocalFile()).name();
|
||||
imageInfo["size"] = image.sizeInBytes();
|
||||
imageInfo["w"_ls] = image.width();
|
||||
imageInfo["h"_ls] = image.height();
|
||||
imageInfo["mimetype"_ls] = QMimeDatabase().mimeTypeForFile(location.toLocalFile()).name();
|
||||
imageInfo["size"_ls] = image.sizeInBytes();
|
||||
|
||||
emojiData[QStringLiteral("%1").arg(name)] = QJsonObject({
|
||||
{QStringLiteral("url"), url},
|
||||
@@ -77,8 +77,8 @@ void CustomEmojiModel::addEmoji(const QString &name, const QUrl &location)
|
||||
{"usage"_ls, "emoticon"_ls},
|
||||
});
|
||||
|
||||
json["images"] = emojiData;
|
||||
Controller::instance().activeConnection()->setAccountData("im.ponies.user_emotes", json);
|
||||
json["images"_ls] = emojiData;
|
||||
Controller::instance().activeConnection()->setAccountData("im.ponies.user_emotes"_ls, json);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -86,30 +86,30 @@ void CustomEmojiModel::removeEmoji(const QString &name)
|
||||
{
|
||||
using namespace Quotient;
|
||||
|
||||
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes");
|
||||
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes"_ls);
|
||||
Q_ASSERT(data);
|
||||
auto json = data->contentJson();
|
||||
const QString _name = name.mid(1).chopped(1);
|
||||
auto emojiData = json["images"].toObject();
|
||||
auto emojiData = json["images"_ls].toObject();
|
||||
|
||||
if (emojiData.contains(name)) {
|
||||
emojiData.remove(name);
|
||||
json["images"] = emojiData;
|
||||
json["images"_ls] = emojiData;
|
||||
}
|
||||
if (emojiData.contains(_name)) {
|
||||
emojiData.remove(_name);
|
||||
json["images"] = emojiData;
|
||||
json["images"_ls] = emojiData;
|
||||
}
|
||||
emojiData = json["emoticons"].toObject();
|
||||
emojiData = json["emoticons"_ls].toObject();
|
||||
if (emojiData.contains(name)) {
|
||||
emojiData.remove(name);
|
||||
json["emoticons"] = emojiData;
|
||||
json["emoticons"_ls] = emojiData;
|
||||
}
|
||||
if (emojiData.contains(_name)) {
|
||||
emojiData.remove(_name);
|
||||
json["emoticons"] = emojiData;
|
||||
json["emoticons"_ls] = emojiData;
|
||||
}
|
||||
Controller::instance().activeConnection()->setAccountData("im.ponies.user_emotes", json);
|
||||
Controller::instance().activeConnection()->setAccountData("im.ponies.user_emotes"_ls, json);
|
||||
}
|
||||
|
||||
CustomEmojiModel::CustomEmojiModel(QObject *parent)
|
||||
@@ -174,7 +174,7 @@ QHash<int, QByteArray> CustomEmojiModel::roleNames() const
|
||||
|
||||
QString CustomEmojiModel::preprocessText(const QString &text)
|
||||
{
|
||||
auto parts = text.split("```");
|
||||
auto parts = text.split("```"_ls);
|
||||
bool skip = true;
|
||||
for (auto &part : parts) {
|
||||
skip = !skip;
|
||||
@@ -187,7 +187,7 @@ QString CustomEmojiModel::preprocessText(const QString &text)
|
||||
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("```");
|
||||
return parts.join("```"_ls);
|
||||
}
|
||||
|
||||
QVariantList CustomEmojiModel::filterModel(const QString &filter)
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
#include "devicesmodel.h"
|
||||
|
||||
#include "controller.h"
|
||||
#include "jobs/neochatdeletedevicejob.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QLocale>
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
@@ -52,6 +56,12 @@ QVariant DevicesModel::data(const QModelIndex &index, int role) const
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
case TimestampString:
|
||||
if (device.lastSeenTs) {
|
||||
return QDateTime::fromMSecsSinceEpoch(*device.lastSeenTs).toString(QLocale().dateTimeFormat(QLocale::ShortFormat));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
case Type:
|
||||
if (device.deviceId == m_connection->deviceId()) {
|
||||
return This;
|
||||
@@ -81,6 +91,7 @@ QHash<int, QByteArray> DevicesModel::roleNames() const
|
||||
{DisplayName, "displayName"},
|
||||
{LastIp, "lastIp"},
|
||||
{LastTimestamp, "lastTimestamp"},
|
||||
{TimestampString, "timestamp"},
|
||||
{Type, "type"},
|
||||
};
|
||||
}
|
||||
@@ -103,11 +114,11 @@ void DevicesModel::logout(const QString &deviceId, const QString &password)
|
||||
if (job->error() != BaseJob::Success) {
|
||||
QJsonObject replyData = job->jsonData();
|
||||
QJsonObject authData;
|
||||
authData["session"] = replyData["session"];
|
||||
authData["password"] = password;
|
||||
authData["type"] = "m.login.password";
|
||||
QJsonObject identifier = {{"type", "m.id.user"}, {"user", Controller::instance().activeConnection()->user()->id()}};
|
||||
authData["identifier"] = identifier;
|
||||
authData["session"_ls] = replyData["session"_ls];
|
||||
authData["password"_ls] = password;
|
||||
authData["type"_ls] = "m.login.password"_ls;
|
||||
QJsonObject identifier = {{"type"_ls, "m.id.user"_ls}, {"user"_ls, Controller::instance().activeConnection()->user()->id()}};
|
||||
authData["identifier"_ls] = identifier;
|
||||
auto *innerJob = Controller::instance().activeConnection()->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId, authData);
|
||||
connect(innerJob, &BaseJob::success, this, onSuccess);
|
||||
} else {
|
||||
|
||||
@@ -40,6 +40,7 @@ public:
|
||||
DisplayName, /**< Display name set by the user for this device. */
|
||||
LastIp, /**< The IP address where this device was last seen. */
|
||||
LastTimestamp, /**< The timestamp when this devices was last seen. */
|
||||
TimestampString, /**< String for the timestamp when this devices was last seen. */
|
||||
Type, /**< The category to sort this device into. */
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
class DevicesProxyModel : public QSortFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int type READ type WRITE setType NOTIFY typeChanged);
|
||||
Q_PROPERTY(int type READ type WRITE setType NOTIFY typeChanged)
|
||||
|
||||
public:
|
||||
DevicesProxyModel(QObject *parent = nullptr);
|
||||
|
||||
@@ -63,7 +63,7 @@ QHash<int, QByteArray> EmojiModel::roleNames() const
|
||||
|
||||
QVariantList EmojiModel::history() const
|
||||
{
|
||||
return m_settings.value("Editor/emojis", QVariantList()).toList();
|
||||
return m_settings.value(QStringLiteral("Editor/emojis"), QVariantList()).toList();
|
||||
}
|
||||
|
||||
QVariantList EmojiModel::filterModel(const QString &filter, bool limit)
|
||||
@@ -105,7 +105,7 @@ void EmojiModel::emojiUsed(const QVariant &modelData)
|
||||
}
|
||||
|
||||
list.push_front(modelData);
|
||||
m_settings.setValue("Editor/emojis", list);
|
||||
m_settings.setValue(QStringLiteral("Editor/emojis"), list);
|
||||
|
||||
Q_EMIT historyChanged();
|
||||
}
|
||||
@@ -133,8 +133,8 @@ QVariantList EmojiModel::emojis(Category category) const
|
||||
|
||||
QVariantList EmojiModel::tones(const QString &baseEmoji) const
|
||||
{
|
||||
if (baseEmoji.endsWith("tone")) {
|
||||
return EmojiTones::_tones.values(baseEmoji.split(":")[0]);
|
||||
if (baseEmoji.endsWith(QStringLiteral("tone"))) {
|
||||
return EmojiTones::_tones.values(baseEmoji.split(QStringLiteral(":"))[0]);
|
||||
}
|
||||
return EmojiTones::_tones.values(baseEmoji);
|
||||
}
|
||||
@@ -145,54 +145,54 @@ QVariantList EmojiModel::categories() const
|
||||
{
|
||||
return QVariantList{
|
||||
{QVariantMap{
|
||||
{"category", EmojiModel::HistoryNoCustom},
|
||||
{"name", i18nc("Previously used emojis", "History")},
|
||||
{"emoji", QStringLiteral("⌛️")},
|
||||
{QStringLiteral("category"), EmojiModel::HistoryNoCustom},
|
||||
{QStringLiteral("name"), i18nc("Previously used emojis", "History")},
|
||||
{QStringLiteral("emoji"), QStringLiteral("⌛️")},
|
||||
}},
|
||||
{QVariantMap{
|
||||
{"category", EmojiModel::Smileys},
|
||||
{"name", i18nc("'Smileys' is a category of emoji", "Smileys")},
|
||||
{"emoji", QStringLiteral("😏")},
|
||||
{QStringLiteral("category"), EmojiModel::Smileys},
|
||||
{QStringLiteral("name"), i18nc("'Smileys' is a category of emoji", "Smileys")},
|
||||
{QStringLiteral("emoji"), QStringLiteral("😏")},
|
||||
}},
|
||||
{QVariantMap{
|
||||
{"category", EmojiModel::People},
|
||||
{"name", i18nc("'People' is a category of emoji", "People")},
|
||||
{"emoji", QStringLiteral("🙋♂️")},
|
||||
{QStringLiteral("category"), EmojiModel::People},
|
||||
{QStringLiteral("name"), i18nc("'People' is a category of emoji", "People")},
|
||||
{QStringLiteral("emoji"), QStringLiteral("🙋♂️")},
|
||||
}},
|
||||
{QVariantMap{
|
||||
{"category", EmojiModel::Nature},
|
||||
{"name", i18nc("'Nature' is a category of emoji", "Nature")},
|
||||
{"emoji", QStringLiteral("🌲")},
|
||||
{QStringLiteral("category"), EmojiModel::Nature},
|
||||
{QStringLiteral("name"), i18nc("'Nature' is a category of emoji", "Nature")},
|
||||
{QStringLiteral("emoji"), QStringLiteral("🌲")},
|
||||
}},
|
||||
{QVariantMap{
|
||||
{"category", EmojiModel::Food},
|
||||
{"name", i18nc("'Food' is a category of emoji", "Food")},
|
||||
{"emoji", QStringLiteral("🍛")},
|
||||
{QStringLiteral("category"), EmojiModel::Food},
|
||||
{QStringLiteral("name"), i18nc("'Food' is a category of emoji", "Food")},
|
||||
{QStringLiteral("emoji"), QStringLiteral("🍛")},
|
||||
}},
|
||||
{QVariantMap{
|
||||
{"category", EmojiModel::Activities},
|
||||
{"name", i18nc("'Activities' is a category of emoji", "Activities")},
|
||||
{"emoji", QStringLiteral("🚁")},
|
||||
{QStringLiteral("category"), EmojiModel::Activities},
|
||||
{QStringLiteral("name"), i18nc("'Activities' is a category of emoji", "Activities")},
|
||||
{QStringLiteral("emoji"), QStringLiteral("🚁")},
|
||||
}},
|
||||
{QVariantMap{
|
||||
{"category", EmojiModel::Travel},
|
||||
{"name", i18nc("'Travel' is a category of emoji", "Travel")},
|
||||
{"emoji", QStringLiteral("🚅")},
|
||||
{QStringLiteral("category"), EmojiModel::Travel},
|
||||
{QStringLiteral("name"), i18nc("'Travel' is a category of emoji", "Travel")},
|
||||
{QStringLiteral("emoji"), QStringLiteral("🚅")},
|
||||
}},
|
||||
{QVariantMap{
|
||||
{"category", EmojiModel::Objects},
|
||||
{"name", i18nc("'Objects' is a category of emoji", "Objects")},
|
||||
{"emoji", QStringLiteral("💡")},
|
||||
{QStringLiteral("category"), EmojiModel::Objects},
|
||||
{QStringLiteral("name"), i18nc("'Objects' is a category of emoji", "Objects")},
|
||||
{QStringLiteral("emoji"), QStringLiteral("💡")},
|
||||
}},
|
||||
{QVariantMap{
|
||||
{"category", EmojiModel::Symbols},
|
||||
{"name", i18nc("'Symbols' is a category of emoji", "Symbols")},
|
||||
{"emoji", QStringLiteral("🔣")},
|
||||
{QStringLiteral("category"), EmojiModel::Symbols},
|
||||
{QStringLiteral("name"), i18nc("'Symbols' is a category of emoji", "Symbols")},
|
||||
{QStringLiteral("emoji"), QStringLiteral("🔣")},
|
||||
}},
|
||||
{QVariantMap{
|
||||
{"category", EmojiModel::Flags},
|
||||
{"name", i18nc("'Flags' is a category of emoji", "Flags")},
|
||||
{"emoji", QStringLiteral("🏁")},
|
||||
{QStringLiteral("category"), EmojiModel::Flags},
|
||||
{QStringLiteral("name"), i18nc("'Flags' is a category of emoji", "Flags")},
|
||||
{QStringLiteral("emoji"), QStringLiteral("🏁")},
|
||||
}},
|
||||
};
|
||||
}
|
||||
@@ -203,15 +203,15 @@ QVariantList EmojiModel::categoriesWithCustom() const
|
||||
cats.removeAt(0);
|
||||
cats.insert(0,
|
||||
QVariantMap{
|
||||
{"category", EmojiModel::History},
|
||||
{"name", i18nc("Previously used emojis", "History")},
|
||||
{"emoji", QStringLiteral("⌛️")},
|
||||
{QStringLiteral("category"), EmojiModel::History},
|
||||
{QStringLiteral("name"), i18nc("Previously used emojis", "History")},
|
||||
{QStringLiteral("emoji"), QStringLiteral("⌛️")},
|
||||
});
|
||||
cats.insert(1,
|
||||
QVariantMap{
|
||||
{"category", EmojiModel::Custom},
|
||||
{"name", i18nc("'Custom' is a category of emoji", "Custom")},
|
||||
{"emoji", QStringLiteral("🖼️")},
|
||||
{QStringLiteral("category"), EmojiModel::Custom},
|
||||
{QStringLiteral("name"), i18nc("'Custom' is a category of emoji", "Custom")},
|
||||
{QStringLiteral("emoji"), QStringLiteral("🖼️")},
|
||||
});
|
||||
;
|
||||
return cats;
|
||||
|
||||
@@ -33,7 +33,7 @@ struct Emoji {
|
||||
{
|
||||
arch >> object.unicode;
|
||||
arch >> object.shortName;
|
||||
object.isCustom = object.unicode.startsWith("image://");
|
||||
object.isCustom = object.unicode.startsWith(QStringLiteral("image://"));
|
||||
return arch;
|
||||
}
|
||||
|
||||
|
||||
@@ -82,8 +82,9 @@ void ImagePacksModel::reloadImages()
|
||||
// Load emoticons from the account data
|
||||
if (m_room->connection()->hasAccountData("im.ponies.user_emotes"_ls)) {
|
||||
auto json = m_room->connection()->accountData("im.ponies.user_emotes"_ls)->contentJson();
|
||||
json["pack"] = QJsonObject{
|
||||
{"display_name", m_showStickers ? i18nc("As in 'The user's own Stickers'", "Own Stickers") : i18nc("As in 'The user's own emojis", "Own Emojis")},
|
||||
json["pack"_ls] = QJsonObject{
|
||||
{"display_name"_ls,
|
||||
m_showStickers ? i18nc("As in 'The user's own Stickers'", "Own Stickers") : i18nc("As in 'The user's own emojis", "Own Emojis")},
|
||||
};
|
||||
const auto &content = ImagePackEventContent(json);
|
||||
if (!content.images.isEmpty()) {
|
||||
@@ -104,8 +105,8 @@ void ImagePacksModel::reloadImages()
|
||||
for (const auto &packKey : packs.keys()) {
|
||||
if (const auto &pack = stickerRoom->currentState().get<ImagePackEvent>(packKey)) {
|
||||
const auto packContent = pack->content();
|
||||
if ((!packContent.pack || !packContent.pack->usage || (packContent.pack->usage->contains("emoticon") && showEmoticons())
|
||||
|| (packContent.pack->usage->contains("sticker") && showStickers()))
|
||||
if ((!packContent.pack || !packContent.pack->usage || (packContent.pack->usage->contains("emoticon"_ls) && showEmoticons())
|
||||
|| (packContent.pack->usage->contains("sticker"_ls) && showStickers()))
|
||||
&& !packContent.images.isEmpty()) {
|
||||
m_events += packContent;
|
||||
}
|
||||
@@ -115,12 +116,12 @@ void ImagePacksModel::reloadImages()
|
||||
}
|
||||
|
||||
// Load emoticons from the current room
|
||||
auto events = m_room->currentState().eventsOfType("im.ponies.room_emotes");
|
||||
auto events = m_room->currentState().eventsOfType("im.ponies.room_emotes"_ls);
|
||||
for (const auto &event : events) {
|
||||
auto packContent = eventCast<const ImagePackEvent>(event)->content();
|
||||
if (packContent.pack.has_value()) {
|
||||
if (!packContent.pack->usage || (packContent.pack->usage->contains("emoticon") && showEmoticons())
|
||||
|| (packContent.pack->usage->contains("sticker") && showStickers())) {
|
||||
if (!packContent.pack->usage || (packContent.pack->usage->contains("emoticon"_ls) && showEmoticons())
|
||||
|| (packContent.pack->usage->contains("sticker"_ls) && showStickers())) {
|
||||
m_events += packContent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
bool operator<(const LiveLocationData &lhs, const LiveLocationData &rhs)
|
||||
@@ -76,7 +78,7 @@ QVariant LiveLocationsModel::data(const QModelIndex &index, int roleName) const
|
||||
return longitude.toFloat();
|
||||
}
|
||||
case AssetRole:
|
||||
return data.beaconInfo["org.matrix.msc3488.asset"_ls].toObject()["type"].toString();
|
||||
return data.beaconInfo["org.matrix.msc3488.asset"_ls].toObject()["type"_ls].toString();
|
||||
case AuthorRole:
|
||||
return m_room->getUser(data.senderId);
|
||||
case IsLiveRole: {
|
||||
@@ -88,6 +90,11 @@ QVariant LiveLocationsModel::data(const QModelIndex &index, int roleName) const
|
||||
const auto timeout = data.beaconInfo.value("timeout"_ls).toDouble(600000);
|
||||
return lastTs + timeout >= QDateTime::currentDateTime().toMSecsSinceEpoch();
|
||||
}
|
||||
case HeadingRole: {
|
||||
bool success = false;
|
||||
const auto heading = data.beacon["org.matrix.msc3488.location"_ls].toObject()["org.kde.itinerary.heading"_ls].toString().toDouble(&success);
|
||||
return success ? heading : NAN;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
@@ -101,6 +108,7 @@ QHash<int, QByteArray> LiveLocationsModel::roleNames() const
|
||||
r.insert(AssetRole, "asset");
|
||||
r.insert(AuthorRole, "author");
|
||||
r.insert(IsLiveRole, "isLive");
|
||||
r.insert(HeadingRole, "heading");
|
||||
return r;
|
||||
}
|
||||
|
||||
@@ -121,7 +129,7 @@ QRectF LiveLocationsModel::boundingBox() const
|
||||
|
||||
void LiveLocationsModel::addEvent(const Quotient::RoomEvent *event)
|
||||
{
|
||||
if (event->isStateEvent() && event->matrixType() == "org.matrix.msc3672.beacon_info") {
|
||||
if (event->isStateEvent() && event->matrixType() == "org.matrix.msc3672.beacon_info"_ls) {
|
||||
LiveLocationData data;
|
||||
data.senderId = event->senderId();
|
||||
data.beaconInfo = event->contentJson();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user