Compare commits
130 Commits
work/nvrwh
...
v25.07.80
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
124ffba5e0 | ||
|
|
3aaaa610df | ||
|
|
18e883834c | ||
|
|
6acbd2dffd | ||
|
|
dc5c27aa2d | ||
|
|
26774bbe56 | ||
|
|
87213903b1 | ||
|
|
3d9d211d25 | ||
|
|
d5d291396d | ||
|
|
d7cd38b5e5 | ||
|
|
1a25cfc0e3 | ||
|
|
8d9669e033 | ||
|
|
690d2e2b12 | ||
|
|
2208b1b2b3 | ||
|
|
67da272ccd | ||
|
|
171930fb1d | ||
|
|
2bd59fa16f | ||
|
|
d0d0384bdb | ||
|
|
680573de61 | ||
|
|
ba7f866cf5 | ||
|
|
57a95ae972 | ||
|
|
de97275a38 | ||
|
|
d08181f56d | ||
|
|
f6e8491bf1 | ||
|
|
a1447ebd6f | ||
|
|
3b67e4deaf | ||
|
|
e55afd0402 | ||
|
|
7c0710d445 | ||
|
|
61f9cd41f7 | ||
|
|
6c7a7a7be5 | ||
|
|
63125d97c3 | ||
|
|
cb4e7d4943 | ||
|
|
b9d5ed699e | ||
|
|
75fe5c8970 | ||
|
|
91da2d01b7 | ||
|
|
4bade72ce4 | ||
|
|
9c05e2feed | ||
|
|
ba94098411 | ||
|
|
43c691160e | ||
|
|
e2daa091e8 | ||
|
|
f026414b1a | ||
|
|
fd640a4bd0 | ||
|
|
dd433d7f99 | ||
|
|
3183e00acc | ||
|
|
1860de12ea | ||
|
|
277f1ab252 | ||
|
|
324b332fa6 | ||
|
|
33d29f6b02 | ||
|
|
419aed6375 | ||
|
|
7bb26ac3be | ||
|
|
7654333ec1 | ||
|
|
d3a2da391d | ||
|
|
f19f59c37f | ||
|
|
94e53e14a3 | ||
|
|
d2b3788872 | ||
|
|
8095062db2 | ||
|
|
89beaa9316 | ||
|
|
dfaffd043e | ||
|
|
a8c200222f | ||
|
|
598a1c28ac | ||
|
|
b972703f34 | ||
|
|
d30fdc67c6 | ||
|
|
6a5a2e6144 | ||
|
|
235143528c | ||
|
|
33ca2b8d09 | ||
|
|
50176cfafb | ||
|
|
a67fb36a84 | ||
|
|
aea9984187 | ||
|
|
7a6c234b40 | ||
|
|
4cdaa513d3 | ||
|
|
aaf655ea5b | ||
|
|
207c824ec7 | ||
|
|
29abe0bacb | ||
|
|
d14eda2ca0 | ||
|
|
0425cf41d0 | ||
|
|
4ff014f288 | ||
|
|
2e24500866 | ||
|
|
f13a256150 | ||
|
|
c958e8cba9 | ||
|
|
7668da68d3 | ||
|
|
c478a0c0fc | ||
|
|
fe60a08817 | ||
|
|
dec5369a8f | ||
|
|
5ec0b9393e | ||
|
|
3d8f724bb1 | ||
|
|
bc7a663f92 | ||
|
|
90369e927f | ||
|
|
034cad79c7 | ||
|
|
5c8ba7e29e | ||
|
|
bf1dc00338 | ||
|
|
54f7fd08cb | ||
|
|
e7afa07c84 | ||
|
|
e196ef03d6 | ||
|
|
0fa452ca04 | ||
|
|
15b625a3f8 | ||
|
|
e2427c0683 | ||
|
|
a0e8039d92 | ||
|
|
2f87b1f398 | ||
|
|
86fd2e8e1e | ||
|
|
4167f55ad8 | ||
|
|
76919a13b8 | ||
|
|
fe0f159490 | ||
|
|
4527a6399e | ||
|
|
b8ff75c5ce | ||
|
|
049e8af8b2 | ||
|
|
8ce2386cc9 | ||
|
|
5239040d9c | ||
|
|
c5eb7578b7 | ||
|
|
76ce44230d | ||
|
|
4a20371b87 | ||
|
|
9a76c30aaf | ||
|
|
9cba57368e | ||
|
|
d895f8d771 | ||
|
|
de8c9f4878 | ||
|
|
f029cf842a | ||
|
|
6ce9b77b3c | ||
|
|
5a60c6ec67 | ||
|
|
a1ca768711 | ||
|
|
f0d2c19393 | ||
|
|
67db05a0c3 | ||
|
|
05e932b884 | ||
|
|
763713cd32 | ||
|
|
2687448212 | ||
|
|
d059195e92 | ||
|
|
6ef7acc8e5 | ||
|
|
2cb89807ef | ||
|
|
b5fcad3db0 | ||
|
|
d3fd441c88 | ||
|
|
e9568b50fc | ||
|
|
e7040a518a |
@@ -2,7 +2,5 @@
|
|||||||
; SPDX-License-Identifier: CC0-1.0
|
; SPDX-License-Identifier: CC0-1.0
|
||||||
|
|
||||||
[BlueprintSettings]
|
[BlueprintSettings]
|
||||||
kde/frameworks/extra-cmake-modules.version=master
|
|
||||||
kde/unreleased/kirigami-addons.version=master
|
|
||||||
kde/applications/neochat.packageAppx=True
|
kde/applications/neochat.packageAppx=True
|
||||||
libs/qt.qtMajorVersion=6
|
libs/qt.qtMajorVersion=6
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"id": "org.kde.neochat",
|
"id": "org.kde.neochat",
|
||||||
"branch": "master",
|
"branch": "master",
|
||||||
"runtime": "org.kde.Platform",
|
"runtime": "org.kde.Platform",
|
||||||
"runtime-version": "6.8",
|
"runtime-version": "6.9",
|
||||||
"sdk": "org.kde.Sdk",
|
"sdk": "org.kde.Sdk",
|
||||||
"command": "neochat",
|
"command": "neochat",
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -149,27 +149,6 @@
|
|||||||
],
|
],
|
||||||
"builddir": true
|
"builddir": true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "qcoro",
|
|
||||||
"buildsystem": "cmake-ninja",
|
|
||||||
"config-opts": [
|
|
||||||
"-DQCORO_BUILD_EXAMPLES=OFF",
|
|
||||||
"-DBUILD_TESTING=OFF"
|
|
||||||
],
|
|
||||||
"sources": [
|
|
||||||
{
|
|
||||||
"type": "archive",
|
|
||||||
"url": "https://github.com/danvratil/qcoro/archive/refs/tags/v0.11.0.tar.gz",
|
|
||||||
"sha256": "9942c5b4c533192f6c5954dc6d10178b3829075e6a621b67df73f0a4b74d8297",
|
|
||||||
"x-checker-data": {
|
|
||||||
"type": "anitya",
|
|
||||||
"project-id": 236236,
|
|
||||||
"stable-only": true,
|
|
||||||
"url-template": "https://github.com/danvratil/qcoro/archive/refs/tags/v$version.tar.gz"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "kunifiedpush",
|
"name": "kunifiedpush",
|
||||||
"buildsystem": "cmake-ninja",
|
"buildsystem": "cmake-ninja",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ cmake_minimum_required(VERSION 3.16)
|
|||||||
# KDE Applications version, managed by release script.
|
# KDE Applications version, managed by release script.
|
||||||
set(RELEASE_SERVICE_VERSION_MAJOR "25")
|
set(RELEASE_SERVICE_VERSION_MAJOR "25")
|
||||||
set(RELEASE_SERVICE_VERSION_MINOR "07")
|
set(RELEASE_SERVICE_VERSION_MINOR "07")
|
||||||
set(RELEASE_SERVICE_VERSION_MICRO "70")
|
set(RELEASE_SERVICE_VERSION_MICRO "80")
|
||||||
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
|
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
|
||||||
|
|
||||||
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
|
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
|
||||||
@@ -164,6 +164,7 @@ endif()
|
|||||||
|
|
||||||
if(ANDROID)
|
if(ANDROID)
|
||||||
find_package(Sqlite3)
|
find_package(Sqlite3)
|
||||||
|
set(BUILD_TESTING FALSE)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
ki18n_install(po)
|
ki18n_install(po)
|
||||||
@@ -178,7 +179,7 @@ add_definitions(-DQT_NO_FOREACH)
|
|||||||
add_subdirectory(src)
|
add_subdirectory(src)
|
||||||
|
|
||||||
if (BUILD_TESTING)
|
if (BUILD_TESTING)
|
||||||
find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test)
|
find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test HttpServer)
|
||||||
add_subdirectory(autotests)
|
add_subdirectory(autotests)
|
||||||
# add_subdirectory(appiumtests)
|
# add_subdirectory(appiumtests)
|
||||||
if (NOT ANDROID)
|
if (NOT ANDROID)
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
|
|
||||||
enable_testing()
|
enable_testing()
|
||||||
|
|
||||||
|
add_library(neochat_server STATIC server.cpp)
|
||||||
|
|
||||||
|
target_link_libraries(neochat_server PUBLIC Qt::HttpServer QuotientQt6)
|
||||||
|
|
||||||
add_definitions(-DDATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data" )
|
add_definitions(-DDATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data" )
|
||||||
|
|
||||||
ecm_add_test(
|
ecm_add_test(
|
||||||
@@ -85,6 +89,6 @@ ecm_add_test(
|
|||||||
|
|
||||||
ecm_add_test(
|
ecm_add_test(
|
||||||
actionstest.cpp
|
actionstest.cpp
|
||||||
LINK_LIBRARIES neochat Qt::Test
|
LINK_LIBRARIES neochat Qt::Test neochat_server
|
||||||
TEST_NAME actionstest
|
TEST_NAME actionstest
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,9 +6,11 @@
|
|||||||
#include <QSignalSpy>
|
#include <QSignalSpy>
|
||||||
#include <QVariantList>
|
#include <QVariantList>
|
||||||
|
|
||||||
|
#include "accountmanager.h"
|
||||||
#include "chatbarcache.h"
|
#include "chatbarcache.h"
|
||||||
#include "models/actionsmodel.h"
|
#include "models/actionsmodel.h"
|
||||||
|
|
||||||
|
#include "server.h"
|
||||||
#include "testutils.h"
|
#include "testutils.h"
|
||||||
|
|
||||||
using namespace Quotient;
|
using namespace Quotient;
|
||||||
@@ -21,10 +23,12 @@ class ActionsTest : public QObject
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
Connection *connection = nullptr;
|
Connection *connection = nullptr;
|
||||||
TestUtils::TestRoom *room = nullptr;
|
NeoChatRoom *room = nullptr;
|
||||||
|
|
||||||
void expectMessage(const QString &actionName, const QString &args, MessageType::Type type, const QString &message);
|
void expectMessage(const QString &actionName, const QString &args, MessageType::Type type, const QString &message);
|
||||||
|
|
||||||
|
Server server;
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void initTestCase();
|
void initTestCase();
|
||||||
void testActions();
|
void testActions();
|
||||||
@@ -34,8 +38,23 @@ private Q_SLOTS:
|
|||||||
|
|
||||||
void ActionsTest::initTestCase()
|
void ActionsTest::initTestCase()
|
||||||
{
|
{
|
||||||
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
|
Connection::setRoomType<NeoChatRoom>();
|
||||||
room = new TestUtils::TestRoom(connection, QStringLiteral("#myroom:kde.org"), QLatin1String("test-min-sync.json"));
|
server.start();
|
||||||
|
KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat"));
|
||||||
|
auto accountManager = new AccountManager(true);
|
||||||
|
QSignalSpy spy(accountManager, &AccountManager::connectionAdded);
|
||||||
|
connection = accountManager->accounts()->front();
|
||||||
|
auto roomId = server.createRoom(u"@user:localhost:1234"_s);
|
||||||
|
server.inviteUser(roomId, u"@invited:example.com"_s);
|
||||||
|
server.banUser(roomId, u"@banned:example.com"_s);
|
||||||
|
server.joinUser(roomId, u"@example:example.com"_s);
|
||||||
|
|
||||||
|
QSignalSpy syncSpy(connection, &Connection::syncDone);
|
||||||
|
// We need to wait for two syncs, as the next one won't have the changes yet
|
||||||
|
QVERIFY(syncSpy.wait());
|
||||||
|
QVERIFY(syncSpy.wait());
|
||||||
|
room = dynamic_cast<NeoChatRoom *>(connection->room(roomId));
|
||||||
|
QVERIFY(room);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ActionsTest::testActions_data()
|
void ActionsTest::testActions_data()
|
||||||
@@ -90,7 +109,7 @@ static ActionsModel::Action findAction(const QString &name)
|
|||||||
void ActionsTest::expectMessage(const QString &actionName, const QString &args, MessageType::Type type, const QString &message)
|
void ActionsTest::expectMessage(const QString &actionName, const QString &args, MessageType::Type type, const QString &message)
|
||||||
{
|
{
|
||||||
auto action = findAction(actionName);
|
auto action = findAction(actionName);
|
||||||
QSignalSpy spy(room, &TestUtils::TestRoom::showMessage);
|
QSignalSpy spy(room, &NeoChatRoom::showMessage);
|
||||||
auto result = action.handle(args, room, nullptr);
|
auto result = action.handle(args, room, nullptr);
|
||||||
auto expected = QVariantList {type, message};
|
auto expected = QVariantList {type, message};
|
||||||
auto signal = spy.takeFirst();
|
auto signal = spy.takeFirst();
|
||||||
@@ -106,14 +125,26 @@ void ActionsTest::testInvite()
|
|||||||
QCOMPARE(room->memberState(u"@banned:example.com"_s), Membership::Ban);
|
QCOMPARE(room->memberState(u"@banned:example.com"_s), Membership::Ban);
|
||||||
expectMessage(u"invite"_s, connection->userId(), MessageType::Positive, u"You are already in this room."_s);
|
expectMessage(u"invite"_s, connection->userId(), MessageType::Positive, u"You are already in this room."_s);
|
||||||
QCOMPARE(room->memberState(connection->userId()), Membership::Join);
|
QCOMPARE(room->memberState(connection->userId()), Membership::Join);
|
||||||
expectMessage(u"invite"_s, u"@example:example.org"_s, MessageType::Information, u"@example:example.org is already in this room."_s);
|
expectMessage(u"invite"_s, u"@example:example.com"_s, MessageType::Information, u"@example:example.com is already in this room."_s);
|
||||||
QCOMPARE(room->memberState(u"@example:example.org"_s), Membership::Join);
|
QCOMPARE(room->memberState(u"@example:example.com"_s), Membership::Join);
|
||||||
|
|
||||||
QCOMPARE(room->memberState(u"@user:example.com"_s), Membership::Leave);
|
QCOMPARE(room->memberState(u"@user:example.com"_s), Membership::Leave);
|
||||||
expectMessage(u"invite"_s, u"@user:example.com"_s, MessageType::Positive, u"@user:example.com was invited into this room."_s);
|
expectMessage(u"invite"_s, u"@user:example.com"_s, MessageType::Positive, u"@user:example.com was invited into this room."_s);
|
||||||
|
|
||||||
//TODO mock server, wait for invite state to change
|
QSignalSpy spy(room, &NeoChatRoom::changed);
|
||||||
//TODO QCOMPARE(room->memberState(u"@user:example.com"_s), Membership::Invite);
|
QVERIFY(spy.wait());
|
||||||
|
|
||||||
|
auto tries = 0;
|
||||||
|
|
||||||
|
while (room->memberState(u"@user:example.com"_s) != Membership::Invite) {
|
||||||
|
QVERIFY(spy.wait());
|
||||||
|
tries += 1;
|
||||||
|
if (tries > 3) {
|
||||||
|
QVERIFY(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QCOMPARE(room->memberState(u"@user:example.com"_s), Membership::Invite);
|
||||||
}
|
}
|
||||||
|
|
||||||
QTEST_MAIN(ActionsTest)
|
QTEST_MAIN(ActionsTest)
|
||||||
|
|||||||
20
autotests/data/localhost.crt
Normal file
20
autotests/data/localhost.crt
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDNTCCAh2gAwIBAgIUXbyWfTfcvVLrVB1qx36pW/7IkwMwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwQjELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEcMBoGA1UE
|
||||||
|
CgwTRGVmYXVsdCBDb21wYW55IEx0ZDAgFw0yNDEyMjQxNTAxMDNaGA8yNTcyMDcy
|
||||||
|
NDE1MDEwM1owQjELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEc
|
||||||
|
MBoGA1UECgwTRGVmYXVsdCBDb21wYW55IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQAD
|
||||||
|
ggEPADCCAQoCggEBAKlxZ540TQ1uUDAR7ZJ9ue0PzcD2dPmblIIddyekvZS59V7X
|
||||||
|
drhamclXpHE2EelR87Sexst0BaHH/jmrHwxCtwbeXHZ8ueJHkGHJ5DLZCCiwfG+Q
|
||||||
|
gml7wlSXxXz37vie2tdlZh2yJSM8yvLAYceHb2zOskaGvul7ZITIS0JrPc3o6VZk
|
||||||
|
+MYGkYtA2JfUsv3jH4oQbxOf7RXqhWNAXbB+3hlwRBwMIdyoBNK6YS9QSrTeS9jj
|
||||||
|
UqgO5QmaQZOVvpaPf1Y/rHHLd2Qa6+a/cCJ1sr2biagb75AihpQFsK/oy6D1PP70
|
||||||
|
zTe7hPWn/efEpmtCV7CQ8ti4cRu0Kjy0T8grtCsCAwEAAaMhMB8wHQYDVR0OBBYE
|
||||||
|
FIFlylzwADNLfgTDNkhFeFelaEDxMA0GCSqGSIb3DQEBCwUAA4IBAQBQ2rw4GLIU
|
||||||
|
v+GY7Qru9LttkrQPd2bZXKxDMd/jT+wjmMVtqS4MAsCuDYwaYLjU1aWyqy0mN+lY
|
||||||
|
A17kD0VjBNBy45sYqkZveY0ks8mCScBemtrIDmjz2tiueecBIEASwEPBOZgv5/MV
|
||||||
|
cz864FiChF+2r8Zl8bhycGy9DEpRjzYKvIQWSDHQ3zpuh3iBnjfoieLHWX2kKCpk
|
||||||
|
ouS3V6485rHNCWsZT5IcCwfBFQkOuWRJpIazpz4AfwZh1TK9+bgiKA5EyZjSNrKw
|
||||||
|
xGQSpMSTRQMB0/FOCL/AixhN9unVFUViqUcdtSfoHE1VyBHv9kDT/cYms/Xl4B0t
|
||||||
|
/ZSQJ/D/Km1+
|
||||||
|
-----END CERTIFICATE-----
|
||||||
28
autotests/data/localhost.key
Normal file
28
autotests/data/localhost.key
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCpcWeeNE0NblAw
|
||||||
|
Ee2SfbntD83A9nT5m5SCHXcnpL2UufVe13a4WpnJV6RxNhHpUfO0nsbLdAWhx/45
|
||||||
|
qx8MQrcG3lx2fLniR5BhyeQy2QgosHxvkIJpe8JUl8V89+74ntrXZWYdsiUjPMry
|
||||||
|
wGHHh29szrJGhr7pe2SEyEtCaz3N6OlWZPjGBpGLQNiX1LL94x+KEG8Tn+0V6oVj
|
||||||
|
QF2wft4ZcEQcDCHcqATSumEvUEq03kvY41KoDuUJmkGTlb6Wj39WP6xxy3dkGuvm
|
||||||
|
v3AidbK9m4moG++QIoaUBbCv6Mug9Tz+9M03u4T1p/3nxKZrQlewkPLYuHEbtCo8
|
||||||
|
tE/IK7QrAgMBAAECggEAH9qmeKrra2F4KLlOGNKS//qPGz4Z+ozhi95/NpA1Zb7Z
|
||||||
|
3pUSCBFcROo5i2D3WA4kiymoRLpQjrv60puVcCggoWVvK4VCKsR6Y6/hOx/q9T9M
|
||||||
|
fWrE4ZC3FVEc+uPfZJT0nja9TkrdyXSV0LITD8Ap1eI7yJ9vR5R/bqj64QcpLMrU
|
||||||
|
QeoQIy1oTMR+qdjj33duyRwBZU3Yf8FRB2iW6OILZ8hzFo1jngec7dph9a1RK4e0
|
||||||
|
mEPdc9ywsKlDM7P0Y7zdmjar5XtQn87GiwNhz23f1fzCC2axLtOW0Xm4e4Qumehb
|
||||||
|
WrIi6Vfq8IWMglU7QrBJ7iR0Ls+XoKA5GxomV2IJZQKBgQDoIkOl5YGPQ3iGR+WK
|
||||||
|
e5/2Ml4G/uURzYiOlzSsyfoPXyO4EI2BJd5HkH+EvfgRx4xKkxUZRJdzR7llYPl8
|
||||||
|
BFYcFitvhO8SbD0mNAB5YW7f+3v1pgEN2umzoKd389Zx5WqTZ7YB1VG5RN/Q1JJL
|
||||||
|
2JM0Xgamq2vNtx3roRPxDBeW7QKBgQC63R/bmACJbgIzfaVBX4Zie3NQG0/Hf+gF
|
||||||
|
LnBwUmQDZOR7MY+kSiIUVMn3NuZRiCSCFBVwApruyK8r535JCibTVm5PWjvhFddY
|
||||||
|
LgaPOCKGlm9TLScjoH1pErYgG3uJ4nXeRfXhg4mco6EkrC7RzQywrd0VDoqpuc1Y
|
||||||
|
EKfEsYk8dwKBgE+mSh3nNOBKX1V73+f3aTiZqaeu2DyWkG+UtE9BclrJ40Cp9VPG
|
||||||
|
AZH+o7KRWEgJdzqzYv7riSfWCWgesRv7hOxYMwktzLY+i3DLUQpVAy05ZhwwnJX7
|
||||||
|
ckrfKfc/pGoqNLplUI8qecMfPciy14vMwR2r0Y5orTHFzi9mcqg35PQ1AoGAW2LX
|
||||||
|
OLq+0HdHhk0Va8I+450CSRQCUUvhed87SANTPEG0Z/dWC3/h6NWKrGdh/k+5oxAV
|
||||||
|
Z+EuSkdFPBCLt0bKtCKZ8h7sF+lplotz08kdQXsC2MfFU2wiySdIgK1QHp/tCxZl
|
||||||
|
6LM+sqdnoJrAjwRcB3AQJkMlV1ox7ba/hbdZqYMCgYBS6+JUXSSASpm5ZHd32a8m
|
||||||
|
xwryEZ7H6Hek6lvMHdxmwoKat5dCavxw64nrtyeeGZpg1W3zLLyamF9x/8kMyr6y
|
||||||
|
KKvtBfJ5sCvAbt80o9Pbs6R3yDB3AKiD3s3PQK7lol1nhE/8IbsF2r8JEQVcYd/k
|
||||||
|
oBzkl7MrMyLhhaCqSxwqQQ==
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
235
autotests/server.cpp
Normal file
235
autotests/server.cpp
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 Tobias Fella <tobias.fella@kde.org>
|
||||||
|
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "server.h"
|
||||||
|
|
||||||
|
#include <QFile>
|
||||||
|
#include <QHttpServer>
|
||||||
|
#include <QHttpServerResponder>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QSslCertificate>
|
||||||
|
#include <QSslKey>
|
||||||
|
#include <QSslServer>
|
||||||
|
#include <QUuid>
|
||||||
|
|
||||||
|
#include <Quotient/networkaccessmanager.h>
|
||||||
|
|
||||||
|
using namespace Qt::Literals::StringLiterals;
|
||||||
|
|
||||||
|
QString generateEventId()
|
||||||
|
{
|
||||||
|
return u"$"_s + QString::fromLatin1(QCryptographicHash::hash(QUuid::createUuid().toString().toLatin1(), QCryptographicHash::Sha1).toBase64());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString generateRoomId()
|
||||||
|
{
|
||||||
|
return u"!%1:localhost:1234"_s
|
||||||
|
.arg(QString::fromLatin1(QCryptographicHash::hash(QUuid::createUuid().toString().toLatin1(), QCryptographicHash::Sha1).toBase64()))
|
||||||
|
.replace(u'/', QChar());
|
||||||
|
}
|
||||||
|
|
||||||
|
Server::Server()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void Server::start()
|
||||||
|
{
|
||||||
|
QObject::connect(Quotient::NetworkAccessManager::instance(),
|
||||||
|
&QNetworkAccessManager::sslErrors,
|
||||||
|
Quotient::NetworkAccessManager::instance(),
|
||||||
|
[](QNetworkReply *reply) {
|
||||||
|
reply->ignoreSslErrors();
|
||||||
|
});
|
||||||
|
m_server.route(u"/.well-known/matrix/client"_s, QHttpServerRequest::Method::Get, [](QHttpServerResponder &responder) {
|
||||||
|
responder.write(QJsonDocument(QJsonObject{
|
||||||
|
{u"m.homeserver"_s, QJsonObject{{u"base_url"_s, u"https://localhost:1234"_s}}},
|
||||||
|
}),
|
||||||
|
QHttpServerResponder::StatusCode::Ok);
|
||||||
|
});
|
||||||
|
m_server.route(u"/_matrix/client/versions"_s, QHttpServerRequest::Method::Get, [](QHttpServerResponder &responder) {
|
||||||
|
responder.write(QJsonDocument(QJsonObject{
|
||||||
|
{u"versions"_s,
|
||||||
|
QJsonArray{
|
||||||
|
u"v1.0"_s,
|
||||||
|
u"v1.1"_s,
|
||||||
|
u"v1.2"_s,
|
||||||
|
u"v1.3"_s,
|
||||||
|
u"v1.4"_s,
|
||||||
|
u"v1.5"_s,
|
||||||
|
u"v1.6"_s,
|
||||||
|
u"v1.7"_s,
|
||||||
|
u"v1.8"_s,
|
||||||
|
u"v1.9"_s,
|
||||||
|
u"v1.10"_s,
|
||||||
|
u"v1.11"_s,
|
||||||
|
u"v1.12"_s,
|
||||||
|
u"v1.13"_s,
|
||||||
|
}},
|
||||||
|
}),
|
||||||
|
QHttpServerResponder::StatusCode::Ok);
|
||||||
|
});
|
||||||
|
m_server.route(u"/_matrix/client/v3/capabilities"_s, QHttpServerRequest::Method::Get, [](QHttpServerResponder &responder) {
|
||||||
|
responder.write(
|
||||||
|
QJsonDocument(QJsonObject{{u"capabilities"_s,
|
||||||
|
QJsonObject{
|
||||||
|
{u"m.room_versions"_s, QJsonObject{{u"m.available"_s, QJsonObject{{u"1"_s, u"stable"_s}}}, {u"default"_s, u"1"_s}}},
|
||||||
|
}}}),
|
||||||
|
QHttpServerResponder::StatusCode::Ok);
|
||||||
|
});
|
||||||
|
m_server.route(u"/_matrix/client/v3/account/whoami"_s, QHttpServerRequest::Method::Get, [](QHttpServerResponder &responder) {
|
||||||
|
responder.write(QJsonDocument(QJsonObject{
|
||||||
|
{u"device_id"_s, u"device_id_1234"_s},
|
||||||
|
{u"user_id"_s, u"@user:localhost:1234"_s},
|
||||||
|
}),
|
||||||
|
QHttpServerResponder::StatusCode::Ok);
|
||||||
|
});
|
||||||
|
|
||||||
|
m_server.route(u"/_matrix/client/v3/login"_s, QHttpServerRequest::Method::Post, [](QHttpServerResponder &responder) {
|
||||||
|
// TODO
|
||||||
|
// if data["identifier"]["user"] != "user" or data["password"] != "1234":
|
||||||
|
// abort(403)
|
||||||
|
responder.write(QJsonDocument(QJsonObject{
|
||||||
|
{u"access_token"_s, u"token_login"_s},
|
||||||
|
{u"device_id"_s, u"device_1234"_s},
|
||||||
|
{u"user_id"_s, u"@user:localhost:1234"_s},
|
||||||
|
}),
|
||||||
|
QHttpServerResponder::StatusCode::Ok);
|
||||||
|
});
|
||||||
|
|
||||||
|
m_server.route(u"/_matrix/client/v3/login"_s, QHttpServerRequest::Method::Get, [](QHttpServerResponder &responder) {
|
||||||
|
responder.write(QJsonDocument(QJsonObject{
|
||||||
|
{u"flows"_s, QJsonArray{QJsonObject{{u"type"_s, u"m.login.password"_s}}}},
|
||||||
|
}),
|
||||||
|
QHttpServerResponder::StatusCode::Ok);
|
||||||
|
});
|
||||||
|
|
||||||
|
m_server.route(u"/_matrix/client/v3/rooms/<arg>/invite"_s,
|
||||||
|
QHttpServerRequest::Method::Post,
|
||||||
|
[this](const QString &roomId, QHttpServerResponder &responder, const QHttpServerRequest &request) {
|
||||||
|
m_invitedUsers[roomId] += QJsonDocument::fromJson(request.body()).object()[u"user_id"_s].toString();
|
||||||
|
responder.write(QJsonDocument(QJsonObject{}), QHttpServerResponder::StatusCode::Ok);
|
||||||
|
});
|
||||||
|
|
||||||
|
m_server.route(u"/_matrix/client/r0/sync"_s, QHttpServerRequest::Method::Get, [this](QHttpServerResponder &responder) {
|
||||||
|
QMap<QString, QJsonArray> stateEvents;
|
||||||
|
|
||||||
|
for (const auto &[roomId, matrixId] : m_roomsToCreate) {
|
||||||
|
stateEvents[roomId] += QJsonObject{
|
||||||
|
{u"content"_s, QJsonObject{{u"room_version"_s, u"11"_s}}},
|
||||||
|
{u"event_id"_s, generateEventId()},
|
||||||
|
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
|
||||||
|
{u"room_id"_s, roomId},
|
||||||
|
{u"sender"_s, matrixId},
|
||||||
|
{u"state_key"_s, QString()},
|
||||||
|
{u"type"_s, u"m.room.create"_s},
|
||||||
|
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
|
||||||
|
};
|
||||||
|
stateEvents[roomId] += QJsonObject{
|
||||||
|
{u"content"_s, QJsonObject{{u"displayname"_s, u"User"_s}, {u"membership"_s, u"join"_s}}},
|
||||||
|
{u"event_id"_s, generateEventId()},
|
||||||
|
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
|
||||||
|
{u"room_id"_s, roomId},
|
||||||
|
{u"sender"_s, matrixId},
|
||||||
|
{u"state_key"_s, matrixId},
|
||||||
|
{u"type"_s, u"m.room.member"_s},
|
||||||
|
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
m_roomsToCreate.clear();
|
||||||
|
for (const auto &roomId : m_invitedUsers.keys()) {
|
||||||
|
const auto &values = m_invitedUsers[roomId];
|
||||||
|
for (const auto &value : values) {
|
||||||
|
stateEvents[roomId] += QJsonObject{
|
||||||
|
{u"content"_s, QJsonObject{{u"displayname"_s, u"User"_s}, {u"membership"_s, u"invite"_s}}},
|
||||||
|
{u"event_id"_s, generateEventId()},
|
||||||
|
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
|
||||||
|
{u"room_id"_s, roomId},
|
||||||
|
{u"sender"_s, u"@user:localhost:1234"_s},
|
||||||
|
{u"state_key"_s, value},
|
||||||
|
{u"type"_s, u"m.room.member"_s},
|
||||||
|
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_invitedUsers.clear();
|
||||||
|
|
||||||
|
for (const auto &roomId : m_bannedUsers.keys()) {
|
||||||
|
const auto &values = m_bannedUsers[roomId];
|
||||||
|
for (const auto &value : values) {
|
||||||
|
stateEvents[roomId] += QJsonObject{
|
||||||
|
{u"content"_s, QJsonObject{{u"displayname"_s, u"User"_s}, {u"membership"_s, u"ban"_s}}},
|
||||||
|
{u"event_id"_s, generateEventId()},
|
||||||
|
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
|
||||||
|
{u"room_id"_s, roomId},
|
||||||
|
{u"sender"_s, u"@user:localhost:1234"_s},
|
||||||
|
{u"state_key"_s, value},
|
||||||
|
{u"type"_s, u"m.room.member"_s},
|
||||||
|
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_bannedUsers.clear();
|
||||||
|
|
||||||
|
for (const auto &roomId : m_joinedUsers.keys()) {
|
||||||
|
const auto &values = m_joinedUsers[roomId];
|
||||||
|
for (const auto &value : values) {
|
||||||
|
stateEvents[roomId] += QJsonObject{
|
||||||
|
{u"content"_s, QJsonObject{{u"displayname"_s, u"User"_s}, {u"membership"_s, u"join"_s}}},
|
||||||
|
{u"event_id"_s, generateEventId()},
|
||||||
|
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
|
||||||
|
{u"room_id"_s, roomId},
|
||||||
|
{u"sender"_s, u"@user:localhost:1234"_s},
|
||||||
|
{u"state_key"_s, value},
|
||||||
|
{u"type"_s, u"m.room.member"_s},
|
||||||
|
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_joinedUsers.clear();
|
||||||
|
|
||||||
|
QJsonObject rooms;
|
||||||
|
for (const auto &roomId : stateEvents.keys()) {
|
||||||
|
rooms[roomId] = QJsonObject{{u"state"_s, QJsonObject{{u"events"_s, stateEvents[roomId]}}}};
|
||||||
|
}
|
||||||
|
|
||||||
|
responder.write(QJsonDocument(QJsonObject{{u"rooms"_s, QJsonObject{{u"join"_s, rooms}}}}), QHttpServerResponder::StatusCode::Ok);
|
||||||
|
});
|
||||||
|
|
||||||
|
QSslConfiguration config;
|
||||||
|
QFile key(QStringLiteral(DATA_DIR) + u"/localhost.key"_s);
|
||||||
|
key.open(QFile::ReadOnly);
|
||||||
|
config.setPrivateKey(QSslKey(&key, QSsl::Rsa));
|
||||||
|
config.setLocalCertificate(QSslCertificate::fromPath(QStringLiteral(DATA_DIR) + u"/localhost.crt"_s).front());
|
||||||
|
m_sslServer.setSslConfiguration(config);
|
||||||
|
if (!m_sslServer.listen(QHostAddress::LocalHost, 1234) || !m_server.bind(&m_sslServer)) {
|
||||||
|
qFatal() << "Server failed to listen on a port.";
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
qWarning() << "Server listening";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Server::createRoom(const QString &matrixId)
|
||||||
|
{
|
||||||
|
auto roomId = generateRoomId();
|
||||||
|
m_roomsToCreate += {roomId, matrixId};
|
||||||
|
return roomId;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Server::inviteUser(const QString &roomId, const QString &matrixId)
|
||||||
|
{
|
||||||
|
m_invitedUsers[roomId] += matrixId;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Server::banUser(const QString &roomId, const QString &matrixId)
|
||||||
|
{
|
||||||
|
m_bannedUsers[roomId] += matrixId;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Server::joinUser(const QString &roomId, const QString &matrixId)
|
||||||
|
{
|
||||||
|
m_joinedUsers[roomId] += matrixId;
|
||||||
|
}
|
||||||
33
autotests/server.h
Normal file
33
autotests/server.h
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 Tobias Fella <tobias.fella@kde.org>
|
||||||
|
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <QHttpServer>
|
||||||
|
#include <QSslServer>
|
||||||
|
|
||||||
|
class Server
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Server();
|
||||||
|
|
||||||
|
void start();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a room and place the user with id matrixId in it.
|
||||||
|
* Returns the room's id
|
||||||
|
*/
|
||||||
|
QString createRoom(const QString &matrixId);
|
||||||
|
|
||||||
|
void inviteUser(const QString &roomId, const QString &matrixId);
|
||||||
|
void banUser(const QString &roomId, const QString &matrixId);
|
||||||
|
void joinUser(const QString &roomId, const QString &matrixId);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QHttpServer m_server;
|
||||||
|
QSslServer m_sslServer;
|
||||||
|
|
||||||
|
QHash<QString, QList<QString>> m_invitedUsers;
|
||||||
|
QHash<QString, QList<QString>> m_bannedUsers;
|
||||||
|
QHash<QString, QList<QString>> m_joinedUsers;
|
||||||
|
|
||||||
|
QList<std::pair<QString, QString>> m_roomsToCreate;
|
||||||
|
};
|
||||||
@@ -61,6 +61,7 @@ private Q_SLOTS:
|
|||||||
void receiveRichStrikethrough();
|
void receiveRichStrikethrough();
|
||||||
void receiveRichtextIn();
|
void receiveRichtextIn();
|
||||||
void receiveRichMxcUrl();
|
void receiveRichMxcUrl();
|
||||||
|
void receiveRichPlainUrl_data();
|
||||||
void receiveRichPlainUrl();
|
void receiveRichPlainUrl();
|
||||||
void receiveRichEdited_data();
|
void receiveRichEdited_data();
|
||||||
void receiveRichEdited();
|
void receiveRichEdited();
|
||||||
@@ -264,6 +265,8 @@ void TextHandlerTest::sendCustomTags_data()
|
|||||||
QTest::newRow("inside code block spoiler") << u"```||apple||```"_s << u"<code>||apple||</code>"_s;
|
QTest::newRow("inside code block spoiler") << u"```||apple||```"_s << u"<code>||apple||</code>"_s;
|
||||||
QTest::newRow("outside code block spoiler") << u"||apple|| ```||banana||``` ||pear||"_s
|
QTest::newRow("outside code block spoiler") << u"||apple|| ```||banana||``` ||pear||"_s
|
||||||
<< u"<span data-mx-spoiler>apple</span> <code>||banana||</code> <span data-mx-spoiler>pear</span>"_s;
|
<< u"<span data-mx-spoiler>apple</span> <code>||banana||</code> <span data-mx-spoiler>pear</span>"_s;
|
||||||
|
QTest::newRow("complex spoiler") << u"Between `formFactor == Horizontal||Vertical` and `location == top||left||bottom||right`"_s
|
||||||
|
<< u"Between <code>formFactor == Horizontal||Vertical</code> and <code>location == top||left||bottom||right</code>"_s;
|
||||||
|
|
||||||
// strikethrough
|
// strikethrough
|
||||||
QTest::newRow("incomplete strikethrough") << u"~~test"_s << u"~~test"_s;
|
QTest::newRow("incomplete strikethrough") << u"~~test"_s << u"~~test"_s;
|
||||||
@@ -448,6 +451,32 @@ void TextHandlerTest::receiveRichMxcUrl()
|
|||||||
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, room->messageEvents().at(0).get()), testOutputString);
|
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, room->messageEvents().at(0).get()), testOutputString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TextHandlerTest::receiveRichPlainUrl_data()
|
||||||
|
{
|
||||||
|
QTest::addColumn<QString>("input");
|
||||||
|
QTest::addColumn<QString>("output");
|
||||||
|
|
||||||
|
// This is an actual link that caused trouble which is why it's so long. Keeping
|
||||||
|
// so we can confirm consistent behaviour for complex urls.
|
||||||
|
QTest::addRow("link 1")
|
||||||
|
<< u"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im <a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im\">Link already rich</a>"_s
|
||||||
|
<< u"<a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im\">https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im</a> <a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im\">Link already rich</a>"_s;
|
||||||
|
|
||||||
|
// Another real case. The linkification wasn't handling it when a single link
|
||||||
|
// contains what looks like and email. It was broken into 3 but needs to
|
||||||
|
// be just single link.
|
||||||
|
QTest::addRow("link 2")
|
||||||
|
<< u"https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/"_s
|
||||||
|
<< u"<a href=\"https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/\">https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/</a>"_s;
|
||||||
|
|
||||||
|
QTest::addRow("email") << uR"(email@example.com <a href="mailto:email@example.com">Link already rich</a>)"_s
|
||||||
|
<< uR"(<a href="mailto:email@example.com">email@example.com</a> <a href="mailto:email@example.com">Link already rich</a>)"_s;
|
||||||
|
QTest::addRow("mxid")
|
||||||
|
<< u"@user:kde.org <a href=\"https://matrix.to/#/@user:kde.org\">Link already rich</a>"_s
|
||||||
|
<< u"<b><a href=\"https://matrix.to/#/@user:kde.org\">@user:kde.org</a></b> <b><a href=\"https://matrix.to/#/@user:kde.org\">Link already rich</a></b>"_s;
|
||||||
|
QTest::addRow("mxid with prefix") << u"a @user:kde.org b"_s << u"a <b><a href=\"https://matrix.to/#/@user:kde.org\">@user:kde.org</a></b> b"_s;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For when your rich input string has a plain text url left in.
|
* For when your rich input string has a plain text url left in.
|
||||||
*
|
*
|
||||||
@@ -456,46 +485,13 @@ void TextHandlerTest::receiveRichMxcUrl()
|
|||||||
*/
|
*/
|
||||||
void TextHandlerTest::receiveRichPlainUrl()
|
void TextHandlerTest::receiveRichPlainUrl()
|
||||||
{
|
{
|
||||||
// This is an actual link that caused trouble which is why it's so long. Keeping
|
QFETCH(QString, input);
|
||||||
// so we can confirm consistent behaviour for complex urls.
|
QFETCH(QString, output);
|
||||||
const QString testInputStringLink1 =
|
|
||||||
u"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im <a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im\">Link already rich</a>"_s;
|
|
||||||
const QString testOutputStringLink1 =
|
|
||||||
u"<a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im\">https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im</a> <a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im\">Link already rich</a>"_s;
|
|
||||||
|
|
||||||
// Another real case. The linkification wasn't handling it when a single link
|
|
||||||
// contains what looks like and email. It was been broken into 3 but needs to
|
|
||||||
// be just single link.
|
|
||||||
const QString testInputStringLink2 = u"https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/"_s;
|
|
||||||
const QString testOutputStringLink2 =
|
|
||||||
u"<a href=\"https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/\">https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/</a>"_s;
|
|
||||||
|
|
||||||
QString testInputStringEmail = uR"(email@example.com <a href="mailto:email@example.com">Link already rich</a>)"_s;
|
|
||||||
QString testOutputStringEmail = uR"(<a href="mailto:email@example.com">email@example.com</a> <a href="mailto:email@example.com">Link already rich</a>)"_s;
|
|
||||||
|
|
||||||
QString testInputStringMxId = u"@user:kde.org <a href=\"https://matrix.to/#/@user:kde.org\">Link already rich</a>"_s;
|
|
||||||
QString testOutputStringMxId =
|
|
||||||
u"<b><a href=\"https://matrix.to/#/@user:kde.org\">@user:kde.org</a></b> <b><a href=\"https://matrix.to/#/@user:kde.org\">Link already rich</a></b>"_s;
|
|
||||||
|
|
||||||
QString testInputStringMxIdWithPrefix = u"a @user:kde.org b"_s;
|
|
||||||
QString testOutputStringMxIdWithPrefix = u"a <b><a href=\"https://matrix.to/#/@user:kde.org\">@user:kde.org</a></b> b"_s;
|
|
||||||
|
|
||||||
TextHandler testTextHandler;
|
TextHandler testTextHandler;
|
||||||
testTextHandler.setData(testInputStringLink1);
|
testTextHandler.setData(input);
|
||||||
|
|
||||||
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringLink1);
|
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), output);
|
||||||
|
|
||||||
testTextHandler.setData(testInputStringLink2);
|
|
||||||
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringLink2);
|
|
||||||
|
|
||||||
testTextHandler.setData(testInputStringEmail);
|
|
||||||
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringEmail);
|
|
||||||
|
|
||||||
testTextHandler.setData(testInputStringMxId);
|
|
||||||
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringMxId);
|
|
||||||
|
|
||||||
testTextHandler.setData(testInputStringMxIdWithPrefix);
|
|
||||||
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringMxIdWithPrefix);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextHandlerTest::receiveRichEdited_data()
|
void TextHandlerTest::receiveRichEdited_data()
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ void TimelineMessageModelTest::switchEmptyRoom()
|
|||||||
auto firstRoom = new TestUtils::TestRoom(connection, u"#firstRoom:kde.org"_s);
|
auto firstRoom = new TestUtils::TestRoom(connection, u"#firstRoom:kde.org"_s);
|
||||||
auto secondRoom = new TestUtils::TestRoom(connection, u"#secondRoom:kde.org"_s);
|
auto secondRoom = new TestUtils::TestRoom(connection, u"#secondRoom:kde.org"_s);
|
||||||
|
|
||||||
QSignalSpy spy(model, SIGNAL(roomChanged()));
|
QSignalSpy spy(model, SIGNAL(roomChanged(NeoChatRoom *, NeoChatRoom *)));
|
||||||
|
|
||||||
QCOMPARE(model->room(), nullptr);
|
QCOMPARE(model->room(), nullptr);
|
||||||
model->setRoom(firstRoom);
|
model->setRoom(firstRoom);
|
||||||
@@ -77,7 +77,7 @@ void TimelineMessageModelTest::switchSyncedRoom()
|
|||||||
auto firstRoom = new TestUtils::TestRoom(connection, u"#firstRoom:kde.org"_s, u"test-messageventmodel-sync.json"_s);
|
auto firstRoom = new TestUtils::TestRoom(connection, u"#firstRoom:kde.org"_s, u"test-messageventmodel-sync.json"_s);
|
||||||
auto secondRoom = new TestUtils::TestRoom(connection, u"#secondRoom:kde.org"_s, u"test-messageventmodel-sync.json"_s);
|
auto secondRoom = new TestUtils::TestRoom(connection, u"#secondRoom:kde.org"_s, u"test-messageventmodel-sync.json"_s);
|
||||||
|
|
||||||
QSignalSpy spy(model, SIGNAL(roomChanged()));
|
QSignalSpy spy(model, SIGNAL(roomChanged(NeoChatRoom *, NeoChatRoom *)));
|
||||||
|
|
||||||
QCOMPARE(model->room(), nullptr);
|
QCOMPARE(model->room(), nullptr);
|
||||||
model->setRoom(firstRoom);
|
model->setRoom(firstRoom);
|
||||||
@@ -208,7 +208,7 @@ void TimelineMessageModelTest::idToRow()
|
|||||||
auto room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, u"test-min-sync.json"_s);
|
auto room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, u"test-min-sync.json"_s);
|
||||||
model->setRoom(room);
|
model->setRoom(room);
|
||||||
|
|
||||||
QCOMPARE(model->eventIdToRow(u"$153456789:example.org"_s), 0);
|
QCOMPARE(model->indexforEventId(u"$153456789:example.org"_s).row(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TimelineMessageModelTest::cleanup()
|
void TimelineMessageModelTest::cleanup()
|
||||||
|
|||||||
@@ -477,6 +477,8 @@
|
|||||||
<content_attribute id="social-chat">intense</content_attribute>
|
<content_attribute id="social-chat">intense</content_attribute>
|
||||||
</content_rating>
|
</content_rating>
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="25.04.3" date="2025-07-03"/>
|
||||||
|
<release version="25.04.2" date="2025-06-05"/>
|
||||||
<release version="25.04.1" date="2025-05-08"/>
|
<release version="25.04.1" date="2025-05-08"/>
|
||||||
<release version="25.04.0" date="2025-04-17"/>
|
<release version="25.04.0" date="2025-04-17"/>
|
||||||
<release version="24.12.3" date="2025-03-06"/>
|
<release version="24.12.3" date="2025-03-06"/>
|
||||||
|
|||||||
2405
po/ar/neochat.po
2405
po/ar/neochat.po
File diff suppressed because it is too large
Load Diff
2290
po/ast/neochat.po
2290
po/ast/neochat.po
File diff suppressed because it is too large
Load Diff
2575
po/az/neochat.po
2575
po/az/neochat.po
File diff suppressed because it is too large
Load Diff
2339
po/ca/neochat.po
2339
po/ca/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2394
po/cs/neochat.po
2394
po/cs/neochat.po
File diff suppressed because it is too large
Load Diff
2482
po/da/neochat.po
2482
po/da/neochat.po
File diff suppressed because it is too large
Load Diff
2483
po/de/neochat.po
2483
po/de/neochat.po
File diff suppressed because it is too large
Load Diff
2516
po/el/neochat.po
2516
po/el/neochat.po
File diff suppressed because it is too large
Load Diff
2481
po/en_GB/neochat.po
2481
po/en_GB/neochat.po
File diff suppressed because it is too large
Load Diff
2455
po/eo/neochat.po
2455
po/eo/neochat.po
File diff suppressed because it is too large
Load Diff
2324
po/es/neochat.po
2324
po/es/neochat.po
File diff suppressed because it is too large
Load Diff
2429
po/eu/neochat.po
2429
po/eu/neochat.po
File diff suppressed because it is too large
Load Diff
2465
po/fi/neochat.po
2465
po/fi/neochat.po
File diff suppressed because it is too large
Load Diff
2494
po/fr/neochat.po
2494
po/fr/neochat.po
File diff suppressed because it is too large
Load Diff
2512
po/gl/neochat.po
2512
po/gl/neochat.po
File diff suppressed because it is too large
Load Diff
2371
po/he/neochat.po
2371
po/he/neochat.po
File diff suppressed because it is too large
Load Diff
2462
po/hi/neochat.po
2462
po/hi/neochat.po
File diff suppressed because it is too large
Load Diff
2892
po/hu/neochat.po
2892
po/hu/neochat.po
File diff suppressed because it is too large
Load Diff
2399
po/ia/neochat.po
2399
po/ia/neochat.po
File diff suppressed because it is too large
Load Diff
2563
po/id/neochat.po
2563
po/id/neochat.po
File diff suppressed because it is too large
Load Diff
2519
po/ie/neochat.po
2519
po/ie/neochat.po
File diff suppressed because it is too large
Load Diff
2395
po/it/neochat.po
2395
po/it/neochat.po
File diff suppressed because it is too large
Load Diff
2280
po/ja/neochat.po
2280
po/ja/neochat.po
File diff suppressed because it is too large
Load Diff
2366
po/ka/neochat.po
2366
po/ka/neochat.po
File diff suppressed because it is too large
Load Diff
2420
po/ko/neochat.po
2420
po/ko/neochat.po
File diff suppressed because it is too large
Load Diff
2304
po/lt/neochat.po
2304
po/lt/neochat.po
File diff suppressed because it is too large
Load Diff
2537
po/lv/neochat.po
2537
po/lv/neochat.po
File diff suppressed because it is too large
Load Diff
2384
po/nl/neochat.po
2384
po/nl/neochat.po
File diff suppressed because it is too large
Load Diff
2404
po/nn/neochat.po
2404
po/nn/neochat.po
File diff suppressed because it is too large
Load Diff
2573
po/pa/neochat.po
2573
po/pa/neochat.po
File diff suppressed because it is too large
Load Diff
2487
po/pl/neochat.po
2487
po/pl/neochat.po
File diff suppressed because it is too large
Load Diff
2553
po/pt/neochat.po
2553
po/pt/neochat.po
File diff suppressed because it is too large
Load Diff
2563
po/pt_BR/neochat.po
2563
po/pt_BR/neochat.po
File diff suppressed because it is too large
Load Diff
2489
po/ru/neochat.po
2489
po/ru/neochat.po
File diff suppressed because it is too large
Load Diff
2454
po/sa/neochat.po
2454
po/sa/neochat.po
File diff suppressed because it is too large
Load Diff
2548
po/sk/neochat.po
2548
po/sk/neochat.po
File diff suppressed because it is too large
Load Diff
2378
po/sl/neochat.po
2378
po/sl/neochat.po
File diff suppressed because it is too large
Load Diff
2456
po/sv/neochat.po
2456
po/sv/neochat.po
File diff suppressed because it is too large
Load Diff
2460
po/ta/neochat.po
2460
po/ta/neochat.po
File diff suppressed because it is too large
Load Diff
2473
po/tok/neochat.po
2473
po/tok/neochat.po
File diff suppressed because it is too large
Load Diff
2383
po/tr/neochat.po
2383
po/tr/neochat.po
File diff suppressed because it is too large
Load Diff
2410
po/uk/neochat.po
2410
po/uk/neochat.po
File diff suppressed because it is too large
Load Diff
2286
po/zh_CN/neochat.po
2286
po/zh_CN/neochat.po
File diff suppressed because it is too large
Load Diff
2357
po/zh_TW/neochat.po
2357
po/zh_TW/neochat.po
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,8 @@ endif()
|
|||||||
add_subdirectory(libneochat)
|
add_subdirectory(libneochat)
|
||||||
add_subdirectory(login)
|
add_subdirectory(login)
|
||||||
add_subdirectory(rooms)
|
add_subdirectory(rooms)
|
||||||
|
add_subdirectory(roominfo)
|
||||||
|
add_subdirectory(messagecontent)
|
||||||
add_subdirectory(timeline)
|
add_subdirectory(timeline)
|
||||||
add_subdirectory(spaces)
|
add_subdirectory(spaces)
|
||||||
add_subdirectory(chatbar)
|
add_subdirectory(chatbar)
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ add_library(neochat STATIC
|
|||||||
controller.h
|
controller.h
|
||||||
roommanager.cpp
|
roommanager.cpp
|
||||||
roommanager.h
|
roommanager.h
|
||||||
models/userfiltermodel.cpp
|
|
||||||
models/userfiltermodel.h
|
|
||||||
models/userdirectorylistmodel.cpp
|
models/userdirectorylistmodel.cpp
|
||||||
models/userdirectorylistmodel.h
|
models/userdirectorylistmodel.h
|
||||||
notificationsmanager.cpp
|
notificationsmanager.cpp
|
||||||
@@ -22,8 +20,6 @@ add_library(neochat STATIC
|
|||||||
windowcontroller.h
|
windowcontroller.h
|
||||||
models/serverlistmodel.cpp
|
models/serverlistmodel.cpp
|
||||||
models/serverlistmodel.h
|
models/serverlistmodel.h
|
||||||
logger.cpp
|
|
||||||
logger.h
|
|
||||||
models/notificationsmodel.cpp
|
models/notificationsmodel.cpp
|
||||||
models/notificationsmodel.h
|
models/notificationsmodel.h
|
||||||
proxycontroller.cpp
|
proxycontroller.cpp
|
||||||
@@ -49,6 +45,9 @@ if(ANDROID OR WIN32)
|
|||||||
set_source_files_properties(qml/ShareActionStub.qml PROPERTIES
|
set_source_files_properties(qml/ShareActionStub.qml PROPERTIES
|
||||||
QT_QML_SOURCE_TYPENAME ShareAction
|
QT_QML_SOURCE_TYPENAME ShareAction
|
||||||
)
|
)
|
||||||
|
set_source_files_properties(qml/GlobalMenuStub.qml PROPERTIES
|
||||||
|
QT_QML_SOURCE_TYPENAME GlobalMenu
|
||||||
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
|
ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
|
||||||
@@ -58,18 +57,13 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
|
|||||||
qml/AccountMenu.qml
|
qml/AccountMenu.qml
|
||||||
qml/CollapsedRoomDelegate.qml
|
qml/CollapsedRoomDelegate.qml
|
||||||
qml/RoomPage.qml
|
qml/RoomPage.qml
|
||||||
qml/ExploreRoomsPage.qml
|
|
||||||
qml/ManualRoomDialog.qml
|
qml/ManualRoomDialog.qml
|
||||||
qml/ExplorerDelegate.qml
|
qml/ExplorerDelegate.qml
|
||||||
qml/InviteUserPage.qml
|
|
||||||
qml/ImageEditorPage.qml
|
|
||||||
qml/NeochatMaximizeComponent.qml
|
qml/NeochatMaximizeComponent.qml
|
||||||
qml/TypingPane.qml
|
|
||||||
qml/QuickSwitcher.qml
|
qml/QuickSwitcher.qml
|
||||||
qml/AttachmentPane.qml
|
qml/AttachmentPane.qml
|
||||||
qml/QuickFormatBar.qml
|
qml/QuickFormatBar.qml
|
||||||
qml/UserDetailDialog.qml
|
qml/UserDetailDialog.qml
|
||||||
qml/CreateRoomDialog.qml
|
|
||||||
qml/OpenFileDialog.qml
|
qml/OpenFileDialog.qml
|
||||||
qml/KeyVerificationDialog.qml
|
qml/KeyVerificationDialog.qml
|
||||||
qml/ConfirmLogoutDialog.qml
|
qml/ConfirmLogoutDialog.qml
|
||||||
@@ -79,26 +73,14 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
|
|||||||
qml/EmojiSas.qml
|
qml/EmojiSas.qml
|
||||||
qml/VerificationCanceled.qml
|
qml/VerificationCanceled.qml
|
||||||
qml/MessageSourceSheet.qml
|
qml/MessageSourceSheet.qml
|
||||||
qml/RoomSearchPage.qml
|
|
||||||
qml/RoomPinnedMessagesPage.qml
|
|
||||||
qml/LocationChooser.qml
|
qml/LocationChooser.qml
|
||||||
qml/InvitationView.qml
|
qml/InvitationView.qml
|
||||||
qml/AvatarTabButton.qml
|
qml/AvatarTabButton.qml
|
||||||
qml/OsmLocationPlugin.qml
|
qml/OsmLocationPlugin.qml
|
||||||
qml/FullScreenMap.qml
|
qml/FullScreenMap.qml
|
||||||
qml/LocationsPage.qml
|
|
||||||
qml/LocationMapItem.qml
|
|
||||||
qml/RoomDrawer.qml
|
|
||||||
qml/RoomDrawerPage.qml
|
|
||||||
qml/DirectChatDrawerHeader.qml
|
|
||||||
qml/GroupChatDrawerHeader.qml
|
|
||||||
qml/RoomInformation.qml
|
|
||||||
qml/RoomMedia.qml
|
|
||||||
qml/ChooseRoomDialog.qml
|
qml/ChooseRoomDialog.qml
|
||||||
qml/RemoveChildDialog.qml
|
|
||||||
qml/QrCodeMaximizeComponent.qml
|
qml/QrCodeMaximizeComponent.qml
|
||||||
qml/NotificationsView.qml
|
qml/NotificationsView.qml
|
||||||
qml/SearchPage.qml
|
|
||||||
qml/ServerComboBox.qml
|
qml/ServerComboBox.qml
|
||||||
qml/UserSearchPage.qml
|
qml/UserSearchPage.qml
|
||||||
qml/ManualUserDialog.qml
|
qml/ManualUserDialog.qml
|
||||||
@@ -118,12 +100,15 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
|
|||||||
qml/AvatarNotification.qml
|
qml/AvatarNotification.qml
|
||||||
qml/ReasonDialog.qml
|
qml/ReasonDialog.qml
|
||||||
qml/NewPollDialog.qml
|
qml/NewPollDialog.qml
|
||||||
|
qml/UserMenu.qml
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
QtCore
|
QtCore
|
||||||
QtQuick
|
QtQuick
|
||||||
IMPORTS
|
IMPORTS
|
||||||
org.kde.neochat.libneochat
|
org.kde.neochat.libneochat
|
||||||
org.kde.neochat.rooms
|
org.kde.neochat.rooms
|
||||||
|
org.kde.neochat.roominfo
|
||||||
|
org.kde.neochat.messagecontent
|
||||||
org.kde.neochat.timeline
|
org.kde.neochat.timeline
|
||||||
org.kde.neochat.spaces
|
org.kde.neochat.spaces
|
||||||
org.kde.neochat.settings
|
org.kde.neochat.settings
|
||||||
@@ -139,7 +124,10 @@ if(NOT ANDROID AND NOT WIN32)
|
|||||||
qml/EditMenu.qml
|
qml/EditMenu.qml
|
||||||
)
|
)
|
||||||
else()
|
else()
|
||||||
qt_target_qml_sources(neochat QML_FILES qml/ShareActionStub.qml)
|
qt_target_qml_sources(neochat QML_FILES
|
||||||
|
qml/ShareActionStub.qml
|
||||||
|
qml/GlobalMenuStub.qml
|
||||||
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
@@ -189,7 +177,7 @@ else()
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/models)
|
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/models)
|
||||||
target_link_libraries(neochat PRIVATE Loginplugin Roomsplugin Timelineplugin Spacesplugin Chatbarplugin Settingsplugin Devtoolsplugin)
|
target_link_libraries(neochat PRIVATE Loginplugin Roomsplugin RoomInfoplugin MessageContentplugin Timelineplugin Spacesplugin Chatbarplugin Settingsplugin Devtoolsplugin)
|
||||||
target_link_libraries(neochat PUBLIC
|
target_link_libraries(neochat PUBLIC
|
||||||
LibNeoChat
|
LibNeoChat
|
||||||
Timeline
|
Timeline
|
||||||
@@ -214,6 +202,7 @@ target_link_libraries(neochat PUBLIC
|
|||||||
QuotientQt6
|
QuotientQt6
|
||||||
Login
|
Login
|
||||||
Rooms
|
Rooms
|
||||||
|
MessageContent
|
||||||
Spaces
|
Spaces
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -368,3 +357,10 @@ install(TARGETS neochat-app ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
|
|||||||
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
|
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
|
||||||
install(FILES plasma-runner-neochat.desktop DESTINATION ${KDE_INSTALL_DATAROOTDIR}/krunner/dbusplugins)
|
install(FILES plasma-runner-neochat.desktop DESTINATION ${KDE_INSTALL_DATAROOTDIR}/krunner/dbusplugins)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if (APPLE)
|
||||||
|
set_target_properties(neochat-app PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.neochat")
|
||||||
|
set_target_properties(neochat-app PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "NeoChat")
|
||||||
|
set_target_properties(neochat-app PROPERTIES MACOSX_BUNDLE_SHORT_VERSION_STRING ${RELEASE_SERVICE_VERSION})
|
||||||
|
set_target_properties(neochat-app PROPERTIES MACOSX_BUNDLE_BUNDLE_VERSION ${RELEASE_SERVICE_VERSION})
|
||||||
|
endif ()
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
#include "neochatroom.h"
|
#include "neochatroom.h"
|
||||||
#include "notificationsmanager.h"
|
#include "notificationsmanager.h"
|
||||||
#include "proxycontroller.h"
|
#include "proxycontroller.h"
|
||||||
|
#include "roommanager.h"
|
||||||
|
|
||||||
#if defined(Q_OS_WIN) || defined(Q_OS_MAC)
|
#if defined(Q_OS_WIN) || defined(Q_OS_MAC)
|
||||||
#include "trayicon.h"
|
#include "trayicon.h"
|
||||||
@@ -98,6 +99,7 @@ Controller::Controller(QObject *parent)
|
|||||||
MessageModel::setHiddenFilter(hiddenEventFilter);
|
MessageModel::setHiddenFilter(hiddenEventFilter);
|
||||||
RoomListModel::setHiddenFilter(hiddenEventFilter);
|
RoomListModel::setHiddenFilter(hiddenEventFilter);
|
||||||
RoomTreeModel::setHiddenFilter(hiddenEventFilter);
|
RoomTreeModel::setHiddenFilter(hiddenEventFilter);
|
||||||
|
NeoChatRoom::setHiddenFilter(hiddenEventFilter);
|
||||||
|
|
||||||
MediaSizeHelper::setMaxSize(NeoChatConfig::mediaMaxWidth(), NeoChatConfig::mediaMaxHeight());
|
MediaSizeHelper::setMaxSize(NeoChatConfig::mediaMaxWidth(), NeoChatConfig::mediaMaxHeight());
|
||||||
connect(NeoChatConfig::self(), &NeoChatConfig::MediaMaxWidthChanged, this, []() {
|
connect(NeoChatConfig::self(), &NeoChatConfig::MediaMaxWidthChanged, this, []() {
|
||||||
@@ -230,6 +232,7 @@ void Controller::initConnection(NeoChatConnection *connection)
|
|||||||
m_notificationsManager.handleNotifications(connection);
|
m_notificationsManager.handleNotifications(connection);
|
||||||
});
|
});
|
||||||
connect(this, &Controller::globalUrlPreviewDefaultChanged, connection, &NeoChatConnection::globalUrlPreviewEnabledChanged);
|
connect(this, &Controller::globalUrlPreviewDefaultChanged, connection, &NeoChatConnection::globalUrlPreviewEnabledChanged);
|
||||||
|
connect(connection, &NeoChatConnection::roomAboutToBeLeft, &RoomManager::instance(), &RoomManager::roomLeft);
|
||||||
Q_EMIT connectionAdded(connection);
|
Q_EMIT connectionAdded(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,4 +407,9 @@ void Controller::markImageShown(const QString &eventId)
|
|||||||
m_shownImages.append(eventId);
|
m_shownImages.append(eventId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Controller::markImageHidden(const QString &eventId)
|
||||||
|
{
|
||||||
|
m_shownImages.removeAll(eventId);
|
||||||
|
}
|
||||||
|
|
||||||
#include "moc_controller.cpp"
|
#include "moc_controller.cpp"
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ public:
|
|||||||
|
|
||||||
Q_INVOKABLE bool isImageShown(const QString &eventId);
|
Q_INVOKABLE bool isImageShown(const QString &eventId);
|
||||||
Q_INVOKABLE void markImageShown(const QString &eventId);
|
Q_INVOKABLE void markImageShown(const QString &eventId);
|
||||||
|
Q_INVOKABLE void markImageHidden(const QString &eventId);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
explicit Controller(QObject *parent = nullptr);
|
explicit Controller(QObject *parent = nullptr);
|
||||||
|
|||||||
@@ -1,223 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 1997 Matthias Kalle Dalheimer <kalle@kde.org>
|
|
||||||
// SPDX-FileCopyrightText: 2002 Holger Freyther <freyther@kde.org>
|
|
||||||
// SPDX-FileCopyrightText: 2008 Volker Krause <vkrause@kde.org>
|
|
||||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <fella@posteo.de>
|
|
||||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
|
||||||
|
|
||||||
#include "logger.h"
|
|
||||||
|
|
||||||
#include <QDateTime>
|
|
||||||
#include <QDir>
|
|
||||||
#include <QFileInfo>
|
|
||||||
#include <QLoggingCategory>
|
|
||||||
#include <QMutex>
|
|
||||||
#include <QStandardPaths>
|
|
||||||
|
|
||||||
using namespace Qt::StringLiterals;
|
|
||||||
|
|
||||||
static QLoggingCategory::CategoryFilter oldCategoryFilter = nullptr;
|
|
||||||
static QtMessageHandler oldHandler = nullptr;
|
|
||||||
static bool e2eeDebugEnabled = false;
|
|
||||||
|
|
||||||
class FileDebugStream : public QIODevice
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
FileDebugStream()
|
|
||||||
: mType(QtCriticalMsg)
|
|
||||||
{
|
|
||||||
open(WriteOnly);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isSequential() const override
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
qint64 readData(char *, qint64) override
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
qint64 readLineData(char *, qint64) override
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
qint64 writeData(const char *data, qint64 len) override
|
|
||||||
{
|
|
||||||
if (!mFileName.isEmpty()) {
|
|
||||||
QFile outputFile(mFileName);
|
|
||||||
outputFile.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Unbuffered);
|
|
||||||
outputFile.write(data, len);
|
|
||||||
outputFile.putChar('\n');
|
|
||||||
outputFile.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
return len;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setFileName(const QString &fileName)
|
|
||||||
{
|
|
||||||
mFileName = fileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setType(QtMsgType type)
|
|
||||||
{
|
|
||||||
mType = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
QString mFileName;
|
|
||||||
QtMsgType mType;
|
|
||||||
};
|
|
||||||
|
|
||||||
class DebugPrivate
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
DebugPrivate()
|
|
||||||
: origHandler(nullptr)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
~DebugPrivate()
|
|
||||||
{
|
|
||||||
qInstallMessageHandler(origHandler);
|
|
||||||
file.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
void log(QtMsgType type, const QMessageLogContext &context, const QString &message)
|
|
||||||
{
|
|
||||||
QMutexLocker locker(&mutex);
|
|
||||||
QByteArray buf;
|
|
||||||
QTextStream str(&buf);
|
|
||||||
str << QDateTime::currentDateTime().toString(Qt::ISODate) << u" ["_s;
|
|
||||||
switch (type) {
|
|
||||||
case QtDebugMsg:
|
|
||||||
str << u"DEBUG"_s;
|
|
||||||
break;
|
|
||||||
case QtInfoMsg:
|
|
||||||
str << u"INFO "_s;
|
|
||||||
break;
|
|
||||||
case QtWarningMsg:
|
|
||||||
str << u"WARN "_s;
|
|
||||||
break;
|
|
||||||
case QtFatalMsg:
|
|
||||||
str << u"FATAL"_s;
|
|
||||||
break;
|
|
||||||
case QtCriticalMsg:
|
|
||||||
str << u"CRITICAL"_s;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
str << u"] "_s << context.category << u": "_s;
|
|
||||||
if (context.file && *context.file && context.line) {
|
|
||||||
str << context.file << u":"_s << context.line << u": "_s;
|
|
||||||
}
|
|
||||||
if (context.function && *context.function) {
|
|
||||||
str << context.function << u": "_s;
|
|
||||||
}
|
|
||||||
str << message << u"\n"_s;
|
|
||||||
str.flush();
|
|
||||||
file.write(buf.constData(), buf.size());
|
|
||||||
file.flush();
|
|
||||||
|
|
||||||
if (oldHandler && (!context.category || (strcmp(context.category, "quotient.e2ee") != 0 || e2eeDebugEnabled))) {
|
|
||||||
oldHandler(type, context, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void setName(const QString &appName)
|
|
||||||
{
|
|
||||||
name = appName;
|
|
||||||
|
|
||||||
if (file.isOpen()) {
|
|
||||||
file.close();
|
|
||||||
}
|
|
||||||
const auto &filePath = u"%1%2%3"_s.arg(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), QDir::separator(), appName);
|
|
||||||
|
|
||||||
QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QDir::separator());
|
|
||||||
auto entryList = dir.entryList({appName + u".*"_s});
|
|
||||||
std::sort(entryList.begin(), entryList.end(), [](const auto &left, const auto &right) {
|
|
||||||
auto leftIndex = left.split(u"."_s).last().toInt();
|
|
||||||
auto rightIndex = right.split(u"."_s).last().toInt();
|
|
||||||
return leftIndex > rightIndex;
|
|
||||||
});
|
|
||||||
for (const auto &entry : entryList) {
|
|
||||||
bool ok = false;
|
|
||||||
const auto index = entry.split(u"."_s).last().toInt(&ok);
|
|
||||||
if (!ok) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
QFileInfo info(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QDir::separator() + entry);
|
|
||||||
if (info.exists()) {
|
|
||||||
QFile file(info.absoluteFilePath());
|
|
||||||
if (index > 50) {
|
|
||||||
file.remove();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const auto &newName = u"%1.%2"_s.arg(filePath, QString::number(index + 1));
|
|
||||||
const auto success = file.copy(newName);
|
|
||||||
if (success) {
|
|
||||||
file.remove();
|
|
||||||
} else {
|
|
||||||
qFatal("Cannot rename log file '%s' to '%s': %s",
|
|
||||||
qUtf8Printable(file.fileName()),
|
|
||||||
qUtf8Printable(newName),
|
|
||||||
qUtf8Printable(file.errorString()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QFileInfo finfo(filePath);
|
|
||||||
if (!finfo.absoluteDir().exists()) {
|
|
||||||
QDir().mkpath(finfo.absolutePath());
|
|
||||||
}
|
|
||||||
file.setFileName(filePath + u".0"_s);
|
|
||||||
file.open(QIODevice::WriteOnly | QIODevice::Unbuffered);
|
|
||||||
}
|
|
||||||
|
|
||||||
void setOrigHandler(QtMessageHandler origHandler_)
|
|
||||||
{
|
|
||||||
origHandler = origHandler_;
|
|
||||||
}
|
|
||||||
|
|
||||||
QMutex mutex;
|
|
||||||
QFile file;
|
|
||||||
QString name;
|
|
||||||
QtMessageHandler origHandler;
|
|
||||||
QByteArray loggingCategory;
|
|
||||||
};
|
|
||||||
|
|
||||||
Q_GLOBAL_STATIC(DebugPrivate, sInstance)
|
|
||||||
|
|
||||||
void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message)
|
|
||||||
{
|
|
||||||
switch (type) {
|
|
||||||
case QtDebugMsg:
|
|
||||||
case QtInfoMsg:
|
|
||||||
case QtWarningMsg:
|
|
||||||
case QtCriticalMsg:
|
|
||||||
sInstance()->log(type, context, message);
|
|
||||||
break;
|
|
||||||
case QtFatalMsg:
|
|
||||||
sInstance()->log(QtInfoMsg, context, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void filter(QLoggingCategory *category)
|
|
||||||
{
|
|
||||||
if (qstrcmp(category->categoryName(), "quotient.e2ee") == 0) {
|
|
||||||
category->setEnabled(QtDebugMsg, true);
|
|
||||||
} else if (oldCategoryFilter) {
|
|
||||||
oldCategoryFilter(category);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void initLogging()
|
|
||||||
{
|
|
||||||
e2eeDebugEnabled = QLoggingCategory("quotient.e2ee", QtInfoMsg).isEnabled(QtDebugMsg);
|
|
||||||
oldCategoryFilter = QLoggingCategory::installFilter(filter);
|
|
||||||
oldHandler = qInstallMessageHandler(messageHandler);
|
|
||||||
sInstance->setOrigHandler(oldHandler);
|
|
||||||
sInstance->setName(u"neochat.log"_s);
|
|
||||||
}
|
|
||||||
|
|
||||||
#include "logger.moc"
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initlalize logging to file and enables some additional categories, which will only be logged to the file
|
|
||||||
*/
|
|
||||||
void initLogging();
|
|
||||||
@@ -49,7 +49,6 @@
|
|||||||
#include "blurhashimageprovider.h"
|
#include "blurhashimageprovider.h"
|
||||||
#include "colorschemer.h"
|
#include "colorschemer.h"
|
||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
#include "logger.h"
|
|
||||||
#include "login.h"
|
#include "login.h"
|
||||||
#include "registration.h"
|
#include "registration.h"
|
||||||
#include "roommanager.h"
|
#include "roommanager.h"
|
||||||
@@ -138,6 +137,11 @@ int main(int argc, char *argv[])
|
|||||||
font.setHintingPreference(QFont::PreferNoHinting);
|
font.setHintingPreference(QFont::PreferNoHinting);
|
||||||
app.setFont(font);
|
app.setFont(font);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef Q_OS_MACOS
|
||||||
|
QApplication::setStyle(u"breeze"_s);
|
||||||
|
#endif
|
||||||
|
|
||||||
KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat"));
|
KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat"));
|
||||||
|
|
||||||
QGuiApplication::setOrganizationName("KDE"_L1);
|
QGuiApplication::setOrganizationName("KDE"_L1);
|
||||||
@@ -177,8 +181,6 @@ int main(int argc, char *argv[])
|
|||||||
KCrash::initialize();
|
KCrash::initialize();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
initLogging();
|
|
||||||
|
|
||||||
Connection::setEncryptionDefault(true);
|
Connection::setEncryptionDefault(true);
|
||||||
Connection::setDirectChatEncryptionDefault(true);
|
Connection::setDirectChatEncryptionDefault(true);
|
||||||
|
|
||||||
@@ -195,6 +197,9 @@ int main(int argc, char *argv[])
|
|||||||
parser.addPositionalArgument(u"urls"_s, i18n("Supports matrix: url scheme"));
|
parser.addPositionalArgument(u"urls"_s, i18n("Supports matrix: url scheme"));
|
||||||
parser.addOption(QCommandLineOption("ignore-ssl-errors"_L1, i18n("Ignore all SSL Errors, e.g., unsigned certificates.")));
|
parser.addOption(QCommandLineOption("ignore-ssl-errors"_L1, i18n("Ignore all SSL Errors, e.g., unsigned certificates.")));
|
||||||
|
|
||||||
|
QCommandLineOption replaceOption({QStringLiteral("replace")}, i18nc("command line description", "Replace an existing instance"));
|
||||||
|
parser.addOption(replaceOption);
|
||||||
|
|
||||||
QCommandLineOption testOption("test"_L1, i18n("Only used for autotests"));
|
QCommandLineOption testOption("test"_L1, i18n("Only used for autotests"));
|
||||||
testOption.setFlags(QCommandLineOption::HiddenFromHelp);
|
testOption.setFlags(QCommandLineOption::HiddenFromHelp);
|
||||||
parser.addOption(testOption);
|
parser.addOption(testOption);
|
||||||
@@ -231,7 +236,7 @@ int main(int argc, char *argv[])
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef HAVE_KDBUSADDONS
|
#ifdef HAVE_KDBUSADDONS
|
||||||
KDBusService service(KDBusService::Unique);
|
KDBusService service(KDBusService::Unique | (parser.isSet(replaceOption) ? KDBusService::Replace : KDBusService::StartupOption(0)));
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
const auto accountManager = std::make_unique<AccountManager>(parser.isSet("test"_L1));
|
const auto accountManager = std::make_unique<AccountManager>(parser.isSet("test"_L1));
|
||||||
@@ -239,13 +244,6 @@ int main(int argc, char *argv[])
|
|||||||
LoginHelper::instance().setAccountManager(accountManager.get());
|
LoginHelper::instance().setAccountManager(accountManager.get());
|
||||||
Registration::instance().setAccountManager(accountManager.get());
|
Registration::instance().setAccountManager(accountManager.get());
|
||||||
|
|
||||||
Q_IMPORT_QML_PLUGIN(org_kde_neochat_settingsPlugin)
|
|
||||||
Q_IMPORT_QML_PLUGIN(org_kde_neochat_roomsPlugin)
|
|
||||||
Q_IMPORT_QML_PLUGIN(org_kde_neochat_timelinePlugin)
|
|
||||||
Q_IMPORT_QML_PLUGIN(org_kde_neochat_devtoolsPlugin)
|
|
||||||
Q_IMPORT_QML_PLUGIN(org_kde_neochat_loginPlugin)
|
|
||||||
Q_IMPORT_QML_PLUGIN(org_kde_neochat_chatbarPlugin)
|
|
||||||
|
|
||||||
qml_register_types_org_kde_neochat();
|
qml_register_types_org_kde_neochat();
|
||||||
qmlRegisterUncreatableMetaObject(Quotient::staticMetaObject, "Quotient", 1, 0, "JoinRule", u"Access to JoinRule enum only"_s);
|
qmlRegisterUncreatableMetaObject(Quotient::staticMetaObject, "Quotient", 1, 0, "JoinRule", u"Access to JoinRule enum only"_s);
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,12 @@
|
|||||||
<label>Use a compact room list layout</label>
|
<label>Use a compact room list layout</label>
|
||||||
<default>false</default>
|
<default>false</default>
|
||||||
</entry>
|
</entry>
|
||||||
|
<entry name="MarkReadCondition" type="Enum">
|
||||||
|
<label>The sort order for the rooms in the list.</label>
|
||||||
|
<choices name="::TimelineMarkReadCondition::Condition">
|
||||||
|
</choices>
|
||||||
|
<default>2</default>
|
||||||
|
</entry>
|
||||||
<entry name="ShowStateEvent" type="bool">
|
<entry name="ShowStateEvent" type="bool">
|
||||||
<label>Show state events in the timeline</label>
|
<label>Show state events in the timeline</label>
|
||||||
<default>true</default>
|
<default>true</default>
|
||||||
@@ -205,6 +211,10 @@
|
|||||||
<label>Enable add phone numbers as 3PIDs</label>
|
<label>Enable add phone numbers as 3PIDs</label>
|
||||||
<default>false</default>
|
<default>false</default>
|
||||||
</entry>
|
</entry>
|
||||||
|
<entry name="Calls" type="bool">
|
||||||
|
<label>Enable audio and video calling</label>
|
||||||
|
<default>false</default>
|
||||||
|
</entry>
|
||||||
</group>
|
</group>
|
||||||
<group name="Security">
|
<group name="Security">
|
||||||
<entry name="RejectUnknownInvites" type="bool">
|
<entry name="RejectUnknownInvites" type="bool">
|
||||||
|
|||||||
@@ -276,7 +276,7 @@ void NotificationsManager::postInviteNotification(NeoChatRoom *rawRoom)
|
|||||||
if (inAnyOfOurRooms) {
|
if (inAnyOfOurRooms) {
|
||||||
doPostInviteNotification(room);
|
doPostInviteNotification(room);
|
||||||
} else {
|
} else {
|
||||||
room->leaveRoom();
|
room->forget();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -330,14 +330,14 @@ void NotificationsManager::doPostInviteNotification(QPointer<NeoChatRoom> room)
|
|||||||
if (!room) {
|
if (!room) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
RoomManager::instance().leaveRoom(room);
|
room->forget();
|
||||||
notification->close();
|
notification->close();
|
||||||
});
|
});
|
||||||
connect(rejectAndIgnoreAction, &KNotificationAction::activated, this, [room, notification]() {
|
connect(rejectAndIgnoreAction, &KNotificationAction::activated, this, [room, notification]() {
|
||||||
if (!room) {
|
if (!room) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
RoomManager::instance().leaveRoom(room);
|
room->forget();
|
||||||
room->connection()->addToIgnoredUsers(room->invitingUserId());
|
room->connection()->addToIgnoredUsers(room->invitingUserId());
|
||||||
notification->close();
|
notification->close();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,14 +36,18 @@ KirigamiComponents.ConvergentContextMenu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Kirigami.Action {
|
||||||
|
text: i18nc("@action:inmenu", "Switch Account")
|
||||||
|
icon.name: "system-switch-user"
|
||||||
|
shortcut: "Ctrl+U"
|
||||||
|
onTriggered: accountSwitchDialog.createObject(QQC2.Overlay.overlay, {
|
||||||
|
connection: root.connection
|
||||||
|
}).open();
|
||||||
|
}
|
||||||
QQC2.Action {
|
QQC2.Action {
|
||||||
text: i18n("Edit This Account")
|
text: i18n("Edit This Account")
|
||||||
icon.name: "document-edit"
|
icon.name: "document-edit"
|
||||||
onTriggered: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat.settings', 'AccountEditorPage'), {
|
onTriggered: NeoChatSettingsView.openWithInitialProperties("accounts", {initialAccount: root.connection});
|
||||||
connection: root.connection
|
|
||||||
}, {
|
|
||||||
title: i18n("Account editor")
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QQC2.Action {
|
QQC2.Action {
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ Delegates.RoundedItemDelegate {
|
|||||||
|
|
||||||
TapHandler {
|
TapHandler {
|
||||||
acceptedDevices: PointerDevice.TouchScreen
|
acceptedDevices: PointerDevice.TouchScreen
|
||||||
|
onTapped: root.selected()
|
||||||
onLongPressed: root.contextMenuRequested()
|
onLongPressed: root.contextMenuRequested()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ Kirigami.PromptDialog {
|
|||||||
text: i18nc("@action:button", "Leave Room")
|
text: i18nc("@action:button", "Leave Room")
|
||||||
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
|
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
|
||||||
icon.name: "arrow-left-symbolic"
|
icon.name: "arrow-left-symbolic"
|
||||||
onClicked: RoomManager.leaveRoom(root.room)
|
onClicked: root.room.forget();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import QtPositioning
|
|||||||
|
|
||||||
import org.kde.kirigami as Kirigami
|
import org.kde.kirigami as Kirigami
|
||||||
|
|
||||||
|
import org.kde.neochat.libneochat
|
||||||
|
|
||||||
ApplicationWindow {
|
ApplicationWindow {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import org.kde.neochat.settings
|
|||||||
Labs.MenuBar {
|
Labs.MenuBar {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property NeoChatConnection connection
|
required property NeoChatConnection connection
|
||||||
|
|
||||||
Labs.Menu {
|
Labs.Menu {
|
||||||
title: i18nc("menu", "NeoChat")
|
title: i18nc("menu", "NeoChat")
|
||||||
@@ -38,25 +38,31 @@ Labs.MenuBar {
|
|||||||
title: i18nc("menu", "File")
|
title: i18nc("menu", "File")
|
||||||
|
|
||||||
Labs.MenuItem {
|
Labs.MenuItem {
|
||||||
text: i18nc("menu", "Find your friends")
|
icon.name: "list-add-user"
|
||||||
|
text: i18nc("@action:inmenu", "Find your Friends")
|
||||||
enabled: pageStack.layers.currentItem.title !== i18n("Find your friends") && AccountRegistry.accountCount > 0
|
enabled: pageStack.layers.currentItem.title !== i18n("Find your friends") && AccountRegistry.accountCount > 0
|
||||||
onTriggered: pushReplaceLayer(Qt.createComponent('org.kde.neochat', 'UserSearchPage'), {
|
onTriggered: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UserSearchPage'), {
|
||||||
connection: root.connection
|
connection: root.connection
|
||||||
}, {
|
}, {
|
||||||
title: i18nc("@title", "Find your friends")
|
title: i18nc("@title", "Find your friends")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Labs.MenuItem {
|
Labs.MenuItem {
|
||||||
text: i18nc("menu", "New Group…")
|
icon.name: "system-users-symbolic"
|
||||||
|
text: i18nc("@action:inmenu", "Create a Room…")
|
||||||
enabled: pageStack.layers.currentItem.title !== i18n("Find your friends") && AccountRegistry.accountCount > 0
|
enabled: pageStack.layers.currentItem.title !== i18n("Find your friends") && AccountRegistry.accountCount > 0
|
||||||
shortcut: StandardKey.New
|
shortcut: StandardKey.New
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
const dialog = createRoomDialog.createObject(root.overlay);
|
pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'CreateRoomDialog'), {
|
||||||
dialog.open();
|
connection: root.connection
|
||||||
|
}, {
|
||||||
|
title: i18nc("@title", "Create a Room")
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Labs.MenuItem {
|
Labs.MenuItem {
|
||||||
text: i18nc("menu", "Browse Chats…")
|
icon.name: "compass-symbolic"
|
||||||
|
text: i18nc("@action:inmenu", "Explore Rooms")
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
let dialog = pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
|
let dialog = pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
|
||||||
connection: root.connection
|
connection: root.connection
|
||||||
@@ -77,7 +83,8 @@ Labs.MenuBar {
|
|||||||
title: i18nc("menu", "View")
|
title: i18nc("menu", "View")
|
||||||
|
|
||||||
Labs.MenuItem {
|
Labs.MenuItem {
|
||||||
text: i18nc("menu item that opens a UI element called the 'Quick Switcher', which offers a fast keyboard-based interface for switching in between chats.", "Open Quick Switcher")
|
icon.name: "search-symbolic"
|
||||||
|
text: i18nc("@action:inmenu opens a UI element called the 'Quick Switcher', which offers a fast keyboard-based interface for switching in between chats.", "Search Rooms")
|
||||||
onTriggered: quickSwitcher.open()
|
onTriggered: quickSwitcher.open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,6 +92,7 @@ Labs.MenuBar {
|
|||||||
title: i18nc("menu", "Window")
|
title: i18nc("menu", "Window")
|
||||||
|
|
||||||
Labs.MenuItem {
|
Labs.MenuItem {
|
||||||
|
icon.name: "view-fullscreen-symbolic"
|
||||||
text: root.visibility === Window.FullScreen ? i18nc("menu", "Exit Full Screen") : i18nc("menu", "Enter Full Screen")
|
text: root.visibility === Window.FullScreen ? i18nc("menu", "Exit Full Screen") : i18nc("menu", "Enter Full Screen")
|
||||||
onTriggered: root.visibility === Window.FullScreen ? root.showNormal() : root.showFullScreen()
|
onTriggered: root.visibility === Window.FullScreen ? root.showNormal() : root.showFullScreen()
|
||||||
}
|
}
|
||||||
@@ -93,14 +101,12 @@ Labs.MenuBar {
|
|||||||
title: i18nc("menu", "Help")
|
title: i18nc("menu", "Help")
|
||||||
|
|
||||||
Labs.MenuItem {
|
Labs.MenuItem {
|
||||||
text: i18nc("menu", "About Matrix")
|
icon.name: "help-about-symbolic"
|
||||||
onTriggered: UrlHelper.openUrl("https://matrix.org/docs/chat_basics/matrix-for-im/")
|
|
||||||
}
|
|
||||||
Labs.MenuItem {
|
|
||||||
text: i18nc("menu", "About NeoChat")
|
text: i18nc("menu", "About NeoChat")
|
||||||
onTriggered: pageStack.pushDialogLayer(Qt.createComponent("org.kde.kirigamiaddons.formcard", "AboutPage"))
|
onTriggered: pageStack.pushDialogLayer(Qt.createComponent("org.kde.kirigamiaddons.formcard", "AboutPage"))
|
||||||
}
|
}
|
||||||
Labs.MenuItem {
|
Labs.MenuItem {
|
||||||
|
icon.name: "kde-symbolic"
|
||||||
text: i18nc("menu", "About KDE")
|
text: i18nc("menu", "About KDE")
|
||||||
onTriggered: pageStack.pushDialogLayer(Qt.createComponent("org.kde.kirigamiaddons.formcard", "AboutKDEPage"))
|
onTriggered: pageStack.pushDialogLayer(Qt.createComponent("org.kde.kirigamiaddons.formcard", "AboutKDEPage"))
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/app/qml/GlobalMenuStub.qml
Normal file
10
src/app/qml/GlobalMenuStub.qml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
import org.kde.neochat
|
||||||
|
|
||||||
|
Item {
|
||||||
|
required property NeoChatConnection connection
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ ColumnLayout {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property NeoChatRoom currentRoom
|
required property NeoChatRoom currentRoom
|
||||||
readonly property var invitingMember: currentRoom.member(currentRoom.invitingUserId)
|
readonly property var invitingMember: currentRoom.qmlSafeMember(currentRoom.invitingUserId)
|
||||||
readonly property string inviteTimestamp: root.currentRoom.inviteTimestamp.toLocaleString(Qt.locale(), Locale.ShortFormat)
|
readonly property string inviteTimestamp: root.currentRoom.inviteTimestamp.toLocaleString(Qt.locale(), Locale.ShortFormat)
|
||||||
|
|
||||||
spacing: Kirigami.Units.smallSpacing
|
spacing: Kirigami.Units.smallSpacing
|
||||||
@@ -33,7 +33,7 @@ ColumnLayout {
|
|||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
name: root.invitingMember.displayName
|
name: root.invitingMember.displayName
|
||||||
source: root.invitingMember.avatarUrl
|
source: NeoChatConfig.hideImages ? undefined : root.invitingMember.avatarUrl
|
||||||
color: root.invitingMember.color
|
color: root.invitingMember.color
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ ColumnLayout {
|
|||||||
icon.name: "dialog-cancel-symbolic"
|
icon.name: "dialog-cancel-symbolic"
|
||||||
text: i18nc("@action:button Reject this invite", "Reject Invite")
|
text: i18nc("@action:button Reject this invite", "Reject Invite")
|
||||||
|
|
||||||
onClicked: RoomManager.leaveRoom(root.currentRoom)
|
onClicked: root.currentRoom.forget()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ ColumnLayout {
|
|||||||
text: i18nc("@action:button Block the user", "Block %1", root.invitingMember.displayName)
|
text: i18nc("@action:button Block the user", "Block %1", root.invitingMember.displayName)
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
RoomManager.leaveRoom(root.currentRoom);
|
root.currentRoom.forget()
|
||||||
root.currentRoom.connection.addToIgnoredUsers(root.currentRoom.invitingUserId);
|
root.currentRoom.connection.addToIgnoredUsers(root.currentRoom.invitingUserId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ Kirigami.ApplicationWindow {
|
|||||||
showExisting: true
|
showExisting: true
|
||||||
onConnectionChosen: root.load()
|
onConnectionChosen: root.load()
|
||||||
}
|
}
|
||||||
|
columnView.columnResizeMode: pageStack.wideMode ? Kirigami.ColumnView.DynamicColumns : Kirigami.ColumnView.SingleColumn
|
||||||
globalToolBar.canContainHandles: true
|
globalToolBar.canContainHandles: true
|
||||||
globalToolBar {
|
globalToolBar {
|
||||||
style: Kirigami.ApplicationHeaderStyle.ToolBar
|
style: Kirigami.ApplicationHeaderStyle.ToolBar
|
||||||
@@ -80,9 +81,8 @@ Kirigami.ApplicationWindow {
|
|||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
active: Kirigami.Settings.hasPlatformMenuBar && !Kirigami.Settings.isMobile
|
active: Kirigami.Settings.hasPlatformMenuBar && !Kirigami.Settings.isMobile
|
||||||
sourceComponent: Qt.createComponent("org.kde.neochat", "GlobalMenu")
|
sourceComponent: GlobalMenu {
|
||||||
onActiveChanged: if (active) {
|
connection: root.connection
|
||||||
item.connection = root.connection;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,9 +149,13 @@ Kirigami.ApplicationWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openRoomDrawer() {
|
function openRoomDrawer() {
|
||||||
pageStack.push(Qt.createComponent('org.kde.neochat', 'RoomDrawerPage'), {
|
const page = pageStack.push(Qt.createComponent('org.kde.neochat', 'RoomDrawerPage'), {
|
||||||
connection: root.connection
|
connection: root.connection,
|
||||||
|
room: RoomManager.currentRoom,
|
||||||
|
userListModel: RoomManager.userListModel,
|
||||||
|
mediaMessageFilterModel: RoomManager.mediaMessageFilterModel
|
||||||
});
|
});
|
||||||
|
page.resolveResource.connect((idOrUri, action) => RoomManager.resolveResource(idOrUri, action))
|
||||||
}
|
}
|
||||||
|
|
||||||
contextDrawer: RoomDrawer {
|
contextDrawer: RoomDrawer {
|
||||||
@@ -161,7 +165,18 @@ Kirigami.ApplicationWindow {
|
|||||||
// It is used to ensure that user choice is remembered when changing pages and expanding and contracting the window width
|
// It is used to ensure that user choice is remembered when changing pages and expanding and contracting the window width
|
||||||
property bool drawerUserState: NeoChatConfig.autoRoomInfoDrawer
|
property bool drawerUserState: NeoChatConfig.autoRoomInfoDrawer
|
||||||
|
|
||||||
|
room: RoomManager.currentRoom
|
||||||
connection: root.connection
|
connection: root.connection
|
||||||
|
userListModel: RoomManager.userListModel
|
||||||
|
mediaMessageFilterModel: RoomManager.mediaMessageFilterModel
|
||||||
|
|
||||||
|
onResolveResource: (idOrUri, action) => RoomManager.resolveResource(idOrUri, action)
|
||||||
|
|
||||||
|
roomDrawerWidth: NeoChatConfig.roomDrawerWidth
|
||||||
|
onRoomDrawerWidthChanged: {
|
||||||
|
NeoChatConfig.roomDrawerWidth = actualWidth;
|
||||||
|
NeoChatConfig.save();
|
||||||
|
}
|
||||||
|
|
||||||
handleClosedIcon.source: "documentinfo-symbolic"
|
handleClosedIcon.source: "documentinfo-symbolic"
|
||||||
handleClosedToolTip: i18nc("@action:button", "Show Room Information")
|
handleClosedToolTip: i18nc("@action:button", "Show Room Information")
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ Components.AlbumMaximizeComponent {
|
|||||||
id: saveAsDialog
|
id: saveAsDialog
|
||||||
Dialogs.FileDialog {
|
Dialogs.FileDialog {
|
||||||
fileMode: Dialogs.FileDialog.SaveFile
|
fileMode: Dialogs.FileDialog.SaveFile
|
||||||
currentFolder: root.saveFolder
|
currentFolder: NeoChatConfig.lastSaveDirectory.length > 0 ? NeoChatConfig.lastSaveDirectory : Core.StandardPaths.writableLocation(Core.StandardPaths.DownloadLocation)
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
NeoChatConfig.lastSaveDirectory = currentFolder;
|
NeoChatConfig.lastSaveDirectory = currentFolder;
|
||||||
NeoChatConfig.save();
|
NeoChatConfig.save();
|
||||||
|
|||||||
@@ -28,6 +28,14 @@ Kirigami.Page {
|
|||||||
placeholderText: root.placeholder
|
placeholderText: root.placeholder
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
wrapMode: TextEdit.Wrap
|
wrapMode: TextEdit.Wrap
|
||||||
|
focus: true
|
||||||
|
|
||||||
|
Keys.onReturnPressed: event => {
|
||||||
|
if (event.modifiers & Qt.ControlModifier) {
|
||||||
|
root.accepted(reason.text);
|
||||||
|
root.closeDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
color: Kirigami.Theme.backgroundColor
|
color: Kirigami.Theme.backgroundColor
|
||||||
@@ -50,6 +58,7 @@ Kirigami.Page {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
QQC2.Button {
|
QQC2.Button {
|
||||||
|
icon.name: "dialog-cancel-symbolic"
|
||||||
text: i18nc("@action", "Cancel")
|
text: i18nc("@action", "Cancel")
|
||||||
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.RejectRole
|
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.RejectRole
|
||||||
onClicked: root.closeDialog()
|
onClicked: root.closeDialog()
|
||||||
|
|||||||
@@ -11,15 +11,14 @@ import org.kde.kirigami as Kirigami
|
|||||||
import org.kde.kitemmodels
|
import org.kde.kitemmodels
|
||||||
|
|
||||||
import org.kde.neochat
|
import org.kde.neochat
|
||||||
import org.kde.neochat.chatbar
|
|
||||||
|
|
||||||
Kirigami.Page {
|
Kirigami.Page {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
/// Not readonly because of the separate window view.
|
/**
|
||||||
property NeoChatRoom currentRoom: RoomManager.currentRoom
|
* @brief The NeoChatRoom the delegate is being displayed in.
|
||||||
|
*/
|
||||||
required property NeoChatConnection connection
|
readonly property NeoChatRoom currentRoom: RoomManager.currentRoom
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The TimelineModel to use.
|
* @brief The TimelineModel to use.
|
||||||
@@ -59,11 +58,6 @@ Kirigami.Page {
|
|||||||
*/
|
*/
|
||||||
property MediaMessageFilterModel mediaMessageFilterModel: RoomManager.mediaMessageFilterModel
|
property MediaMessageFilterModel mediaMessageFilterModel: RoomManager.mediaMessageFilterModel
|
||||||
|
|
||||||
property bool loading: !root.currentRoom || (root.currentRoom.timelineSize === 0 && !root.currentRoom.allHistoryLoaded)
|
|
||||||
|
|
||||||
/// Disable cancel shortcut. Used by the separate window since it provides its own cancel implementation.
|
|
||||||
property bool disableCancelShortcut: false
|
|
||||||
|
|
||||||
title: root.currentRoom ? root.currentRoom.displayName : ""
|
title: root.currentRoom ? root.currentRoom.displayName : ""
|
||||||
focus: true
|
focus: true
|
||||||
padding: 0
|
padding: 0
|
||||||
@@ -86,9 +80,9 @@ Kirigami.Page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: root.connection
|
target: root.currentRoom.connection
|
||||||
function onIsOnlineChanged() {
|
function onIsOnlineChanged() {
|
||||||
if (!root.connection.isOnline) {
|
if (!root.currentRoom.connection.isOnline) {
|
||||||
banner.text = i18n("NeoChat is offline. Please check your network connection.");
|
banner.text = i18n("NeoChat is offline. Please check your network connection.");
|
||||||
banner.visible = true;
|
banner.visible = true;
|
||||||
banner.type = Kirigami.MessageType.Error;
|
banner.type = Kirigami.MessageType.Error;
|
||||||
@@ -109,18 +103,15 @@ Kirigami.Page {
|
|||||||
Loader {
|
Loader {
|
||||||
id: timelineViewLoader
|
id: timelineViewLoader
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
active: root.currentRoom && !root.currentRoom.isInvite && !root.loading && !root.currentRoom.isSpace
|
active: root.currentRoom && !root.currentRoom.isInvite && !root.currentRoom.isSpace
|
||||||
|
// We need the loader to be active but invisible while the room is loading messages so signals in TimelineView work.
|
||||||
|
visible: !root.loading
|
||||||
sourceComponent: TimelineView {
|
sourceComponent: TimelineView {
|
||||||
id: timelineView
|
id: timelineView
|
||||||
currentRoom: root.currentRoom
|
|
||||||
page: root
|
|
||||||
timelineModel: root.timelineModel
|
|
||||||
messageFilterModel: root.messageFilterModel
|
messageFilterModel: root.messageFilterModel
|
||||||
onFocusChatBar: {
|
compactLayout: NeoChatConfig.compactLayout
|
||||||
if (chatBarLoader.item) {
|
fileDropEnabled: !Controller.isFlatpak
|
||||||
chatBarLoader.item.forceActiveFocus();
|
markReadCondition: NeoChatConfig.markReadCondition
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +128,9 @@ Kirigami.Page {
|
|||||||
id: spaceLoader
|
id: spaceLoader
|
||||||
active: root.currentRoom && root.currentRoom.isSpace
|
active: root.currentRoom && root.currentRoom.isSpace
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
sourceComponent: SpaceHomePage {}
|
sourceComponent: SpaceHomePage {
|
||||||
|
room: root.currentRoom
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
@@ -150,14 +143,6 @@ Kirigami.Page {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
|
||||||
active: root.loading && !invitationLoader.active && RoomManager.currentRoom && !spaceLoader.active
|
|
||||||
anchors.centerIn: parent
|
|
||||||
sourceComponent: Kirigami.LoadingPlaceholder {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||||
Kirigami.Theme.inherit: false
|
Kirigami.Theme.inherit: false
|
||||||
@@ -172,12 +157,7 @@ Kirigami.Page {
|
|||||||
id: chatBar
|
id: chatBar
|
||||||
width: parent.width
|
width: parent.width
|
||||||
currentRoom: root.currentRoom
|
currentRoom: root.currentRoom
|
||||||
connection: root.connection
|
connection: root.currentRoom.connection
|
||||||
onMessageSent: {
|
|
||||||
if (!timelineViewLoader.item.atYEnd) {
|
|
||||||
timelineViewLoader.item.goToLastMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,21 +174,8 @@ Kirigami.Page {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Shortcut {
|
|
||||||
sequence: StandardKey.Cancel
|
|
||||||
onActivated: {
|
|
||||||
if (!timelineViewLoader.item.atYEnd || !root.currentRoom.partiallyReadStats.empty()) {
|
|
||||||
timelineViewLoader.item.goToLastMessage();
|
|
||||||
root.currentRoom.markAllMessagesAsRead();
|
|
||||||
} else {
|
|
||||||
applicationWindow().pageStack.get(0).forceActiveFocus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
enabled: !root.disableCancelShortcut
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: root.connection
|
target: root.currentRoom.connection
|
||||||
function onJoinedRoom(room, invited) {
|
function onJoinedRoom(room, invited) {
|
||||||
if (root.currentRoom.id === invited.id) {
|
if (root.currentRoom.id === invited.id) {
|
||||||
RoomManager.resolveResource(room.id);
|
RoomManager.resolveResource(room.id);
|
||||||
@@ -264,6 +231,7 @@ Kirigami.Page {
|
|||||||
plainText: plainText,
|
plainText: plainText,
|
||||||
mimeType: mimeType,
|
mimeType: mimeType,
|
||||||
progressInfo: progressInfo,
|
progressInfo: progressInfo,
|
||||||
|
messageComponentType: messageComponentType,
|
||||||
});
|
});
|
||||||
contextMenu.popup();
|
contextMenu.popup();
|
||||||
}
|
}
|
||||||
@@ -293,7 +261,7 @@ Kirigami.Page {
|
|||||||
id: messageDelegateContextMenu
|
id: messageDelegateContextMenu
|
||||||
MessageDelegateContextMenu {
|
MessageDelegateContextMenu {
|
||||||
room: root.currentRoom
|
room: root.currentRoom
|
||||||
connection: root.connection
|
connection: root.currentRoom.connection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,7 +269,7 @@ Kirigami.Page {
|
|||||||
id: fileDelegateContextMenu
|
id: fileDelegateContextMenu
|
||||||
FileDelegateContextMenu {
|
FileDelegateContextMenu {
|
||||||
room: root.currentRoom
|
room: root.currentRoom
|
||||||
connection: root.connection
|
connection: root.currentRoom.connection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ Kirigami.Dialog {
|
|||||||
text: root.user.id
|
text: root.user.id
|
||||||
elide: Qt.ElideRight
|
elide: Qt.ElideRight
|
||||||
elideWidth: root.availableWidth - avatar.width - qrButton.width - detailRow.spacing * 2 - detailRow.Layout.leftMargin - detailRow.Layout.rightMargin
|
elideWidth: root.availableWidth - avatar.width - qrButton.width - detailRow.spacing * 2 - detailRow.Layout.leftMargin - detailRow.Layout.rightMargin
|
||||||
onElideWidthChanged: console.warn(root.availableWidth, avatar.width, qrButton.width, elideWidth, elidedText)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
74
src/app/qml/UserMenu.qml
Normal file
74
src/app/qml/UserMenu.qml
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls as QQC2
|
||||||
|
import QtQuick.Layouts
|
||||||
|
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
import org.kde.kirigamiaddons.components as KirigamiComponents
|
||||||
|
|
||||||
|
import org.kde.neochat
|
||||||
|
import org.kde.neochat.settings
|
||||||
|
import org.kde.neochat.devtools
|
||||||
|
|
||||||
|
KirigamiComponents.ConvergentContextMenu {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property Kirigami.ApplicationWindow window
|
||||||
|
required property var author
|
||||||
|
|
||||||
|
headerContentItem: RowLayout {
|
||||||
|
id: detailRow
|
||||||
|
|
||||||
|
spacing: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
|
KirigamiComponents.Avatar {
|
||||||
|
id: avatar
|
||||||
|
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
|
||||||
|
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
|
||||||
|
|
||||||
|
name: root.author.displayName
|
||||||
|
source: root.author.avatarUrl
|
||||||
|
color: root.author.color
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Kirigami.Heading {
|
||||||
|
level: 1
|
||||||
|
Layout.fillWidth: true
|
||||||
|
font.bold: true
|
||||||
|
|
||||||
|
elide: Text.ElideRight
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
text: root.author.displayName
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
id: idLabel
|
||||||
|
textFormat: TextEdit.PlainText
|
||||||
|
text: root.author.id
|
||||||
|
elide: Qt.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Action {
|
||||||
|
text: i18nc("@action:button", "Open Profile")
|
||||||
|
icon.name: "im-user-symbolic"
|
||||||
|
onTriggered: RoomManager.resolveResource(root.author.uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Action {
|
||||||
|
text: i18nc("@action:button", "Mention")
|
||||||
|
icon.name: "username-copy-symbolic"
|
||||||
|
onTriggered: {
|
||||||
|
RoomManager.currentRoom.mainCache.mentionAdded(root.author.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,9 +59,9 @@ RoomManager::RoomManager(QObject *parent)
|
|||||||
m_directChatsConfig = m_config->group(u"DirectChatsActive"_s);
|
m_directChatsConfig = m_config->group(u"DirectChatsActive"_s);
|
||||||
|
|
||||||
connect(this, &RoomManager::currentRoomChanged, this, [this]() {
|
connect(this, &RoomManager::currentRoomChanged, this, [this]() {
|
||||||
|
m_userListModel->setRoom(m_currentRoom);
|
||||||
m_timelineModel->setRoom(m_currentRoom);
|
m_timelineModel->setRoom(m_currentRoom);
|
||||||
m_sortFilterRoomTreeModel->setCurrentRoom(m_currentRoom);
|
m_sortFilterRoomTreeModel->setCurrentRoom(m_currentRoom);
|
||||||
m_userListModel->setRoom(m_currentRoom);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
connect(&Controller::instance(), &Controller::activeConnectionChanged, this, [this](NeoChatConnection *connection) {
|
connect(&Controller::instance(), &Controller::activeConnectionChanged, this, [this](NeoChatConnection *connection) {
|
||||||
@@ -96,6 +96,7 @@ RoomManager::RoomManager(QObject *parent)
|
|||||||
m_messageFilterModel->invalidate();
|
m_messageFilterModel->invalidate();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
connect(m_timelineModel->timelineMessageModel(), &MessageModel::modelResetComplete, this, &RoomManager::activateUserModel);
|
||||||
MessageFilterModel::setShowAllEvents(NeoChatConfig::self()->showAllEvents());
|
MessageFilterModel::setShowAllEvents(NeoChatConfig::self()->showAllEvents());
|
||||||
connect(NeoChatConfig::self(), &NeoChatConfig::ShowAllEventsChanged, this, [this] {
|
connect(NeoChatConfig::self(), &NeoChatConfig::ShowAllEventsChanged, this, [this] {
|
||||||
MessageFilterModel::setShowAllEvents(NeoChatConfig::self()->showAllEvents());
|
MessageFilterModel::setShowAllEvents(NeoChatConfig::self()->showAllEvents());
|
||||||
@@ -303,7 +304,12 @@ void RoomManager::loadInitialRoom()
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (m_isMobile) {
|
if (m_isMobile) {
|
||||||
setCurrentSpace(m_lastSpaceConfig.readEntry(m_connection->userId(), QString()), false);
|
QString lastSpace = m_lastSpaceConfig.readEntry(m_connection->userId(), QString());
|
||||||
|
// We can't have empty keys in KConfig, so we stored it as "Home"
|
||||||
|
if (lastSpace == u"Home"_s) {
|
||||||
|
lastSpace.clear();
|
||||||
|
}
|
||||||
|
setCurrentSpace(lastSpace, false);
|
||||||
// We don't want to open a room on startup on mobile
|
// We don't want to open a room on startup on mobile
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -325,14 +331,7 @@ void RoomManager::openRoomForActiveConnection()
|
|||||||
setCurrentSpace({}, false);
|
setCurrentSpace({}, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setCurrentSpace(m_lastSpaceConfig.readEntry(m_connection->userId(), QString()), false);
|
setCurrentSpace(m_lastSpaceConfig.readEntry(m_connection->userId(), QString()), true);
|
||||||
const auto &lastRoom = m_lastRoomConfig.readEntry(m_connection->userId(), QString());
|
|
||||||
if (lastRoom.isEmpty() || !m_connection->room(lastRoom)) {
|
|
||||||
setCurrentRoom({});
|
|
||||||
} else {
|
|
||||||
m_currentRoom = nullptr;
|
|
||||||
resolveResource(lastRoom);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UriResolveResult RoomManager::visitUser(User *user, const QString &action)
|
UriResolveResult RoomManager::visitUser(User *user, const QString &action)
|
||||||
@@ -448,29 +447,27 @@ void RoomManager::knockRoom(NeoChatConnection *account, const QString &roomAlias
|
|||||||
Qt::SingleShotConnection);
|
Qt::SingleShotConnection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RoomManager::roomLeft(const QString &id)
|
||||||
|
{
|
||||||
|
if (id.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_currentRoom && m_currentRoom->id() == id) {
|
||||||
|
setCurrentRoom({});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_currentSpaceId == id) {
|
||||||
|
setCurrentSpace({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool RoomManager::visitNonMatrix(const QUrl &url)
|
bool RoomManager::visitNonMatrix(const QUrl &url)
|
||||||
{
|
{
|
||||||
UrlHelper().openUrl(url);
|
UrlHelper().openUrl(url);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void RoomManager::leaveRoom(NeoChatRoom *room)
|
|
||||||
{
|
|
||||||
if (!room) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_currentRoom && m_currentRoom->id() == room->id()) {
|
|
||||||
setCurrentRoom({});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_currentSpaceId == room->id()) {
|
|
||||||
setCurrentSpace({});
|
|
||||||
}
|
|
||||||
|
|
||||||
room->forget();
|
|
||||||
}
|
|
||||||
|
|
||||||
ChatDocumentHandler *RoomManager::chatDocumentHandler() const
|
ChatDocumentHandler *RoomManager::chatDocumentHandler() const
|
||||||
{
|
{
|
||||||
return m_chatDocumentHandler;
|
return m_chatDocumentHandler;
|
||||||
@@ -523,18 +520,32 @@ void RoomManager::setCurrentSpace(const QString &spaceId, bool setRoom)
|
|||||||
|
|
||||||
Q_EMIT currentSpaceChanged();
|
Q_EMIT currentSpaceChanged();
|
||||||
if (m_connection) {
|
if (m_connection) {
|
||||||
m_lastSpaceConfig.writeEntry(m_connection->userId(), spaceId);
|
if (spaceId.isEmpty()) {
|
||||||
|
m_lastSpaceConfig.writeEntry(m_connection->userId(), u"Home"_s);
|
||||||
|
} else {
|
||||||
|
m_lastSpaceConfig.writeEntry(m_connection->userId(), spaceId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!setRoom) {
|
if (!setRoom) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We intentionally don't want to open the last room on mobile
|
||||||
if (!m_isMobile) {
|
if (!m_isMobile) {
|
||||||
if (spaceId.length() > 3) {
|
QString configSpaceId = spaceId;
|
||||||
resolveResource(spaceId, "no_join"_L1);
|
// We can't have empty keys in KConfig, so it's stored as "Home"
|
||||||
|
if (spaceId.isEmpty()) {
|
||||||
|
configSpaceId = u"Home"_s;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &lastRoom = m_lastRoomConfig.readEntry(configSpaceId, QString());
|
||||||
|
if (lastRoom.isEmpty()) {
|
||||||
|
if (spaceId != u"DM"_s && spaceId != u"Home"_s) {
|
||||||
|
resolveResource(spaceId, "no_join"_L1);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
visitRoom({}, {});
|
resolveResource(lastRoom, "no_join"_L1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -557,7 +568,16 @@ void RoomManager::setCurrentRoom(const QString &roomId)
|
|||||||
|
|
||||||
Q_EMIT currentRoomChanged();
|
Q_EMIT currentRoomChanged();
|
||||||
if (m_connection) {
|
if (m_connection) {
|
||||||
m_lastRoomConfig.writeEntry(m_connection->userId(), roomId);
|
if (roomId.isEmpty()) {
|
||||||
|
m_lastRoomConfig.deleteEntry(m_currentSpaceId);
|
||||||
|
} else {
|
||||||
|
// We can't have empty keys in KConfig, so name it "Home"
|
||||||
|
if (m_currentSpaceId.isEmpty()) {
|
||||||
|
m_lastRoomConfig.writeEntry(u"Home"_s, roomId);
|
||||||
|
} else {
|
||||||
|
m_lastRoomConfig.writeEntry(m_currentSpaceId, roomId);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (roomId.isEmpty()) {
|
if (roomId.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -195,11 +195,6 @@ public:
|
|||||||
*/
|
*/
|
||||||
Q_INVOKABLE void loadInitialRoom();
|
Q_INVOKABLE void loadInitialRoom();
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Leave the room and close it if it is open.
|
|
||||||
*/
|
|
||||||
Q_INVOKABLE void leaveRoom(NeoChatRoom *room);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Knock a room.
|
* @brief Knock a room.
|
||||||
*
|
*
|
||||||
@@ -208,6 +203,13 @@ public:
|
|||||||
*/
|
*/
|
||||||
void knockRoom(NeoChatConnection *account, const QString &roomAliasOrId, const QString &reason, const QStringList &viaServers);
|
void knockRoom(NeoChatConnection *account, const QString &roomAliasOrId, const QString &reason, const QStringList &viaServers);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Cleanup after the given room is left.
|
||||||
|
*
|
||||||
|
* This ensures that the current room and space are not set to the left room.
|
||||||
|
*/
|
||||||
|
void roomLeft(const QString &id);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Show a media item maximized.
|
* @brief Show a media item maximized.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -15,4 +15,5 @@ ecm_add_qml_module(Chatbar GENERATE_PLUGIN_SOURCE
|
|||||||
EmojiPicker.qml
|
EmojiPicker.qml
|
||||||
EmojiDialog.qml
|
EmojiDialog.qml
|
||||||
EmojiTonesPicker.qml
|
EmojiTonesPicker.qml
|
||||||
|
ImageEditorPage.qml
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -55,6 +55,19 @@ QQC2.Control {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: currentRoom.mainCache
|
||||||
|
|
||||||
|
function onMentionAdded(mention: string): void {
|
||||||
|
// add mention text
|
||||||
|
textField.append(mention + " ");
|
||||||
|
// move cursor to the end
|
||||||
|
textField.cursorPosition = textField.text.length;
|
||||||
|
// move the focus back to the chat bar
|
||||||
|
textField.forceActiveFocus(Qt.OtherFocusReason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The list of actions in the ChatBar.
|
* @brief The list of actions in the ChatBar.
|
||||||
*
|
*
|
||||||
@@ -147,11 +160,6 @@ QQC2.Control {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief A message has been sent from the chat bar.
|
|
||||||
*/
|
|
||||||
signal messageSent
|
|
||||||
|
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||||
@@ -342,11 +350,13 @@ QQC2.Control {
|
|||||||
icon.name: modelData.isBusy ? "" : (modelData.icon.name.length > 0 ? modelData.icon.name : modelData.icon.source)
|
icon.name: modelData.isBusy ? "" : (modelData.icon.name.length > 0 ? modelData.icon.name : modelData.icon.source)
|
||||||
onClicked: modelData.trigger()
|
onClicked: modelData.trigger()
|
||||||
|
|
||||||
|
padding: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
QQC2.ToolTip.visible: hovered
|
QQC2.ToolTip.visible: hovered
|
||||||
QQC2.ToolTip.text: modelData.tooltip
|
QQC2.ToolTip.text: modelData.tooltip
|
||||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
|
||||||
PieProgressBar {
|
contentItem: PieProgressBar {
|
||||||
visible: modelData.isBusy
|
visible: modelData.isBusy
|
||||||
progress: root.currentRoom.fileUploadingProgress
|
progress: root.currentRoom.fileUploadingProgress
|
||||||
}
|
}
|
||||||
@@ -421,7 +431,6 @@ QQC2.Control {
|
|||||||
repeatTimer.stop();
|
repeatTimer.stop();
|
||||||
root.currentRoom.markAllMessagesAsRead();
|
root.currentRoom.markAllMessagesAsRead();
|
||||||
textField.clear();
|
textField.clear();
|
||||||
messageSent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatText(format, selectionStart, selectionEnd) {
|
function formatText(format, selectionStart, selectionEnd) {
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ ColumnLayout {
|
|||||||
padding: Kirigami.Units.largeSpacing
|
padding: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
contentItem: Image {
|
contentItem: Image {
|
||||||
source: model.avatarUrl
|
source: model.url
|
||||||
fillMode: Image.PreserveAspectFit
|
fillMode: Image.PreserveAspectFit
|
||||||
sourceSize.width: width
|
sourceSize.width: width
|
||||||
sourceSize.height: height
|
sourceSize.height: height
|
||||||
|
|||||||
@@ -36,4 +36,13 @@ FormCard.FormCard {
|
|||||||
NeoChatConfig.save();
|
NeoChatConfig.save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
FormCard.FormCheckDelegate {
|
||||||
|
text: i18nc("@option:check Enable the matrix feature for audio and video calling", "Calls")
|
||||||
|
checked: NeoChatConfig.calls
|
||||||
|
|
||||||
|
onToggled: {
|
||||||
|
NeoChatConfig.calls = checked;
|
||||||
|
NeoChatConfig.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ target_sources(LibNeoChat PRIVATE
|
|||||||
enums/pushrule.h
|
enums/pushrule.h
|
||||||
enums/roomsortparameter.cpp
|
enums/roomsortparameter.cpp
|
||||||
enums/roomsortorder.h
|
enums/roomsortorder.h
|
||||||
|
enums/timelinemarkreadcondition.h
|
||||||
events/imagepackevent.cpp
|
events/imagepackevent.cpp
|
||||||
events/pollevent.cpp
|
events/pollevent.cpp
|
||||||
jobs/neochatgetcommonroomsjob.cpp
|
jobs/neochatgetcommonroomsjob.cpp
|
||||||
@@ -41,12 +42,21 @@ target_sources(LibNeoChat PRIVATE
|
|||||||
models/locationsmodel.cpp
|
models/locationsmodel.cpp
|
||||||
models/roomlistmodel.cpp
|
models/roomlistmodel.cpp
|
||||||
models/stickermodel.cpp
|
models/stickermodel.cpp
|
||||||
|
models/userfiltermodel.cpp
|
||||||
models/userlistmodel.cpp
|
models/userlistmodel.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
ecm_add_qml_module(LibNeoChat GENERATE_PLUGIN_SOURCE
|
ecm_add_qml_module(LibNeoChat GENERATE_PLUGIN_SOURCE
|
||||||
URI org.kde.neochat.libneochat
|
URI org.kde.neochat.libneochat
|
||||||
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat/libneochat
|
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat/libneochat
|
||||||
|
QML_FILES
|
||||||
|
qml/GroupChatDrawerHeader.qml
|
||||||
|
qml/LocationMapItem.qml
|
||||||
|
qml/InviteUserPage.qml
|
||||||
|
qml/ExploreRoomsPage.qml
|
||||||
|
qml/SearchPage.qml
|
||||||
|
qml/CreateRoomDialog.qml
|
||||||
|
qml/CreateSpaceDialog.qml
|
||||||
)
|
)
|
||||||
|
|
||||||
ecm_qt_declare_logging_category(LibNeoChat
|
ecm_qt_declare_logging_category(LibNeoChat
|
||||||
|
|||||||
@@ -23,12 +23,10 @@ AccountManager::AccountManager(bool testMode, QObject *parent)
|
|||||||
loadAccountsFromCache();
|
loadAccountsFromCache();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
auto c = new NeoChatConnection(this);
|
auto c = new NeoChatConnection(QUrl(u"https://localhost:1234"_s), this);
|
||||||
c->assumeIdentity(u"@user:localhost:1234"_s, u"device_1234"_s, u"token_1234"_s);
|
c->assumeIdentity(u"@user:localhost:1234"_s, u"device_1234"_s, u"token_1234"_s);
|
||||||
connect(c, &NeoChatConnection::connected, this, [c, this]() {
|
m_accountRegistry->add(c);
|
||||||
m_accountRegistry->add(c);
|
c->syncLoop();
|
||||||
c->syncLoop();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ Q_SIGNALS:
|
|||||||
void relationIdChanged(const QString &oldEventId, const QString &newEventId);
|
void relationIdChanged(const QString &oldEventId, const QString &newEventId);
|
||||||
void threadIdChanged(const QString &oldThreadId, const QString &newThreadId);
|
void threadIdChanged(const QString &oldThreadId, const QString &newThreadId);
|
||||||
void attachmentPathChanged();
|
void attachmentPathChanged();
|
||||||
|
void mentionAdded(const QString &mention);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString m_text = QString();
|
QString m_text = QString();
|
||||||
|
|||||||
32
src/libneochat/enums/timelinemarkreadcondition.h
Normal file
32
src/libneochat/enums/timelinemarkreadcondition.h
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QQmlEngine>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class TimelineMarkReadCondition
|
||||||
|
*
|
||||||
|
* This class is designed to define the TimelineMarkReadCondition enumeration.
|
||||||
|
*/
|
||||||
|
class TimelineMarkReadCondition : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
QML_UNCREATABLE("")
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief The condition for marking messages as read.
|
||||||
|
*/
|
||||||
|
enum Condition {
|
||||||
|
Never = 0, /**< Messages should never be marked automatically. */
|
||||||
|
Entry, /**< Messages should be marked automatically on entry to the room. */
|
||||||
|
EntryVisible, /**< Messages should be marked automatically on entry to the room if all messages are visible. */
|
||||||
|
Exit, /**< Messages should be marked automatically on exiting the room. */
|
||||||
|
ExitVisible, /**< Messages should be marked automatically on exiting the room if all messages are visible. */
|
||||||
|
};
|
||||||
|
Q_ENUM(Condition);
|
||||||
|
};
|
||||||
@@ -435,6 +435,13 @@ QString EventHandler::getBody(const NeoChatRoom *room, const Quotient::RoomEvent
|
|||||||
return i18nc("[User] configured <name> widget", "configured %1 widget", e.contentJson()["name"_L1].toString());
|
return i18nc("[User] configured <name> widget", "configured %1 widget", e.contentJson()["name"_L1].toString());
|
||||||
},
|
},
|
||||||
[prettyPrint](const StateEvent &e) {
|
[prettyPrint](const StateEvent &e) {
|
||||||
|
if (e.matrixType() == "org.matrix.msc3401.call.member"_L1) {
|
||||||
|
if (e.contentJson().isEmpty()) {
|
||||||
|
return i18nc("[User] left a [voice/video] call", "left a call");
|
||||||
|
} else {
|
||||||
|
return i18nc("[User] joined a [voice/video] call", "joined a call");
|
||||||
|
}
|
||||||
|
}
|
||||||
return e.stateKey().isEmpty() ? i18n("updated %1 state", e.matrixType())
|
return e.stateKey().isEmpty() ? i18n("updated %1 state", e.matrixType())
|
||||||
: i18n("updated %1 state for %2", e.matrixType(), prettyPrint ? e.stateKey().toHtmlEscaped() : e.stateKey());
|
: i18n("updated %1 state for %2", e.matrixType(), prettyPrint ? e.stateKey().toHtmlEscaped() : e.stateKey());
|
||||||
},
|
},
|
||||||
@@ -634,7 +641,14 @@ QString EventHandler::genericBody(const NeoChatRoom *room, const Quotient::RoomE
|
|||||||
}
|
}
|
||||||
return i18n("%1 configured a widget", senderString);
|
return i18n("%1 configured a widget", senderString);
|
||||||
},
|
},
|
||||||
[senderString](const StateEvent &) {
|
[senderString](const StateEvent &e) {
|
||||||
|
if (e.matrixType() == "org.matrix.msc3401.call.member"_L1) {
|
||||||
|
if (e.contentJson().isEmpty()) {
|
||||||
|
return i18nc("[User] left a [voice/video] call", "%1 left a call", senderString);
|
||||||
|
} else {
|
||||||
|
return i18nc("[User] joined a [voice/video] call", "%1 joined a call", senderString);
|
||||||
|
}
|
||||||
|
}
|
||||||
return i18n("%1 updated the state", senderString);
|
return i18n("%1 updated the state", senderString);
|
||||||
},
|
},
|
||||||
[senderString](const PollStartEvent &) {
|
[senderString](const PollStartEvent &) {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ struct MessageComponent {
|
|||||||
QString content;
|
QString content;
|
||||||
QVariantMap attributes;
|
QVariantMap attributes;
|
||||||
|
|
||||||
int operator==(const MessageComponent &right) const
|
bool operator==(const MessageComponent &right) const
|
||||||
{
|
{
|
||||||
return type == right.type && content == right.content && attributes == right.attributes;
|
return type == right.type && content == right.content && attributes == right.attributes;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ QStringList rainbowColors{"#ff2b00"_L1, "#ff5500"_L1, "#ff8000"_L1, "#ffaa00"_L1
|
|||||||
auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *) {
|
auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *) {
|
||||||
if (text.isEmpty()) {
|
if (text.isEmpty()) {
|
||||||
Q_EMIT room->showMessage(MessageType::Information, i18n("Leaving this room."));
|
Q_EMIT room->showMessage(MessageType::Information, i18n("Leaving this room."));
|
||||||
room->connection()->leaveRoom(room);
|
room->forget();
|
||||||
} else {
|
} else {
|
||||||
QRegularExpression roomRegex(uR"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"_s);
|
QRegularExpression roomRegex(uR"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"_s);
|
||||||
auto regexMatch = roomRegex.match(text);
|
auto regexMatch = roomRegex.match(text);
|
||||||
@@ -38,13 +38,13 @@ auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *
|
|||||||
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
|
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
|
||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
auto leaving = room->connection()->room(text);
|
auto leaving = dynamic_cast<NeoChatRoom *>(room->connection()->room(text));
|
||||||
if (!leaving) {
|
if (!leaving) {
|
||||||
leaving = room->connection()->roomByAlias(text);
|
leaving = dynamic_cast<NeoChatRoom *>(room->connection()->roomByAlias(text));
|
||||||
}
|
}
|
||||||
if (leaving) {
|
if (leaving) {
|
||||||
Q_EMIT room->showMessage(MessageType::Information, i18nc("Leaving room <roomname>.", "Leaving room %1.", text));
|
Q_EMIT room->showMessage(MessageType::Information, i18nc("Leaving room <roomname>.", "Leaving room %1.", text));
|
||||||
room->connection()->leaveRoom(leaving);
|
leaving->forget();
|
||||||
} else {
|
} else {
|
||||||
Q_EMIT room->showMessage(MessageType::Information, i18nc("Room <roomname> not found", "Room %1 not found.", text));
|
Q_EMIT room->showMessage(MessageType::Information, i18nc("Room <roomname> not found", "Room %1 not found.", text));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "neochatroom.h"
|
#include "neochatroom.h"
|
||||||
#include "spacehierarchycache.h"
|
#include "spacehierarchycache.h"
|
||||||
|
|
||||||
|
#include <Quotient/connection.h>
|
||||||
#include <Quotient/jobs/basejob.h>
|
#include <Quotient/jobs/basejob.h>
|
||||||
#include <Quotient/quotient_common.h>
|
#include <Quotient/quotient_common.h>
|
||||||
#include <qt6keychain/keychain.h>
|
#include <qt6keychain/keychain.h>
|
||||||
@@ -386,6 +387,13 @@ void NeoChatConnection::createSpace(const QString &name, const QString &topic, c
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Quotient::ForgetRoomJob *NeoChatConnection::forgetRoom(const QString &id)
|
||||||
|
{
|
||||||
|
Q_EMIT roomAboutToBeLeft(id);
|
||||||
|
|
||||||
|
return Connection::forgetRoom(id);
|
||||||
|
}
|
||||||
|
|
||||||
bool NeoChatConnection::directChatExists(Quotient::User *user)
|
bool NeoChatConnection::directChatExists(Quotient::User *user)
|
||||||
{
|
{
|
||||||
return directChats().contains(user);
|
return directChats().contains(user);
|
||||||
|
|||||||
@@ -150,6 +150,14 @@ public:
|
|||||||
*/
|
*/
|
||||||
Q_INVOKABLE void createSpace(const QString &name, const QString &topic, const QString &parent = {}, bool setChildParent = false);
|
Q_INVOKABLE void createSpace(const QString &name, const QString &topic, const QString &parent = {}, bool setChildParent = false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Send /forget to the server and delete room locally.
|
||||||
|
*
|
||||||
|
* @note This wraps around the Quotient::Connection::forgetRoom() to allow
|
||||||
|
* roomAboutToBeLeft() to be emitted.
|
||||||
|
*/
|
||||||
|
Quotient::ForgetRoomJob *forgetRoom(const QString &id);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Whether a direct chat with the user exists.
|
* @brief Whether a direct chat with the user exists.
|
||||||
*/
|
*/
|
||||||
@@ -224,6 +232,11 @@ Q_SIGNALS:
|
|||||||
*/
|
*/
|
||||||
void errorOccured(const QString &error);
|
void errorOccured(const QString &error);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The given room ID is about to be forgotten.
|
||||||
|
*/
|
||||||
|
void roomAboutToBeLeft(const QString &id);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static bool m_globalUrlPreviewDefault;
|
static bool m_globalUrlPreviewDefault;
|
||||||
static PushRuleAction::Action m_defaultAction;
|
static PushRuleAction::Action m_defaultAction;
|
||||||
|
|||||||
@@ -59,6 +59,10 @@
|
|||||||
|
|
||||||
using namespace Quotient;
|
using namespace Quotient;
|
||||||
|
|
||||||
|
std::function<bool(const Quotient::RoomEvent *)> NeoChatRoom::m_hiddenFilter = [](const Quotient::RoomEvent *) -> bool {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinState)
|
NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinState)
|
||||||
: Room(connection, std::move(roomId), joinState)
|
: Room(connection, std::move(roomId), joinState)
|
||||||
{
|
{
|
||||||
@@ -305,8 +309,9 @@ void NeoChatRoom::forget()
|
|||||||
roomIds += predecessor->id();
|
roomIds += predecessor->id();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto neochatConnection = dynamic_cast<NeoChatConnection *>(connection());
|
||||||
for (const auto &id : roomIds) {
|
for (const auto &id : roomIds) {
|
||||||
connection()->forgetRoom(id);
|
neochatConnection->forgetRoom(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,7 +373,7 @@ const RoomEvent *NeoChatRoom::lastEvent(std::function<bool(const RoomEvent *)> f
|
|||||||
|
|
||||||
void NeoChatRoom::cacheLastEvent()
|
void NeoChatRoom::cacheLastEvent()
|
||||||
{
|
{
|
||||||
auto event = lastEvent();
|
auto event = lastEvent(m_hiddenFilter);
|
||||||
if (event != nullptr) {
|
if (event != nullptr) {
|
||||||
auto &roomLastMessageProvider = RoomLastMessageProvider::self();
|
auto &roomLastMessageProvider = RoomLastMessageProvider::self();
|
||||||
|
|
||||||
@@ -480,30 +485,6 @@ void NeoChatRoom::changeAvatar(const QUrl &localFile)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QString msgTypeToString(MessageEventType msgType)
|
|
||||||
{
|
|
||||||
switch (msgType) {
|
|
||||||
case MessageEventType::Text:
|
|
||||||
return "m.text"_L1;
|
|
||||||
case MessageEventType::File:
|
|
||||||
return "m.file"_L1;
|
|
||||||
case MessageEventType::Audio:
|
|
||||||
return "m.audio"_L1;
|
|
||||||
case MessageEventType::Emote:
|
|
||||||
return "m.emote"_L1;
|
|
||||||
case MessageEventType::Image:
|
|
||||||
return "m.image"_L1;
|
|
||||||
case MessageEventType::Video:
|
|
||||||
return "m.video"_L1;
|
|
||||||
case MessageEventType::Notice:
|
|
||||||
return "m.notice"_L1;
|
|
||||||
case MessageEventType::Location:
|
|
||||||
return "m.location"_L1;
|
|
||||||
default:
|
|
||||||
return "m.text"_L1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void NeoChatRoom::toggleReaction(const QString &eventId, const QString &reaction)
|
void NeoChatRoom::toggleReaction(const QString &eventId, const QString &reaction)
|
||||||
{
|
{
|
||||||
if (eventId.isEmpty() || reaction.isEmpty()) {
|
if (eventId.isEmpty() || reaction.isEmpty()) {
|
||||||
@@ -1233,34 +1214,38 @@ QByteArray NeoChatRoom::getEventJsonSource(const QString &eventId)
|
|||||||
void NeoChatRoom::openEventMediaExternally(const QString &eventId)
|
void NeoChatRoom::openEventMediaExternally(const QString &eventId)
|
||||||
{
|
{
|
||||||
const auto evtIt = findInTimeline(eventId);
|
const auto evtIt = findInTimeline(eventId);
|
||||||
if (evtIt != messageEvents().rend() && is<RoomMessageEvent>(**evtIt)) {
|
if (evtIt == messageEvents().rend()) {
|
||||||
const auto event = evtIt->viewAs<RoomMessageEvent>();
|
return;
|
||||||
if (event->has<EventContent::FileContent>()) {
|
}
|
||||||
const auto transferInfo = cachedFileTransferInfo(event);
|
|
||||||
if (transferInfo.completed()) {
|
// TODO: Also allow stickers here, once that's fixed in libQuotient
|
||||||
|
if (!is<RoomMessageEvent>(**evtIt) || !evtIt->viewAs<RoomMessageEvent>()->has<EventContent::FileContentBase>()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto transferInfo = cachedFileTransferInfo(evtIt->viewAs<RoomEvent>());
|
||||||
|
if (transferInfo.completed()) {
|
||||||
|
UrlHelper helper;
|
||||||
|
helper.openUrl(transferInfo.localPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
downloadFile(eventId,
|
||||||
|
QUrl(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + u'/'
|
||||||
|
+ evtIt->event()->id().replace(u':', u'_').replace(u'/', u'_').replace(u'+', u'_') + fileNameToDownload(eventId)));
|
||||||
|
connect(
|
||||||
|
this,
|
||||||
|
&Room::fileTransferCompleted,
|
||||||
|
this,
|
||||||
|
[this, eventId](QString id, QUrl localFile, FileSourceInfo fileMetadata) {
|
||||||
|
Q_UNUSED(localFile);
|
||||||
|
Q_UNUSED(fileMetadata);
|
||||||
|
if (id == eventId) {
|
||||||
|
auto transferInfo = fileTransferInfo(eventId);
|
||||||
UrlHelper helper;
|
UrlHelper helper;
|
||||||
helper.openUrl(transferInfo.localPath);
|
helper.openUrl(transferInfo.localPath);
|
||||||
} else {
|
|
||||||
downloadFile(eventId,
|
|
||||||
QUrl(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + u'/'
|
|
||||||
+ event->id().replace(u':', u'_').replace(u'/', u'_').replace(u'+', u'_') + fileNameToDownload(eventId)));
|
|
||||||
connect(
|
|
||||||
this,
|
|
||||||
&Room::fileTransferCompleted,
|
|
||||||
this,
|
|
||||||
[this, eventId](QString id, QUrl localFile, FileSourceInfo fileMetadata) {
|
|
||||||
Q_UNUSED(localFile);
|
|
||||||
Q_UNUSED(fileMetadata);
|
|
||||||
if (id == eventId) {
|
|
||||||
auto transferInfo = fileTransferInfo(eventId);
|
|
||||||
UrlHelper helper;
|
|
||||||
helper.openUrl(transferInfo.localPath);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
static_cast<Qt::ConnectionType>(Qt::SingleShotConnection));
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
static_cast<Qt::ConnectionType>(Qt::SingleShotConnection));
|
||||||
}
|
}
|
||||||
|
|
||||||
void NeoChatRoom::copyEventMedia(const QString &eventId)
|
void NeoChatRoom::copyEventMedia(const QString &eventId)
|
||||||
@@ -1741,4 +1726,9 @@ QString NeoChatRoom::rootIdForThread(const QString &eventId) const
|
|||||||
return rootId;
|
return rootId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NeoChatRoom::setHiddenFilter(std::function<bool(const Quotient::RoomEvent *)> hiddenFilter)
|
||||||
|
{
|
||||||
|
NeoChatRoom::m_hiddenFilter = hiddenFilter;
|
||||||
|
}
|
||||||
|
|
||||||
#include "moc_neochatroom.cpp"
|
#include "moc_neochatroom.cpp"
|
||||||
|
|||||||
@@ -557,7 +557,7 @@ public:
|
|||||||
* responsibility of the caller to ensure that they only ask for objects
|
* responsibility of the caller to ensure that they only ask for objects
|
||||||
* for real senders.
|
* for real senders.
|
||||||
*/
|
*/
|
||||||
NeochatRoomMember *qmlSafeMember(const QString &memberId);
|
Q_INVOKABLE NeochatRoomMember *qmlSafeMember(const QString &memberId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Pin a message in the room.
|
* @brief Pin a message in the room.
|
||||||
@@ -587,6 +587,8 @@ public:
|
|||||||
*/
|
*/
|
||||||
Q_INVOKABLE QString rootIdForThread(const QString &eventId) const;
|
Q_INVOKABLE QString rootIdForThread(const QString &eventId) const;
|
||||||
|
|
||||||
|
static void setHiddenFilter(std::function<bool(const Quotient::RoomEvent *)> hiddenFilter);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool m_visible = false;
|
bool m_visible = false;
|
||||||
|
|
||||||
@@ -618,6 +620,7 @@ private:
|
|||||||
void cleanupExtraEvent(const QString &eventId);
|
void cleanupExtraEvent(const QString &eventId);
|
||||||
|
|
||||||
std::unordered_map<QString, std::unique_ptr<NeochatRoomMember>> m_memberObjects;
|
std::unordered_map<QString, std::unique_ptr<NeochatRoomMember>> m_memberObjects;
|
||||||
|
static std::function<bool(const Quotient::RoomEvent *)> m_hiddenFilter;
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void updatePushNotificationState(QString type);
|
void updatePushNotificationState(QString type);
|
||||||
|
|||||||
73
src/libneochat/qml/CreateRoomDialog.qml
Normal file
73
src/libneochat/qml/CreateRoomDialog.qml
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-or-later OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls as QQC2
|
||||||
|
import QtQuick.Layouts
|
||||||
|
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
import org.kde.kirigamiaddons.formcard as FormCard
|
||||||
|
import org.kde.kirigamiaddons.labs.components as Components
|
||||||
|
|
||||||
|
import org.kde.neochat
|
||||||
|
|
||||||
|
Kirigami.Dialog {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string parentId
|
||||||
|
|
||||||
|
required property NeoChatConnection connection
|
||||||
|
|
||||||
|
signal newChild(string childName)
|
||||||
|
|
||||||
|
title: i18nc("@title", "Create Room")
|
||||||
|
implicitWidth: Kirigami.Units.gridUnit * 20
|
||||||
|
standardButtons: Kirigami.Dialog.Cancel
|
||||||
|
|
||||||
|
customFooterActions: [
|
||||||
|
Kirigami.Action {
|
||||||
|
icon.name: "list-add-symbolic"
|
||||||
|
text: i18nc("@action:button Create new room", "Create")
|
||||||
|
enabled: roomNameField.text.length > 0
|
||||||
|
onTriggered: {
|
||||||
|
root.connection.createRoom(roomNameField.text, "", root.parentId, false);
|
||||||
|
root.newChild(roomNameField.text);
|
||||||
|
root.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
Component.onCompleted: roomNameField.forceActiveFocus()
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
|
FormCard.FormRadioDelegate {
|
||||||
|
id: privateTypeDelegate
|
||||||
|
text: i18nc("@info:label", "Private")
|
||||||
|
description: i18nc("@info:description", "This room can only be joined with an invite.")
|
||||||
|
checked: true
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormRadioDelegate {
|
||||||
|
id: publicTypeDelegate
|
||||||
|
text: i18nc("@info:label", "Public")
|
||||||
|
description: i18nc("@info:description", "This room can be found and joined by anyone.")
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
||||||
|
FormCard.FormTextFieldDelegate {
|
||||||
|
id: roomNameField
|
||||||
|
label: i18nc("@info:label Name of the room", "Name:")
|
||||||
|
placeholderText: i18nc("@info:placeholder Placeholder for room name", "New Room")
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormTextFieldDelegate {
|
||||||
|
id: roomAddressField
|
||||||
|
label: i18nc("@info:label Address or alias to refer to the room by", "Address:")
|
||||||
|
placeholderText: i18nc("@info:placeholder Placeholder address for the room", "new-room")
|
||||||
|
visible: publicTypeDelegate.checked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user