Compare commits

..

1 Commits

Author SHA1 Message Date
James Graham
db79d35c65 Use QQC2 Dialog with updated style instead of Kirigami.Dialog 2025-05-17 14:14:48 +01:00
284 changed files with 49480 additions and 64955 deletions

View File

@@ -2,5 +2,7 @@
; 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

View File

@@ -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.9", "runtime-version": "6.8",
"sdk": "org.kde.Sdk", "sdk": "org.kde.Sdk",
"command": "neochat", "command": "neochat",
"tags": [ "tags": [
@@ -149,6 +149,27 @@
], ],
"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",

View File

@@ -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 "80") set(RELEASE_SERVICE_VERSION_MICRO "70")
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,7 +164,6 @@ endif()
if(ANDROID) if(ANDROID)
find_package(Sqlite3) find_package(Sqlite3)
set(BUILD_TESTING FALSE)
endif() endif()
ki18n_install(po) ki18n_install(po)
@@ -179,7 +178,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 HttpServer) find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test)
add_subdirectory(autotests) add_subdirectory(autotests)
# add_subdirectory(appiumtests) # add_subdirectory(appiumtests)
if (NOT ANDROID) if (NOT ANDROID)

View File

@@ -3,10 +3,6 @@
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(
@@ -89,6 +85,6 @@ ecm_add_test(
ecm_add_test( ecm_add_test(
actionstest.cpp actionstest.cpp
LINK_LIBRARIES neochat Qt::Test neochat_server LINK_LIBRARIES neochat Qt::Test
TEST_NAME actionstest TEST_NAME actionstest
) )

View File

@@ -6,11 +6,9 @@
#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;
@@ -23,12 +21,10 @@ class ActionsTest : public QObject
private: private:
Connection *connection = nullptr; Connection *connection = nullptr;
NeoChatRoom *room = nullptr; TestUtils::TestRoom *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();
@@ -38,23 +34,8 @@ private Q_SLOTS:
void ActionsTest::initTestCase() void ActionsTest::initTestCase()
{ {
Connection::setRoomType<NeoChatRoom>(); connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
server.start(); room = new TestUtils::TestRoom(connection, QStringLiteral("#myroom:kde.org"), QLatin1String("test-min-sync.json"));
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()
@@ -109,7 +90,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, &NeoChatRoom::showMessage); QSignalSpy spy(room, &TestUtils::TestRoom::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();
@@ -125,26 +106,14 @@ 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.com"_s, MessageType::Information, u"@example:example.com is already in this room."_s); expectMessage(u"invite"_s, u"@example:example.org"_s, MessageType::Information, u"@example:example.org is already in this room."_s);
QCOMPARE(room->memberState(u"@example:example.com"_s), Membership::Join); QCOMPARE(room->memberState(u"@example:example.org"_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);
QSignalSpy spy(room, &NeoChatRoom::changed); //TODO mock server, wait for invite state to change
QVERIFY(spy.wait()); //TODO QCOMPARE(room->memberState(u"@user:example.com"_s), Membership::Invite);
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)

View File

@@ -1,20 +0,0 @@
-----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-----

View File

@@ -1,28 +0,0 @@
-----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-----

View File

@@ -1,235 +0,0 @@
// 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;
}

View File

@@ -1,33 +0,0 @@
// 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;
};

View File

@@ -61,7 +61,6 @@ 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();
@@ -265,8 +264,6 @@ 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;
@@ -451,32 +448,6 @@ 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&amp;via=matrix.org&amp;via=fedora.im <a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im\">Link already rich</a>"_s
<< u"<a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im\">https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im</a> <a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;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.
* *
@@ -485,13 +456,46 @@ void TextHandlerTest::receiveRichPlainUrl_data()
*/ */
void TextHandlerTest::receiveRichPlainUrl() void TextHandlerTest::receiveRichPlainUrl()
{ {
QFETCH(QString, input); // This is an actual link that caused trouble which is why it's so long. Keeping
QFETCH(QString, output); // so we can confirm consistent behaviour for complex urls.
const QString testInputStringLink1 =
u"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im <a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im\">Link already rich</a>"_s;
const QString testOutputStringLink1 =
u"<a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im\">https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im</a> <a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;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(input); testTextHandler.setData(testInputStringLink1);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), output); QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringLink1);
testTextHandler.setData(testInputStringLink2);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringLink2);
testTextHandler.setData(testInputStringEmail);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringEmail);
testTextHandler.setData(testInputStringMxId);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringMxId);
testTextHandler.setData(testInputStringMxIdWithPrefix);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringMxIdWithPrefix);
} }
void TextHandlerTest::receiveRichEdited_data() void TextHandlerTest::receiveRichEdited_data()

View File

@@ -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(NeoChatRoom *, NeoChatRoom *))); QSignalSpy spy(model, SIGNAL(roomChanged()));
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(NeoChatRoom *, NeoChatRoom *))); QSignalSpy spy(model, SIGNAL(roomChanged()));
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->indexforEventId(u"$153456789:example.org"_s).row(), 0); QCOMPARE(model->eventIdToRow(u"$153456789:example.org"_s), 0);
} }
void TimelineMessageModelTest::cleanup() void TimelineMessageModelTest::cleanup()

View File

@@ -477,8 +477,6 @@
<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"/>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -8,8 +8,6 @@ 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)

View File

@@ -8,6 +8,8 @@ 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
@@ -20,6 +22,8 @@ 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
@@ -45,9 +49,6 @@ 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
@@ -57,13 +58,18 @@ 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
@@ -73,14 +79,26 @@ 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
@@ -100,15 +118,12 @@ 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
@@ -124,10 +139,7 @@ if(NOT ANDROID AND NOT WIN32)
qml/EditMenu.qml qml/EditMenu.qml
) )
else() else()
qt_target_qml_sources(neochat QML_FILES qt_target_qml_sources(neochat QML_FILES qml/ShareActionStub.qml)
qml/ShareActionStub.qml
qml/GlobalMenuStub.qml
)
endif() endif()
if(WIN32) if(WIN32)
@@ -177,7 +189,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 RoomInfoplugin MessageContentplugin Timelineplugin Spacesplugin Chatbarplugin Settingsplugin Devtoolsplugin) target_link_libraries(neochat PRIVATE Loginplugin Roomsplugin Timelineplugin Spacesplugin Chatbarplugin Settingsplugin Devtoolsplugin)
target_link_libraries(neochat PUBLIC target_link_libraries(neochat PUBLIC
LibNeoChat LibNeoChat
Timeline Timeline
@@ -202,7 +214,6 @@ target_link_libraries(neochat PUBLIC
QuotientQt6 QuotientQt6
Login Login
Rooms Rooms
MessageContent
Spaces Spaces
) )
@@ -357,10 +368,3 @@ 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 ()

View File

@@ -32,7 +32,6 @@
#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"
@@ -99,7 +98,6 @@ 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, []() {
@@ -232,7 +230,6 @@ 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);
} }
@@ -407,9 +404,4 @@ 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"

View File

@@ -99,7 +99,6 @@ 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);

223
src/app/logger.cpp Normal file
View File

@@ -0,0 +1,223 @@
// 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"

9
src/app/logger.h Normal file
View File

@@ -0,0 +1,9 @@
// 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();

View File

@@ -49,6 +49,7 @@
#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"
@@ -137,11 +138,6 @@ 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);
@@ -181,6 +177,8 @@ 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);
@@ -197,9 +195,6 @@ 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);
@@ -236,7 +231,7 @@ int main(int argc, char *argv[])
#endif #endif
#ifdef HAVE_KDBUSADDONS #ifdef HAVE_KDBUSADDONS
KDBusService service(KDBusService::Unique | (parser.isSet(replaceOption) ? KDBusService::Replace : KDBusService::StartupOption(0))); KDBusService service(KDBusService::Unique);
#endif #endif
const auto accountManager = std::make_unique<AccountManager>(parser.isSet("test"_L1)); const auto accountManager = std::make_unique<AccountManager>(parser.isSet("test"_L1));
@@ -244,6 +239,13 @@ 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);

View File

@@ -78,12 +78,6 @@
<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>
@@ -211,10 +205,6 @@
<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">

View File

@@ -276,7 +276,7 @@ void NotificationsManager::postInviteNotification(NeoChatRoom *rawRoom)
if (inAnyOfOurRooms) { if (inAnyOfOurRooms) {
doPostInviteNotification(room); doPostInviteNotification(room);
} else { } else {
room->forget(); room->leaveRoom();
} }
} }
}); });
@@ -330,14 +330,14 @@ void NotificationsManager::doPostInviteNotification(QPointer<NeoChatRoom> room)
if (!room) { if (!room) {
return; return;
} }
room->forget(); RoomManager::instance().leaveRoom(room);
notification->close(); notification->close();
}); });
connect(rejectAndIgnoreAction, &KNotificationAction::activated, this, [room, notification]() { connect(rejectAndIgnoreAction, &KNotificationAction::activated, this, [room, notification]() {
if (!room) { if (!room) {
return; return;
} }
room->forget(); RoomManager::instance().leaveRoom(room);
room->connection()->addToIgnoredUsers(room->invitingUserId()); room->connection()->addToIgnoredUsers(room->invitingUserId());
notification->close(); notification->close();
}); });

View File

@@ -36,18 +36,14 @@ 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: NeoChatSettingsView.openWithInitialProperties("accounts", {initialAccount: root.connection}); onTriggered: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat.settings', 'AccountEditorPage'), {
connection: root.connection
}, {
title: i18n("Account editor")
})
} }
QQC2.Action { QQC2.Action {
@@ -96,7 +92,7 @@ KirigamiComponents.ConvergentContextMenu {
const dialog = Qt.createComponent("org.kde.kirigami", "PromptDialog").createObject(QQC2.Overlay.overlay, { const dialog = Qt.createComponent("org.kde.kirigami", "PromptDialog").createObject(QQC2.Overlay.overlay, {
title: i18nc("@title", "Verification Request Sent"), title: i18nc("@title", "Verification Request Sent"),
subtitle: i18nc("@info:label", "To proceed, accept the verification request on another device."), subtitle: i18nc("@info:label", "To proceed, accept the verification request on another device."),
standardButtons: Kirigami.Dialog.Ok standardButtons: QQC2.Dialog.Ok
}) })
dialog.open(); dialog.open();
root.connection.onNewKeyVerificationSession.connect(() => { root.connection.onNewKeyVerificationSession.connect(() => {

View File

@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts import QtQuick.Layouts
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
@@ -11,7 +12,7 @@ import org.kde.kirigamiaddons.delegates as Delegates
import org.kde.neochat import org.kde.neochat
Kirigami.Dialog { QQC2.Dialog {
id: root id: root
required property NeoChatConnection connection required property NeoChatConnection connection
@@ -23,7 +24,7 @@ Kirigami.Dialog {
topPadding: 0 topPadding: 0
bottomPadding: 0 bottomPadding: 0
standardButtons: Kirigami.Dialog.NoButton standardButtons: QQC2.Dialog.NoButton
width: Math.min(applicationWindow().width, Kirigami.Units.gridUnit * 24) width: Math.min(applicationWindow().width, Kirigami.Units.gridUnit * 24)
title: i18nc("@title: dialog to switch between logged in accounts", "Switch Account") title: i18nc("@title: dialog to switch between logged in accounts", "Switch Account")

View File

@@ -8,33 +8,30 @@ import org.kde.kirigami as Kirigami
import org.kde.neochat import org.kde.neochat
Kirigami.Dialog { Kirigami.PromptDialog {
id: root id: root
required property var user required property var user
width: Math.min(Kirigami.Units.gridUnit * 24, QQC2.ApplicationWindow.window.width)
height: Kirigami.Units.gridUnit * 8
standardButtons: QQC2.Dialog.Close
title: i18nc("@title:dialog", "Start a chat") title: i18nc("@title:dialog", "Start a chat")
subtitle: i18n("Do you want to start a chat with %1?", root.user.displayName)
dialogType: Kirigami.PromptDialog.Warning
contentItem: QQC2.Label { onRejected: {
text: i18n("Do you want to start a chat with %1?", root.user.displayName) root.close();
textFormat: Text.PlainText
wrapMode: Text.Wrap
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
} }
customFooterActions: [ footer: QQC2.DialogButtonBox {
Kirigami.Action { standardButtons: QQC2.Dialog.Cancel
QQC2.Button {
text: i18nc("@action:button", "Start Chat") text: i18nc("@action:button", "Start Chat")
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
icon.name: "im-user" icon.name: "im-user"
onTriggered: { onClicked: {
root.user.requestDirectChat(); root.user.requestDirectChat();
root.close(); root.close();
} }
} }
] }
} }

View File

@@ -48,7 +48,6 @@ Delegates.RoundedItemDelegate {
TapHandler { TapHandler {
acceptedDevices: PointerDevice.TouchScreen acceptedDevices: PointerDevice.TouchScreen
onTapped: root.selected()
onLongPressed: root.contextMenuRequested() onLongPressed: root.contextMenuRequested()
} }

View File

@@ -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: root.room.forget(); onClicked: RoomManager.leaveRoom(root.room)
} }
} }
} }

View File

@@ -8,32 +8,30 @@ import org.kde.kirigami as Kirigami
import org.kde.neochat import org.kde.neochat
Kirigami.Dialog { Kirigami.PromptDialog {
id: root id: root
required property string url required property string url
width: Math.min(Kirigami.Units.gridUnit * 24, QQC2.ApplicationWindow.window.width)
height: Kirigami.Units.gridUnit * 8
leftPadding: Kirigami.Units.largeSpacing
rightPadding: Kirigami.Units.largeSpacing
title: i18nc("@title:dialog", "User Consent") title: i18nc("@title:dialog", "User Consent")
subtitle: i18nc("@info", "Your homeserver requires you to agree to its terms and conditions before being able to use it. Please click the button below to read them.")
dialogType: Kirigami.PromptDialog.Warning
contentItem: QQC2.Label { onRejected: {
text: i18nc("@info", "Your homeserver requires you to agree to its terms and conditions before being able to use it. Please click the button below to read them.") root.close();
wrapMode: Text.WordWrap
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
} }
customFooterActions: [
Kirigami.Action { footer: QQC2.DialogButtonBox {
standardButtons: QQC2.Dialog.Cancel
QQC2.Button {
text: i18nc("@action:button", "Open") text: i18nc("@action:button", "Open")
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
icon.name: "internet-services" icon.name: "internet-services"
onTriggered: { onClicked: {
UrlHelper.openUrl(root.url); UrlHelper.openUrl(root.url);
root.close(); root.close();
} }
} }
] }
} }

View File

@@ -9,36 +9,124 @@ import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.kirigamiaddons.labs.components as Components import org.kde.kirigamiaddons.labs.components as Components
import org.kde.neochat.libneochat import org.kde.neochat
Kirigami.Dialog { FormCard.FormCardPage {
id: root id: root
property string parentId property string parentId: ""
property bool isSpace: false
property bool showChildType: false
property bool showCreateChoice: false
required property NeoChatConnection connection required property NeoChatConnection connection
signal addChild(string childId, bool setChildParent, bool canonical) signal addChild(string childId, bool setChildParent, bool canonical)
signal newChild(string childName) signal newChild(string childName)
title: i18nc("@title", "Select Existing Room") title: isSpace ? i18nc("@title", "Create a Space") : i18nc("@title", "Create a Room")
implicitWidth: Kirigami.Units.gridUnit * 20
standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel
onAccepted: root.addChild(chosenRoomDelegate.roomId, existingOfficialCheck.checked, makeCanonicalCheck.checked); Component.onCompleted: roomNameField.forceActiveFocus()
Component.onCompleted: pickRoomDelegate.forceActiveFocus() FormCard.FormHeader {
title: root.isSpace ? i18n("New Space Information") : i18n("New Room Information")
}
FormCard.FormCard {
FormCard.FormComboBoxDelegate {
id: roomTypeCombo
property bool isInitialising: true
ColumnLayout { visible: root.showChildType
spacing: Kirigami.Units.largeSpacing
text: i18n("Select type")
model: ListModel {
id: roomTypeModel
}
textRole: "text"
valueRole: "isSpace"
Component.onCompleted: {
currentIndex = indexOfValue(root.isSpace);
roomTypeModel.append({
"text": i18n("Room"),
"isSpace": false
});
roomTypeModel.append({
"text": i18n("Space"),
"isSpace": true
});
roomTypeCombo.currentIndex = 0;
roomTypeCombo.isInitialising = false;
}
onCurrentValueChanged: {
if (!isInitialising) {
root.isSpace = currentValue;
}
}
}
FormCard.FormDelegateSeparator {
visible: root.showChildType
}
FormCard.FormTextFieldDelegate {
id: roomNameField
label: i18n("Name:")
onAccepted: if (roomNameField.text.length > 0) {
roomTopicField.forceActiveFocus();
}
}
FormCard.FormDelegateSeparator {}
FormCard.FormTextFieldDelegate {
id: roomTopicField
label: i18n("Topic:")
onAccepted: ok.clicked()
}
FormCard.FormDelegateSeparator {}
FormCard.FormCheckDelegate {
id: newOfficialCheck
visible: root.parentId.length > 0
text: i18nc("@option:check As in make the space from which this dialog was created an official parent.", "Make this parent official")
checked: true
}
FormCard.FormDelegateSeparator {
visible: root.parentId.length > 0
}
FormCard.FormButtonDelegate { FormCard.FormButtonDelegate {
id: pickRoomDelegate id: ok
text: root.isSpace ? i18nc("@action:button", "Create Space") : i18nc("@action:button", "Create Room")
visible: !chosenRoomDelegate.visible enabled: roomNameField.text.length > 0
text: i18nc("@action:button", "Pick Room")
onClicked: { onClicked: {
let dialog = pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat.libneochat', 'ExploreRoomsPage'), { if (root.isSpace) {
root.connection.createSpace(roomNameField.text, roomTopicField.text, root.parentId, newOfficialCheck.checked);
} else {
root.connection.createRoom(roomNameField.text, roomTopicField.text, root.parentId, newOfficialCheck.checked);
}
root.newChild(roomNameField.text);
root.closeDialog();
}
}
}
FormCard.FormHeader {
visible: root.showChildType
title: i18n("Select Existing Room")
}
FormCard.FormCard {
visible: root.showChildType
FormCard.FormButtonDelegate {
visible: !chosenRoomDelegate.visible
text: i18nc("@action:button", "Pick room")
onClicked: {
let dialog = pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
connection: root.connection connection: root.connection
}, { }, {
title: i18nc("@title", "Explore Rooms") title: i18nc("@title", "Explore Rooms")
@@ -139,15 +227,13 @@ Kirigami.Dialog {
} }
} }
FormCard.FormDelegateSeparator { FormCard.FormDelegateSeparator {}
below: existingOfficialCheck
}
FormCard.FormCheckDelegate { FormCard.FormCheckDelegate {
id: existingOfficialCheck id: existingOfficialCheck
visible: root.parentId.length > 0 visible: root.parentId.length > 0
text: i18nc("@option:check As in make the space from which this dialog was created an official parent.", "Make this parent official") text: i18nc("@option:check As in make the space from which this dialog was created an official parent.", "Make this parent official")
description: enabled ? i18nc("@info:description", "You have the required privilege level in the child to set this state") : i18n("You do not have a high enough privilege level in the child to set this state") description: enabled ? i18n("You have the required privilege level in the child to set this state") : i18n("You do not have a high enough privilege level in the child to set this state")
checked: enabled checked: enabled
enabled: { enabled: {
@@ -164,17 +250,26 @@ Kirigami.Dialog {
} }
FormCard.FormDelegateSeparator { FormCard.FormDelegateSeparator {
above: existingOfficialCheck visible: root.parentId.length > 0
below: makeCanonicalCheck
} }
FormCard.FormCheckDelegate { FormCard.FormCheckDelegate {
id: makeCanonicalCheck id: makeCanonicalCheck
text: i18nc("@option:check The canonical parent is the default one if a room has multiple parent spaces.", "Make this space the canonical parent") text: i18nc("@option:check The canonical parent is the default one if a room has multiple parent spaces.", "Make this space the canonical parent")
description: i18nc("@info:description", "The canonical parent is the default one if a room has multiple parent spaces.")
checked: enabled checked: enabled
enabled: existingOfficialCheck.enabled enabled: existingOfficialCheck.enabled
} }
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18nc("@action:button", "Ok")
enabled: chosenRoomDelegate.visible
onClicked: {
root.addChild(chosenRoomDelegate.roomId, existingOfficialCheck.checked, makeCanonicalCheck.checked);
root.closeDialog();
}
}
} }
} }

View File

@@ -8,7 +8,7 @@ import QtQuick.Layouts
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.labs.components as KirigamiComponents import org.kde.kirigamiaddons.labs.components as KirigamiComponents
import org.kde.neochat.libneochat import org.kde.neochat
ColumnLayout { ColumnLayout {
id: root id: root
@@ -18,8 +18,6 @@ ColumnLayout {
*/ */
required property NeoChatRoom room required property NeoChatRoom room
signal resolveResource(string idOrUri, string action)
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
spacing: 0 spacing: 0
@@ -35,7 +33,7 @@ ColumnLayout {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
onClicked: { onClicked: {
root.resolveResource(root.room.directChatRemoteMember.uri, "") RoomManager.resolveResource(root.room.directChatRemoteMember.uri)
} }
contentItem: KirigamiComponents.Avatar { contentItem: KirigamiComponents.Avatar {

View File

@@ -8,8 +8,6 @@ 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

View File

@@ -15,7 +15,7 @@ import org.kde.neochat.settings
Labs.MenuBar { Labs.MenuBar {
id: root id: root
required property NeoChatConnection connection property NeoChatConnection connection
Labs.Menu { Labs.Menu {
title: i18nc("menu", "NeoChat") title: i18nc("menu", "NeoChat")
@@ -38,31 +38,25 @@ Labs.MenuBar {
title: i18nc("menu", "File") title: i18nc("menu", "File")
Labs.MenuItem { Labs.MenuItem {
icon.name: "list-add-user" text: i18nc("menu", "Find your friends")
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: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UserSearchPage'), { onTriggered: pushReplaceLayer(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 {
icon.name: "system-users-symbolic" text: i18nc("menu", "New Group…")
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: {
pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'CreateRoomDialog'), { const dialog = createRoomDialog.createObject(root.overlay);
connection: root.connection dialog.open();
}, {
title: i18nc("@title", "Create a Room")
});
} }
} }
Labs.MenuItem { Labs.MenuItem {
icon.name: "compass-symbolic" text: i18nc("menu", "Browse Chats…")
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
@@ -83,8 +77,7 @@ Labs.MenuBar {
title: i18nc("menu", "View") title: i18nc("menu", "View")
Labs.MenuItem { Labs.MenuItem {
icon.name: "search-symbolic" 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")
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()
} }
} }
@@ -92,7 +85,6 @@ 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()
} }
@@ -101,12 +93,14 @@ Labs.MenuBar {
title: i18nc("menu", "Help") title: i18nc("menu", "Help")
Labs.MenuItem { Labs.MenuItem {
icon.name: "help-about-symbolic" text: i18nc("menu", "About Matrix")
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"))
} }

View File

@@ -1,10 +0,0 @@
// 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
}

View File

@@ -19,11 +19,6 @@ ColumnLayout {
*/ */
required property NeoChatRoom room required property NeoChatRoom room
/**
* @brief The canonical alias of the room, if it exists. Otherwise falls back to the first available alias.
*/
readonly property var roomAlias: room.aliases[0]
Layout.fillWidth: true Layout.fillWidth: true
RowLayout { RowLayout {
@@ -78,8 +73,8 @@ ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
font: Kirigami.Theme.smallFont font: Kirigami.Theme.smallFont
textFormat: TextEdit.PlainText textFormat: TextEdit.PlainText
visible: root.room && root.roomAlias visible: root.room && root.room.canonicalAlias
text: root.room && root.roomAlias ? root.roomAlias : "" text: root.room && root.room.canonicalAlias ? root.room.canonicalAlias : ""
color: Kirigami.Theme.disabledTextColor color: Kirigami.Theme.disabledTextColor
} }
} }

View File

@@ -16,7 +16,7 @@ ColumnLayout {
id: root id: root
required property NeoChatRoom currentRoom required property NeoChatRoom currentRoom
readonly property var invitingMember: currentRoom.qmlSafeMember(currentRoom.invitingUserId) readonly property var invitingMember: currentRoom.member(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: NeoChatConfig.hideImages ? undefined : root.invitingMember.avatarUrl source: 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: root.currentRoom.forget() onClicked: RoomManager.leaveRoom(root.currentRoom)
} }
} }
@@ -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: {
root.currentRoom.forget() RoomManager.leaveRoom(root.currentRoom);
root.currentRoom.connection.addToIgnoredUsers(root.currentRoom.invitingUserId); root.currentRoom.connection.addToIgnoredUsers(root.currentRoom.invitingUserId);
} }
} }

View File

@@ -12,7 +12,7 @@ import org.kde.prison
import org.kde.neochat import org.kde.neochat
Kirigami.Dialog { QQC2.Dialog {
id: root id: root
required property string room required property string room
@@ -23,7 +23,7 @@ Kirigami.Dialog {
topPadding: 0 topPadding: 0
bottomPadding: 0 bottomPadding: 0
standardButtons: Kirigami.Dialog.NoButton standardButtons: QQC2.Dialog.NoButton
width: Math.min(applicationWindow().width, Kirigami.Units.gridUnit * 24) width: Math.min(applicationWindow().width, Kirigami.Units.gridUnit * 24)
title: i18nc("@title:dialog", "Join Room") title: i18nc("@title:dialog", "Join Room")

View File

@@ -6,13 +6,12 @@ import QtLocation
import QtPositioning import QtPositioning
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.neochat
import org.kde.neochat.libneochat
Kirigami.Page { Kirigami.Page {
id: root id: root
required property NeoChatRoom room required property var room
title: i18nc("Locations on a map", "Locations") title: i18nc("Locations on a map", "Locations")

View File

@@ -41,7 +41,6 @@ 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
@@ -81,8 +80,9 @@ Kirigami.ApplicationWindow {
Loader { Loader {
active: Kirigami.Settings.hasPlatformMenuBar && !Kirigami.Settings.isMobile active: Kirigami.Settings.hasPlatformMenuBar && !Kirigami.Settings.isMobile
sourceComponent: GlobalMenu { sourceComponent: Qt.createComponent("org.kde.neochat", "GlobalMenu")
connection: root.connection onActiveChanged: if (active) {
item.connection = root.connection;
} }
} }
@@ -149,13 +149,9 @@ Kirigami.ApplicationWindow {
} }
function openRoomDrawer() { function openRoomDrawer() {
const page = pageStack.push(Qt.createComponent('org.kde.neochat', 'RoomDrawerPage'), { 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 {
@@ -165,18 +161,7 @@ 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")

View File

@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Window import QtQuick.Window
import QtQuick.Layouts import QtQuick.Layouts
@@ -10,7 +11,7 @@ import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.neochat import org.kde.neochat
Kirigami.Dialog { QQC2.Dialog {
id: root id: root
/** /**
@@ -31,35 +32,41 @@ Kirigami.Dialog {
topPadding: 0 topPadding: 0
bottomPadding: 0 bottomPadding: 0
standardButtons: Kirigami.Dialog.Cancel footer: QQC2.DialogButtonBox {
customFooterActions: [ QQC2.Button {
Kirigami.Action { QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
enabled: roomIdAliasText.isValidText enabled: roomIdAliasText.isValidText
text: i18n("OK") text: i18n("OK")
icon.name: "dialog-ok" icon.name: "dialog-ok"
onTriggered: {
// We don't necessarily have all the info so fill out the best we can.
let roomId = roomIdAliasText.isAlias() ? "" : roomIdAliasText.text;
let displayName = "";
let avatarUrl = "";
let alias = roomIdAliasText.isAlias() ? roomIdAliasText.text : "";
let topic = "";
let memberCount = -1;
let isJoined = false;
if (roomIdAliasText.room) {
roomId = roomIdAliasText.room.id;
displayName = roomIdAliasText.room.displayName;
avatarUrl = roomIdAliasText.room.avatarUrl.toString().length > 0 ? connection.makeMediaUrl(roomIdAliasText.room.avatarUrl) : "";
alias = roomIdAliasText.room.canonicalAlias;
topic = roomIdAliasText.room.topic;
memberCount = roomIdAliasText.room.joinedCount;
isJoined = true;
}
root.roomSelected(roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined);
root.close();
}
} }
] QQC2.Button {
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.RejectRole
text: i18n("Cancel")
icon.name: "dialog-cancel"
}
}
onAccepted: {
// We don't necessarily have all the info so fill out the best we can.
let roomId = roomIdAliasText.isAlias() ? "" : roomIdAliasText.text;
let displayName = "";
let avatarUrl = "";
let alias = roomIdAliasText.isAlias() ? roomIdAliasText.text : "";
let topic = "";
let memberCount = -1;
let isJoined = false;
if (roomIdAliasText.room) {
roomId = roomIdAliasText.room.id;
displayName = roomIdAliasText.room.displayName;
avatarUrl = roomIdAliasText.room.avatarUrl.toString().length > 0 ? connection.makeMediaUrl(roomIdAliasText.room.avatarUrl) : "";
alias = roomIdAliasText.room.canonicalAlias;
topic = roomIdAliasText.room.topic;
memberCount = roomIdAliasText.room.joinedCount;
isJoined = true;
}
root.roomSelected(roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined);
root.close();
}
contentItem: ColumnLayout { contentItem: ColumnLayout {
spacing: 0 spacing: 0

View File

@@ -10,7 +10,7 @@ import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.neochat import org.kde.neochat
Kirigami.Dialog { QQC2.Dialog {
id: root id: root
/** /**
@@ -31,19 +31,6 @@ Kirigami.Dialog {
topPadding: 0 topPadding: 0
bottomPadding: 0 bottomPadding: 0
standardButtons: Kirigami.Dialog.Cancel
customFooterActions: [
Kirigami.Action {
enabled: userIdText.isValidText
text: i18n("OK")
icon.name: "dialog-ok"
onTriggered: {
root.connection.requestDirectChat(userIdText.text);
root.accept();
}
}
]
contentItem: ColumnLayout { contentItem: ColumnLayout {
spacing: 0 spacing: 0
FormCard.FormTextFieldDelegate { FormCard.FormTextFieldDelegate {
@@ -75,6 +62,21 @@ Kirigami.Dialog {
} }
} }
footer: QQC2.DialogButtonBox {
standardButtons: QQC2.Dialog.Cancel
QQC2.Button {
enabled: userIdText.isValidText
text: i18n("OK")
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
icon.name: "dialog-ok"
onClicked: {
root.connection.requestDirectChat(userIdText.text);
root.accept();
}
}
}
onVisibleChanged: { onVisibleChanged: {
userIdText.forceActiveFocus(); userIdText.forceActiveFocus();
timer.restart(); timer.restart();

View File

@@ -139,7 +139,7 @@ Components.AlbumMaximizeComponent {
id: saveAsDialog id: saveAsDialog
Dialogs.FileDialog { Dialogs.FileDialog {
fileMode: Dialogs.FileDialog.SaveFile fileMode: Dialogs.FileDialog.SaveFile
currentFolder: NeoChatConfig.lastSaveDirectory.length > 0 ? NeoChatConfig.lastSaveDirectory : Core.StandardPaths.writableLocation(Core.StandardPaths.DownloadLocation) currentFolder: root.saveFolder
onAccepted: { onAccepted: {
NeoChatConfig.lastSaveDirectory = currentFolder; NeoChatConfig.lastSaveDirectory = currentFolder;
NeoChatConfig.save(); NeoChatConfig.save();

View File

@@ -15,27 +15,18 @@ import Quotient
import org.kde.neochat import org.kde.neochat
Kirigami.Dialog { QQC2.Dialog {
id: root id: root
required property NeoChatRoom room required property NeoChatRoom room
standardButtons: Kirigami.Dialog.Cancel title: i18nc("@title: create new poll in the room", "Create Poll")
customFooterActions: [
Kirigami.Action {
enabled: optionModel.allValuesSet && questionTextField.text.length > 0
text: i18nc("@action:button", "Send")
icon.name: "document-send"
onTriggered: {
root.room.postPoll(pollTypeCombo.currentValue, questionTextField.text, optionModel.values())
root.close()
}
}
]
width: Math.min(applicationWindow().width, Kirigami.Units.gridUnit * 24) width: Math.min(applicationWindow().width, Kirigami.Units.gridUnit * 24)
title: i18nc("@title: create new poll in the room", "Create Poll") leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
contentItem: ColumnLayout { contentItem: ColumnLayout {
spacing: 0 spacing: 0
@@ -153,4 +144,19 @@ Kirigami.Dialog {
onClicked: optionModel.append({optionText: ""}) onClicked: optionModel.append({optionText: ""})
} }
} }
footer: QQC2.DialogButtonBox {
standardButtons: QQC2.Dialog.Cancel
QQC2.Button {
enabled: optionModel.allValuesSet && questionTextField.text.length > 0
text: i18nc("@action:button", "Send")
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
icon.name: "document-send"
onClicked: {
root.room.postPoll(pollTypeCombo.currentValue, questionTextField.text, optionModel.values())
root.close()
}
}
}
} }

View File

@@ -28,14 +28,6 @@ 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
@@ -58,7 +50,6 @@ 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()

View File

@@ -10,13 +10,11 @@ import org.kde.kirigamiaddons.components
import org.kde.neochat import org.kde.neochat
Kirigami.Dialog { QQC2.Dialog {
id: root id: root
property var connection property var connection
parent: applicationWindow().overlay
leftPadding: 0 leftPadding: 0
rightPadding: 0 rightPadding: 0
topPadding: 0 topPadding: 0

View File

@@ -2,14 +2,15 @@
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts import QtQuick.Layouts
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.neochat.libneochat import org.kde.neochat
Kirigami.Dialog { QQC2.Dialog {
id: root id: root
required property NeoChatRoom parentRoom required property NeoChatRoom parentRoom
@@ -28,7 +29,7 @@ Kirigami.Dialog {
width: Math.min(applicationWindow().width, Kirigami.Units.gridUnit * 24) width: Math.min(applicationWindow().width, Kirigami.Units.gridUnit * 24)
standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel standardButtons: QQC2.Dialog.Ok | QQC2.Dialog.Cancel
onAccepted: parentRoom.removeChild(root.roomId, removeOfficalCheck.checked) onAccepted: parentRoom.removeChild(root.roomId, removeOfficalCheck.checked)

View File

@@ -9,19 +9,14 @@ import QtQuick.Layouts
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.kitemmodels import org.kde.kitemmodels
import org.kde.neochat.libneochat import org.kde.neochat
import org.kde.neochat.timeline as Timeline import org.kde.neochat.settings
import org.kde.neochat.settings as Settings
Kirigami.OverlayDrawer { Kirigami.OverlayDrawer {
id: root id: root
required property NeoChatRoom room readonly property NeoChatRoom room: RoomManager.currentRoom
required property NeoChatConnection connection required property NeoChatConnection connection
required property UserListModel userListModel
required property Timeline.MediaMessageFilterModel mediaMessageFilterModel
signal resolveResource(string idOrUri, string action)
width: actualWidth width: actualWidth
interactive: modal interactive: modal
@@ -29,12 +24,11 @@ Kirigami.OverlayDrawer {
readonly property int minWidth: Kirigami.Units.gridUnit * 15 readonly property int minWidth: Kirigami.Units.gridUnit * 15
readonly property int maxWidth: Kirigami.Units.gridUnit * 25 readonly property int maxWidth: Kirigami.Units.gridUnit * 25
readonly property int defaultWidth: Kirigami.Units.gridUnit * 20 readonly property int defaultWidth: Kirigami.Units.gridUnit * 20
property int roomDrawerWidth
property int actualWidth: { property int actualWidth: {
if (root.roomDrawerWidth === -1) { if (NeoChatConfig.roomDrawerWidth === -1) {
return Kirigami.Units.gridUnit * 20; return Kirigami.Units.gridUnit * 20;
} else { } else {
return root.roomDrawerWidth; return NeoChatConfig.roomDrawerWidth;
} }
} }
@@ -52,7 +46,8 @@ Kirigami.OverlayDrawer {
visible: true visible: true
onPressed: _lastX = mapToGlobal(mouseX, mouseY).x onPressed: _lastX = mapToGlobal(mouseX, mouseY).x
onReleased: { onReleased: {
root.roomDrawerWidth = root.actualWidth; NeoChatConfig.roomDrawerWidth = root.actualWidth;
NeoChatConfig.save();
} }
property real _lastX: -1 property real _lastX: -1
@@ -61,9 +56,9 @@ Kirigami.OverlayDrawer {
return; return;
} }
if (Qt.application.layoutDirection === Qt.RightToLeft) { if (Qt.application.layoutDirection === Qt.RightToLeft) {
root.actualWidth = Math.min(root.maxWidth, Math.max(root.minWidth, root.roomDrawerWidth - _lastX + mapToGlobal(mouseX, mouseY).x)); root.actualWidth = Math.min(root.maxWidth, Math.max(root.minWidth, NeoChatConfig.roomDrawerWidth - _lastX + mapToGlobal(mouseX, mouseY).x));
} else { } else {
root.actualWidth = Math.min(root.maxWidth, Math.max(root.minWidth, root.roomDrawerWidth + _lastX - mapToGlobal(mouseX, mouseY).x)); root.actualWidth = Math.min(root.maxWidth, Math.max(root.minWidth, NeoChatConfig.roomDrawerWidth + _lastX - mapToGlobal(mouseX, mouseY).x));
} }
} }
} }
@@ -126,7 +121,7 @@ Kirigami.OverlayDrawer {
QQC2.ToolTip.visible: hovered QQC2.ToolTip.visible: hovered
onClicked: { onClicked: {
Settings.RoomSettingsView.openRoomSettings(root.room, Settings.RoomSettingsView.Room); RoomSettingsView.openRoomSettings(root.room, RoomSettingsView.Room);
} }
} }
} }
@@ -143,17 +138,15 @@ Kirigami.OverlayDrawer {
id: roomInformation id: roomInformation
RoomInformation { RoomInformation {
room: root.room room: root.room
userListModel: root.userListModel connection: root.connection
onResolveResource: (idOrUri, action) => root.resolveResource(idOrUri, action)
} }
} }
Component { Component {
id: roomMedia id: roomMedia
RoomMedia { RoomMedia {
room: root.room currentRoom: root.room
mediaMessageFilterModel: root.mediaMessageFilterModel connection: root.connection
} }
} }

View File

@@ -7,8 +7,7 @@ import QtQuick.Layouts
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.kitemmodels import org.kde.kitemmodels
import org.kde.neochat.libneochat import org.kde.neochat
import org.kde.neochat.timeline as Timeline
/** /**
* @brief Page for holding a room drawer component. * @brief Page for holding a room drawer component.
@@ -25,12 +24,8 @@ Kirigami.Page {
/** /**
* @brief The current room that user is viewing. * @brief The current room that user is viewing.
*/ */
required property NeoChatRoom room readonly property NeoChatRoom room: RoomManager.currentRoom
required property NeoChatConnection connection required property NeoChatConnection connection
required property UserListModel userListModel
required property Timeline.MediaMessageFilterModel mediaMessageFilterModel
signal resolveResource(string idOrUri, string action)
title: drawerItemLoader.item ? drawerItemLoader.item.title : "" title: drawerItemLoader.item ? drawerItemLoader.item.title : ""
@@ -66,17 +61,15 @@ Kirigami.Page {
id: roomInformation id: roomInformation
RoomInformation { RoomInformation {
room: root.room room: root.room
userListModel: root.userListModel connection: root.connection
onResolveResource: (idOrUri, action) => root.resolveResource(idOrUri, action)
} }
} }
Component { Component {
id: roomMedia id: roomMedia
RoomMedia { RoomMedia {
room: root.room currentRoom: root.room
mediaMessageFilterModel: root.mediaMessageFilterModel connection: root.connection
} }
} }

View File

@@ -12,7 +12,7 @@ import org.kde.kirigamiaddons.delegates as Delegates
import org.kde.kirigamiaddons.labs.components as KirigamiComponents import org.kde.kirigamiaddons.labs.components as KirigamiComponents
import org.kde.kitemmodels import org.kde.kitemmodels
import org.kde.neochat.libneochat import org.kde.neochat
/** /**
* @brief Component for visualising the room information. * @brief Component for visualising the room information.
@@ -34,15 +34,13 @@ QQC2.ScrollView {
*/ */
required property NeoChatRoom room required property NeoChatRoom room
required property UserListModel userListModel required property NeoChatConnection connection
/** /**
* @brief The title that should be displayed for this component if available. * @brief The title that should be displayed for this component if available.
*/ */
readonly property string title: root.room.isSpace ? i18nc("@action:title", "Space Members") : i18nc("@action:title", "Room Information") readonly property string title: root.room.isSpace ? i18nc("@action:title", "Space Members") : i18nc("@action:title", "Room Information")
signal resolveResource(string idOrUri, string action)
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890) // HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
@@ -218,7 +216,7 @@ QQC2.ScrollView {
UserFilterModel { UserFilterModel {
id: userFilterModel id: userFilterModel
sourceModel: root.userListModel sourceModel: RoomManager.userListModel
allowEmpty: true allowEmpty: true
} }
@@ -251,7 +249,7 @@ QQC2.ScrollView {
KeyNavigation.backtab: index === 0 ? userList.headerItem.userListSearchField : null KeyNavigation.backtab: index === 0 ? userList.headerItem.userListSearchField : null
onClicked: { onClicked: {
root.resolveResource(userDelegate.userId, "mention"); RoomManager.resolveResource(userDelegate.userId, "mention");
} }
contentItem: RowLayout { contentItem: RowLayout {
@@ -288,8 +286,6 @@ QQC2.ScrollView {
id: directChatDrawerHeader id: directChatDrawerHeader
DirectChatDrawerHeader { DirectChatDrawerHeader {
room: root.room room: root.room
onResolveResource: (idOrUri, action) => root.resolveResource(idOrUri, action)
} }
} }

View File

@@ -6,8 +6,8 @@ import QtQuick.Controls as QQC2
import QtQuick.Layouts import QtQuick.Layouts
import Qt.labs.qmlmodels import Qt.labs.qmlmodels
import org.kde.neochat.libneochat import org.kde.neochat
import org.kde.neochat.timeline as Timeline import org.kde.neochat.timeline
/** /**
* @brief Component for visualising the loaded media items in the room. * @brief Component for visualising the loaded media items in the room.
@@ -31,9 +31,9 @@ QQC2.ScrollView {
/** /**
* @brief The current room that user is viewing. * @brief The current room that user is viewing.
*/ */
required property NeoChatRoom room required property NeoChatRoom currentRoom
required property Timeline.MediaMessageFilterModel mediaMessageFilterModel required property NeoChatConnection connection
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890) // HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
@@ -42,26 +42,26 @@ QQC2.ScrollView {
clip: true clip: true
verticalLayoutDirection: ListView.BottomToTop verticalLayoutDirection: ListView.BottomToTop
model: root.mediaMessageFilterModel model: RoomManager.mediaMessageFilterModel
delegate: DelegateChooser { delegate: DelegateChooser {
role: "type" role: "type"
DelegateChoice { DelegateChoice {
roleValue: Timeline.MediaMessageFilterModel.Image roleValue: MediaMessageFilterModel.Image
delegate: Timeline.MessageDelegate { delegate: MessageDelegate {
alwaysFillWidth: true alwaysFillWidth: true
cardBackground: false cardBackground: false
room: root.room room: root.currentRoom
} }
} }
DelegateChoice { DelegateChoice {
roleValue: Timeline.MediaMessageFilterModel.Video roleValue: MediaMessageFilterModel.Video
delegate: Timeline.MessageDelegate { delegate: MessageDelegate {
alwaysFillWidth: true alwaysFillWidth: true
cardBackground: false cardBackground: false
room: root.room room: root.currentRoom
} }
} }
} }

View File

@@ -11,14 +11,15 @@ 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.
* @brief The NeoChatRoom the delegate is being displayed in. property NeoChatRoom currentRoom: RoomManager.currentRoom
*/
readonly property NeoChatRoom currentRoom: RoomManager.currentRoom required property NeoChatConnection connection
/** /**
* @brief The TimelineModel to use. * @brief The TimelineModel to use.
@@ -58,6 +59,11 @@ 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
@@ -80,9 +86,9 @@ Kirigami.Page {
} }
Connections { Connections {
target: root.currentRoom.connection target: root.connection
function onIsOnlineChanged() { function onIsOnlineChanged() {
if (!root.currentRoom.connection.isOnline) { if (!root.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;
@@ -103,15 +109,18 @@ Kirigami.Page {
Loader { Loader {
id: timelineViewLoader id: timelineViewLoader
anchors.fill: parent anchors.fill: parent
active: root.currentRoom && !root.currentRoom.isInvite && !root.currentRoom.isSpace active: root.currentRoom && !root.currentRoom.isInvite && !root.loading && !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
compactLayout: NeoChatConfig.compactLayout onFocusChatBar: {
fileDropEnabled: !Controller.isFlatpak if (chatBarLoader.item) {
markReadCondition: NeoChatConfig.markReadCondition chatBarLoader.item.forceActiveFocus();
}
}
} }
} }
@@ -128,9 +137,7 @@ 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 {
@@ -143,6 +150,14 @@ 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
@@ -157,7 +172,12 @@ Kirigami.Page {
id: chatBar id: chatBar
width: parent.width width: parent.width
currentRoom: root.currentRoom currentRoom: root.currentRoom
connection: root.currentRoom.connection connection: root.connection
onMessageSent: {
if (!timelineViewLoader.item.atYEnd) {
timelineViewLoader.item.goToLastMessage();
}
}
} }
} }
@@ -174,8 +194,21 @@ 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.currentRoom.connection target: root.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);
@@ -231,7 +264,6 @@ Kirigami.Page {
plainText: plainText, plainText: plainText,
mimeType: mimeType, mimeType: mimeType,
progressInfo: progressInfo, progressInfo: progressInfo,
messageComponentType: messageComponentType,
}); });
contextMenu.popup(); contextMenu.popup();
} }
@@ -261,7 +293,7 @@ Kirigami.Page {
id: messageDelegateContextMenu id: messageDelegateContextMenu
MessageDelegateContextMenu { MessageDelegateContextMenu {
room: root.currentRoom room: root.currentRoom
connection: root.currentRoom.connection connection: root.connection
} }
} }
@@ -269,7 +301,7 @@ Kirigami.Page {
id: fileDelegateContextMenu id: fileDelegateContextMenu
FileDelegateContextMenu { FileDelegateContextMenu {
room: root.currentRoom room: root.currentRoom
connection: root.currentRoom.connection connection: root.connection
} }
} }

Some files were not shown because too many files have changed in this diff Show More