Compare commits

..

2 Commits

Author SHA1 Message Date
Tobias Fella
3a0916866c Work 2025-08-19 15:50:24 +02:00
Tobias Fella
0f5955ae34 Implement call ringing 2025-08-19 13:21:10 +02:00
276 changed files with 47515 additions and 83466 deletions

View File

@@ -1,2 +1,2 @@
[General] [General]
disableUnqualifiedAccess = "i18nc,xi18nc,i18ncp,i18n" disableUnqualifiedAccess = "i18nc,xi18nc,i18ncp"

View File

@@ -44,22 +44,6 @@
} }
] ]
}, },
{
"name": "opencv",
"config-opts": [
"-DBUILD_TESTS=OFF",
"-DWITH_GTK=OFF",
"-DBUILD_LIST=core,imgproc"
],
"buildsystem": "cmake-ninja",
"sources": [
{
"type": "git",
"url": "https://github.com/opencv/opencv"
}
],
"builddir": true
},
{ {
"name": "kquickimageeditor", "name": "kquickimageeditor",
"config-opts": [ "config-opts": [
@@ -147,7 +131,7 @@
{ {
"type": "git", "type": "git",
"url": "https://github.com/quotient-im/libQuotient.git", "url": "https://github.com/quotient-im/libQuotient.git",
"branch": "0.9.6.1", "branch": "dev",
"disable-submodules": true "disable-submodules": true
} }
], ],
@@ -177,15 +161,11 @@
"name": "kunifiedpush", "name": "kunifiedpush",
"buildsystem": "cmake-ninja", "buildsystem": "cmake-ninja",
"builddir": true, "builddir": true,
"config-opts": [
"-DENABLE_TESTING=OFF",
"-DKUNIFIEDPUSH_CLIENT_ONLY=ON"
],
"sources": [ "sources": [
{ {
"type": "archive", "type": "archive",
"url": "https://download.kde.org/stable/release-service/25.08.0/src/kunifiedpush-25.08.0.tar.xz", "url": "https://download.kde.org/stable/release-service/25.04.3/src/kunifiedpush-25.04.3.tar.xz",
"sha256": "846db6ffc7d93f6afea7ce0d5a9f10b52792157ceb593856542279f4197f3518", "sha256": "a16ffe4117b14baa02f3b8ae7de9e509a17359c1b67dcd851aef4f3c3661a1df",
"x-checker-data": { "x-checker-data": {
"type": "anitya", "type": "anitya",
"project-id": 8763, "project-id": 8763,
@@ -206,6 +186,14 @@
{ {
"type": "dir", "type": "dir",
"path": "." "path": "."
},
{
"type": "patch",
"path": "patches/0001-Revert-Bump-KF6-dependency-version.patch"
},
{
"type": "patch",
"path": "patches/0001-Revert-Use-new-Kirigami-builtin-column-resize-handle.patch"
} }
] ]
} }

View File

@@ -10,11 +10,11 @@ Dependencies:
'frameworks/ki18n': '@latest-kf6' 'frameworks/ki18n': '@latest-kf6'
'frameworks/kconfig': '@latest-kf6' 'frameworks/kconfig': '@latest-kf6'
'frameworks/syntax-highlighting': '@latest-kf6' 'frameworks/syntax-highlighting': '@latest-kf6'
'frameworks/kiconthemes': '@latest-kf6'
'frameworks/kitemmodels': '@latest-kf6' 'frameworks/kitemmodels': '@latest-kf6'
'frameworks/kquickcharts': '@latest-kf6' 'frameworks/kquickcharts': '@latest-kf6'
'frameworks/knotifications': '@latest-kf6' 'frameworks/knotifications': '@latest-kf6'
'frameworks/kcolorscheme': '@latest-kf6' 'frameworks/kcolorscheme': '@latest-kf6'
'frameworks/kiconthemes': '@latest-kf6'
'libraries/kquickimageeditor': '@latest-kf6' 'libraries/kquickimageeditor': '@latest-kf6'
'frameworks/sonnet': '@latest-kf6' 'frameworks/sonnet': '@latest-kf6'
'frameworks/prison': '@latest-kf6' 'frameworks/prison': '@latest-kf6'
@@ -29,6 +29,7 @@ Dependencies:
'frameworks/kio': '@latest-kf6' 'frameworks/kio': '@latest-kf6'
'frameworks/kwindowsystem': '@latest-kf6' 'frameworks/kwindowsystem': '@latest-kf6'
'frameworks/kstatusnotifieritem': '@latest-kf6' 'frameworks/kstatusnotifieritem': '@latest-kf6'
'frameworks/kcrash': '@latest-kf6'
- 'on': ['Linux', 'FreeBSD'] - 'on': ['Linux', 'FreeBSD']
'require': 'require':
'frameworks/kdbusaddons': '@latest-kf6' 'frameworks/kdbusaddons': '@latest-kf6'
@@ -42,4 +43,3 @@ Dependencies:
Options: Options:
per-test-timeout: 90 per-test-timeout: 90
require-passing-tests-on: ['Linux', 'Android', 'FreeBSD', 'Windows'] require-passing-tests-on: ['Linux', 'Android', 'FreeBSD', 'Windows']
run-qmllint: True

View File

@@ -8,14 +8,14 @@ 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 "12") set(RELEASE_SERVICE_VERSION_MINOR "11")
set(RELEASE_SERVICE_VERSION_MICRO "3") 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})
set(KF_MIN_VERSION "6.17") set(KF_MIN_VERSION "6.17")
set(QT_MIN_VERSION "6.9") set(QT_MIN_VERSION "6.5")
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE) find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)
@@ -24,7 +24,7 @@ set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake)
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(KDE_COMPILERSETTINGS_LEVEL 6.17) set(KDE_COMPILERSETTINGS_LEVEL 6.0)
include(FeatureSummary) include(FeatureSummary)
include(ECMSetupVersion) include(ECMSetupVersion)
@@ -39,7 +39,6 @@ include(ECMCheckOutboundLicense)
include(ECMQtDeclareLoggingCategory) include(ECMQtDeclareLoggingCategory)
include(ECMAddAndroidApk) include(ECMAddAndroidApk)
include(ECMQmlModule) include(ECMQmlModule)
include(ECMDeprecationSettings)
include(GenerateExportHeader) include(GenerateExportHeader)
include(ECMGenerateHeaders) include(ECMGenerateHeaders)
if (NOT ANDROID) if (NOT ANDROID)
@@ -52,8 +51,6 @@ endif()
set(QUOTIENT_FORCE_NAMESPACED_INCLUDES TRUE) set(QUOTIENT_FORCE_NAMESPACED_INCLUDES TRUE)
ecm_set_disabled_deprecation_versions(Qt 6.9.0 KF 6.17.0)
ecm_setup_version(${PROJECT_VERSION} ecm_setup_version(${PROJECT_VERSION}
VARIABLE_PREFIX NEOCHAT VARIABLE_PREFIX NEOCHAT
VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/neochat-version.h VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/neochat-version.h
@@ -69,7 +66,7 @@ if (QT_KNOWN_POLICY_QTP0004)
qt_policy(SET QTP0004 NEW) qt_policy(SET QTP0004 NEW)
endif () endif ()
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami I18n Notifications Config CoreAddons Sonnet ItemModels ColorScheme IconThemes) find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami I18n Notifications Config CoreAddons Sonnet ItemModels IconThemes ColorScheme)
set_package_properties(KF6 PROPERTIES set_package_properties(KF6 PROPERTIES
TYPE REQUIRED TYPE REQUIRED
PURPOSE "Basic application components" PURPOSE "Basic application components"
@@ -78,7 +75,7 @@ set_package_properties(KF6Kirigami PROPERTIES
TYPE REQUIRED TYPE REQUIRED
PURPOSE "Kirigami application UI framework" PURPOSE "Kirigami application UI framework"
) )
find_package(KF6KirigamiAddons 1.10.0 REQUIRED) find_package(KF6KirigamiAddons 1.6.0 REQUIRED)
if (UNIX AND NOT APPLE AND NOT ANDROID AND NOT NEOCHAT_FLATPAK AND NOT NEOCHAT_APPIMAGE) if (UNIX AND NOT APPLE AND NOT ANDROID AND NOT NEOCHAT_FLATPAK AND NOT NEOCHAT_APPIMAGE)
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS Purpose) find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS Purpose)
@@ -92,7 +89,7 @@ if(ANDROID)
) )
else() else()
find_package(Qt6 ${QT_MIN_VERSION} COMPONENTS Widgets) find_package(Qt6 ${QT_MIN_VERSION} COMPONENTS Widgets)
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle KIO WindowSystem StatusNotifierItem) find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle KIO WindowSystem StatusNotifierItem Crash)
find_package(KF6SyntaxHighlighting ${KF_MIN_VERSION} REQUIRED) find_package(KF6SyntaxHighlighting ${KF_MIN_VERSION} REQUIRED)
set_package_properties(KF6QQC2DesktopStyle PROPERTIES set_package_properties(KF6QQC2DesktopStyle PROPERTIES
TYPE RUNTIME TYPE RUNTIME
@@ -110,7 +107,7 @@ if (NOT ANDROID AND NOT WIN32 AND NOT APPLE AND NOT HAIKU)
find_package(KF6DBusAddons ${KF_MIN_VERSION} REQUIRED) find_package(KF6DBusAddons ${KF_MIN_VERSION} REQUIRED)
endif() endif()
find_package(QuotientQt6 0.9.3) find_package(QuotientQt6 0.9.1)
set_package_properties(QuotientQt6 PROPERTIES set_package_properties(QuotientQt6 PROPERTIES
TYPE REQUIRED TYPE REQUIRED
DESCRIPTION "Qt wrapper around Matrix API" DESCRIPTION "Qt wrapper around Matrix API"

View File

@@ -93,12 +93,6 @@ ecm_add_test(
TEST_NAME actionstest TEST_NAME actionstest
) )
ecm_add_test(
servernoticestest.cpp
LINK_LIBRARIES neochat Qt::Test neochat_server
TEST_NAME servernoticestest
)
ecm_add_test( ecm_add_test(
roommanagertest.cpp roommanagertest.cpp
LINK_LIBRARIES neochat Qt::Test neochat_server LINK_LIBRARIES neochat Qt::Test neochat_server

View File

@@ -63,7 +63,7 @@ void ActionsTest::testActions_data()
QTest::addColumn<std::optional<QString>>("resultText"); QTest::addColumn<std::optional<QString>>("resultText");
QTest::addColumn<std::optional<Quotient::RoomMessageEvent::MsgType>>("type"); QTest::addColumn<std::optional<Quotient::RoomMessageEvent::MsgType>>("type");
QTest::newRow("shrug") << u"/shrug Hello"_s << std::make_optional(u"¯\\\\\\_(ツ)\\_/¯ Hello"_s) QTest::newRow("shrug") << u"/shrug Hello"_s << std::make_optional(u"¯\\\\_(ツ)_/¯ Hello"_s)
<< std::make_optional(Quotient::RoomMessageEvent::MsgType::Text); << std::make_optional(Quotient::RoomMessageEvent::MsgType::Text);
QTest::newRow("lenny") << u"/lenny Hello"_s << std::make_optional(u"( ͡° ͜ʖ ͡°) Hello"_s) << std::make_optional(Quotient::RoomMessageEvent::MsgType::Text); QTest::newRow("lenny") << u"/lenny Hello"_s << std::make_optional(u"( ͡° ͜ʖ ͡°) Hello"_s) << std::make_optional(Quotient::RoomMessageEvent::MsgType::Text);
QTest::newRow("tableflip") << u"/tableflip Hello"_s << std::make_optional(u"(╯°□°)╯︵ ┻━┻ Hello"_s) QTest::newRow("tableflip") << u"/tableflip Hello"_s << std::make_optional(u"(╯°□°)╯︵ ┻━┻ Hello"_s)

View File

@@ -43,13 +43,13 @@ void MessageContentModelTest::missingEvent()
QCOMPARE(model1.rowCount(), 1); QCOMPARE(model1.rowCount(), 1);
QCOMPARE(model1.data(model1.index(0), MessageContentModel::ComponentTypeRole), MessageComponentType::Loading); QCOMPARE(model1.data(model1.index(0), MessageContentModel::ComponentTypeRole), MessageComponentType::Loading);
QCOMPARE(model1.data(model1.index(0), MessageContentModel::DisplayRole), u"Loading"_s); QCOMPARE(model1.data(model1.index(0), MessageContentModel::DisplayRole), u"Loading"_s);
auto model2 = EventMessageContentModel(room, u"$153456789:example.org"_s, true); auto model2 = EventMessageContentModel(room, u"$153456789:example.org"_s, true);
QCOMPARE(model2.rowCount(), 1); QCOMPARE(model2.rowCount(), 1);
QCOMPARE(model2.data(model2.index(0), MessageContentModel::ComponentTypeRole), MessageComponentType::Loading); QCOMPARE(model2.data(model2.index(0), MessageContentModel::ComponentTypeRole), MessageComponentType::Loading);
QCOMPARE(model2.data(model2.index(0), MessageContentModel::DisplayRole), u"Loading reply"_s); QCOMPARE(model2.data(model2.index(0), MessageContentModel::DisplayRole), u"Loading reply"_s);
room->syncNewEvents(u"test-min-sync.json"_s); room->syncNewEvents(u"test-min-sync.json"_s);
QCOMPARE(model1.rowCount(), 2); QCOMPARE(model1.rowCount(), 2);

View File

@@ -28,7 +28,6 @@ private:
private Q_SLOTS: private Q_SLOTS:
void initTestCase(); void initTestCase();
void testMaximizeMedia(); void testMaximizeMedia();
void testResolveMatrixLinks();
}; };
void RoomManagerTest::initTestCase() void RoomManagerTest::initTestCase()
@@ -129,13 +128,5 @@ void RoomManagerTest::testMaximizeMedia()
QVERIFY(spy[1][0] == 0); QVERIFY(spy[1][0] == 0);
} }
void RoomManagerTest::testResolveMatrixLinks()
{
// Test if resolving a non-joined room will bring up the confirmation dialog.
const QSignalSpy askToJoinSpy(&RoomManager::instance(), &RoomManager::askJoinRoom);
RoomManager::instance().resolveResource(QStringLiteral("matrix:r/testbuild:matrix.org"), QStringLiteral("join"));
QTRY_COMPARE(askToJoinSpy.size(), 1);
}
QTEST_MAIN(RoomManagerTest) QTEST_MAIN(RoomManagerTest)
#include "roommanagertest.moc" #include "roommanagertest.moc"

View File

@@ -4,12 +4,15 @@
#include "server.h" #include "server.h"
#include <QFile> #include <QFile>
#include <QHttpServer>
#include <QHttpServerResponder> #include <QHttpServerResponder>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply> #include <QNetworkReply>
#include <QSslCertificate> #include <QSslCertificate>
#include <QSslKey> #include <QSslKey>
#include <QSslServer>
#include <QUuid> #include <QUuid>
#include <Quotient/networkaccessmanager.h> #include <Quotient/networkaccessmanager.h>
@@ -106,20 +109,113 @@ void Server::start()
m_server.route(u"/_matrix/client/v3/rooms/<arg>/invite"_s, m_server.route(u"/_matrix/client/v3/rooms/<arg>/invite"_s,
QHttpServerRequest::Method::Post, QHttpServerRequest::Method::Post,
[this](const QString &roomId, QHttpServerResponder &responder, const QHttpServerRequest &request) { [this](const QString &roomId, QHttpServerResponder &responder, const QHttpServerRequest &request) {
Changes changes; m_invitedUsers[roomId] += QJsonDocument::fromJson(request.body()).object()[u"user_id"_s].toString();
changes.invitations += Changes::InviteUser{
.userId = QJsonDocument::fromJson(request.body()).object()[u"user_id"_s].toString(),
.roomId = roomId,
};
m_state += changes;
responder.write(QJsonDocument(QJsonObject{}), QHttpServerResponder::StatusCode::Ok); responder.write(QJsonDocument(QJsonObject{}), QHttpServerResponder::StatusCode::Ok);
}); });
m_server.route(u"/_matrix/client/r0/sync"_s, QHttpServerRequest::Method::Get, this, &Server::sync); m_server.route(u"/_matrix/client/r0/sync"_s, QHttpServerRequest::Method::Get, [this](QHttpServerResponder &responder) {
QMap<QString, QJsonArray> stateEvents;
QMap<QString, QJsonArray> roomAccountData;
for (const auto &roomData : m_roomsToCreate) {
stateEvents[roomData.id] += 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, roomData.id},
{u"sender"_s, roomData.members[0]},
{u"state_key"_s, QString()},
{u"type"_s, u"m.room.create"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
for (const auto &member : roomData.members) {
stateEvents[roomData.id] += 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, roomData.id},
{u"sender"_s, member},
{u"state_key"_s, member},
{u"type"_s, u"m.room.member"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
}
QJsonObject tags;
for (const auto &tag : roomData.tags) {
tags[tag] = QJsonObject();
}
roomAccountData[roomData.id] += QJsonObject{{u"type"_s, u"m.tag"_s}, {u"content"_s, QJsonObject{{u"tags"_s, tags}}}};
}
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;
auto keys = stateEvents.keys() + m_events.keys();
for (const auto &roomId : QSet(keys.begin(), keys.end())) {
rooms[roomId] = QJsonObject{
{u"state"_s, QJsonObject{{u"events"_s, stateEvents[roomId]}}},
{u"account_data"_s, QJsonObject{{u"events"_s, roomAccountData[roomId]}}},
{u"timeline"_s, QJsonObject{{u"events"_s, m_events[roomId]}}},
};
}
m_events.clear();
auto json = QJsonObject{{u"rooms"_s, QJsonObject{{u"join"_s, rooms}}}};
responder.write(QJsonDocument(json), QHttpServerResponder::StatusCode::Ok);
});
QSslConfiguration config; QSslConfiguration config;
QFile key(QStringLiteral(DATA_DIR) + u"/localhost.key"_s); QFile key(QStringLiteral(DATA_DIR) + u"/localhost.key"_s);
void(key.open(QFile::ReadOnly)); key.open(QFile::ReadOnly);
config.setPrivateKey(QSslKey(&key, QSsl::Rsa)); config.setPrivateKey(QSslKey(&key, QSsl::Rsa));
config.setLocalCertificate(QSslCertificate::fromPath(QStringLiteral(DATA_DIR) + u"/localhost.crt"_s).front()); config.setLocalCertificate(QSslCertificate::fromPath(QStringLiteral(DATA_DIR) + u"/localhost.crt"_s).front());
m_sslServer.setSslConfiguration(config); m_sslServer.setSslConfiguration(config);
@@ -133,239 +229,46 @@ void Server::start()
QString Server::createRoom(const QString &matrixId) QString Server::createRoom(const QString &matrixId)
{ {
const auto roomId = generateRoomId(); auto roomId = generateRoomId();
Changes changes; m_roomsToCreate += RoomData{
changes.newRooms += Changes::NewRoom{ .members = {matrixId},
.initialMembers = {matrixId}, .id = roomId,
.roomId = {roomId},
.tags = {}, .tags = {},
}; };
m_state += changes;
return roomId; return roomId;
} }
void Server::inviteUser(const QString &roomId, const QString &matrixId) void Server::inviteUser(const QString &roomId, const QString &matrixId)
{ {
Changes changes; m_invitedUsers[roomId] += matrixId;
changes.invitations += Changes::InviteUser{
.userId = matrixId,
.roomId = roomId,
};
m_state += changes;
} }
void Server::banUser(const QString &roomId, const QString &matrixId) void Server::banUser(const QString &roomId, const QString &matrixId)
{ {
Changes changes; m_bannedUsers[roomId] += matrixId;
changes.bans += Changes::BanUser{
.userId = matrixId,
.roomId = roomId,
};
m_state += changes;
} }
void Server::joinUser(const QString &roomId, const QString &matrixId) void Server::joinUser(const QString &roomId, const QString &matrixId)
{ {
Changes changes; m_joinedUsers[roomId] += matrixId;
changes.joins += Changes::JoinUser{
.userId = matrixId,
.roomId = roomId,
};
m_state += changes;
} }
QString Server::createServerNoticesRoom(const QString &matrixId) QString Server::createServerNoticesRoom(const QString &matrixId)
{ {
const auto roomId = generateRoomId(); auto roomId = createRoom(matrixId);
Changes changes; m_roomsToCreate.last().tags = {u"m.server_notice"_s};
changes.newRooms += Changes::NewRoom{
.initialMembers = {matrixId},
.roomId = {roomId},
.tags = {u"m.server_notice"_s},
};
m_state += changes;
return roomId; return roomId;
} }
QString Server::sendEvent(const QString &roomId, const QString &eventType, const QJsonObject &content) QString Server::sendEvent(const QString &roomId, const QString &eventType, const QJsonObject &content)
{ {
Changes changes;
const auto eventId = generateEventId(); const auto eventId = generateEventId();
changes.events += Changes::Event{ m_events[roomId] += QJsonObject{
.fullJson = QJsonObject{{u"type"_s, eventType}, {u"type"_s, eventType},
{u"content"_s, content}, {u"content"_s, content},
{u"sender"_s, u"@foo:server.com"_s}, {u"sender"_s, u"@foo:server.com"_s},
{u"event_id"_s, eventId}, {u"event_id"_s, eventId},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()}, {u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, roomId}},
}; };
m_state += changes;
return eventId; return eventId;
} }
void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &responder)
{
QJsonObject joinRooms;
auto token = request.query().queryItemValue(u"since"_s).toInt();
for (const auto &change : m_state.mid(token)) {
for (const auto &newRoom : change.newRooms) {
QJsonArray stateEvents;
stateEvents += 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, newRoom.roomId},
{u"sender"_s, newRoom.initialMembers[0]},
{u"state_key"_s, QString()},
{u"type"_s, u"m.room.create"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
for (const auto &member : newRoom.initialMembers) {
stateEvents += 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, newRoom.roomId},
{u"sender"_s, member},
{u"state_key"_s, member},
{u"type"_s, u"m.room.member"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
}
auto room = QJsonObject{{u"state"_s, QJsonObject{{u"events"_s, stateEvents}}}};
QJsonArray roomAccountData;
QJsonObject tags;
for (const auto &tag : newRoom.tags) {
tags[tag] = QJsonObject();
}
if (!tags.empty()) {
roomAccountData += QJsonObject{{u"type"_s, u"m.tag"_s}, {u"content"_s, QJsonObject{{u"tags"_s, tags}}}};
}
if (roomAccountData.size() > 0) {
room[u"account_data"] = QJsonObject{{u"events"_s, roomAccountData}};
}
joinRooms[newRoom.roomId] = room;
}
}
for (const auto &change : m_state.mid(token)) {
for (const auto &invitation : change.invitations) {
// TODO: The invitation could be for a room we haven't joined yet. Shouldn't be necessary for now, though.
auto stateEvents = joinRooms[invitation.roomId][u"state"_s][u"events"_s].toArray();
stateEvents += 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, invitation.roomId},
{u"sender"_s, u"@user:localhost:1234"_s},
{u"state_key"_s, invitation.userId},
{u"type"_s, u"m.room.member"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
if (joinRooms.contains(invitation.roomId)) {
auto room = joinRooms[invitation.roomId].toObject();
room[u"state"_s] = QJsonObject{{u"events"_s, stateEvents}};
joinRooms[invitation.roomId] = room;
} else {
joinRooms[invitation.roomId] = QJsonObject{{u"state"_s,
QJsonObject{
{u"events"_s, stateEvents},
}}};
}
}
}
for (const auto &change : m_state.mid(token)) {
for (const auto &ban : change.bans) {
// TODO: The ban could be for a room we haven't joined yet. Shouldn't be necessary for now, though.
auto stateEvents = joinRooms[ban.roomId][u"state"_s][u"events"_s].toArray();
stateEvents += 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, ban.roomId},
{u"sender"_s, u"@user:localhost:1234"_s},
{u"state_key"_s, ban.userId},
{u"type"_s, u"m.room.member"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
if (joinRooms.contains(ban.roomId)) {
auto room = joinRooms[ban.roomId].toObject();
room[u"state"_s] = QJsonObject{{u"events"_s, stateEvents}};
joinRooms[ban.roomId] = room;
} else {
joinRooms[ban.roomId] = QJsonObject{{u"state"_s,
QJsonObject{
{u"events"_s, stateEvents},
}}};
}
}
}
for (const auto &change : m_state.mid(token)) {
for (const auto &join : change.joins) {
// TODO: The join could be for a room we haven't joined yet. Shouldn't be necessary for now, though.
auto stateEvents = joinRooms[join.roomId][u"state"_s][u"events"_s].toArray();
stateEvents += 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, join.roomId},
{u"sender"_s, u"@user:localhost:1234"_s},
{u"state_key"_s, join.userId},
{u"type"_s, u"m.room.member"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
if (joinRooms.contains(join.roomId)) {
auto room = joinRooms[join.roomId].toObject();
room[u"state"_s] = QJsonObject{{u"events"_s, stateEvents}};
joinRooms[join.roomId] = room;
} else {
joinRooms[join.roomId] = QJsonObject{{u"state"_s,
QJsonObject{
{u"events"_s, stateEvents},
}}};
}
}
}
for (const auto &change : m_state.mid(token)) {
for (const auto &event : change.events) {
// TODO the room might be in a different join state.
auto timeline = joinRooms[event.fullJson[u"room_id"_s].toString()][u"timeline"_s][u"events"_s].toArray();
timeline += event.fullJson;
if (joinRooms.contains(event.fullJson[u"room_id"_s].toString())) {
auto room = joinRooms[event.fullJson[u"room_id"_s].toString()].toObject();
room[u"timeline"_s] = QJsonObject{{u"events"_s, timeline}};
joinRooms[event.fullJson[u"room_id"_s].toString()] = room;
} else {
joinRooms[event.fullJson[u"room_id"_s].toString()] = QJsonObject{
{u"timeline"_s, QJsonObject{{u"events"_s, timeline}}},
};
}
}
}
QJsonObject syncData = {
// {u"account_data"_s, QJsonObject {}},
// {u"presence"_s, QJsonObject {}},
{u"next_batch"_s, QString::number(m_state.size())},
};
QJsonObject rooms;
if (!joinRooms.isEmpty()) {
rooms[u"join"_s] = joinRooms;
}
if (!rooms.empty()) {
syncData[u"rooms"_s] = rooms;
}
qWarning() << syncData;
responder.write(QJsonDocument(syncData), QHttpServerResponder::StatusCode::Ok);
}

View File

@@ -2,51 +2,16 @@
// SPDX-License-Identifier: LGPL-2.0-or-later // SPDX-License-Identifier: LGPL-2.0-or-later
#include <QHttpServer> #include <QHttpServer>
#include <QJsonObject>
#include <QSslServer> #include <QSslServer>
struct Changes {
struct NewRoom {
QStringList initialMembers;
QString roomId;
QStringList tags;
};
QList<NewRoom> newRooms;
struct InviteUser {
QString userId;
QString roomId;
};
QList<InviteUser> invitations;
struct BanUser {
QString userId;
QString roomId;
};
QList<BanUser> bans;
struct JoinUser {
QString userId;
QString roomId;
};
QList<JoinUser> joins;
struct Event {
QJsonObject fullJson;
};
QList<Event> events;
};
struct RoomData { struct RoomData {
QStringList members; QStringList members;
QString id; QString id;
QStringList tags; QStringList tags;
}; };
class Server : public QObject class Server
{ {
Q_OBJECT
public: public:
Server(); Server();
@@ -72,7 +37,10 @@ private:
QHttpServer m_server; QHttpServer m_server;
QSslServer m_sslServer; QSslServer m_sslServer;
void sync(const QHttpServerRequest &request, QHttpServerResponder &responder); QHash<QString, QList<QString>> m_invitedUsers;
QHash<QString, QList<QString>> m_bannedUsers;
QHash<QString, QList<QString>> m_joinedUsers;
QList<Changes> m_state; QList<RoomData> m_roomsToCreate;
QMap<QString, QJsonArray> m_events;
}; };

View File

@@ -1,87 +0,0 @@
// SPDX-FileCopyrightText: 2025 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <QObject>
#include <QSignalSpy>
#include <QTest>
#include <KLocalizedString>
#include <Quotient/connection.h>
#include <Quotient/eventstats.h>
#include <Quotient/quotient_common.h>
#include <Quotient/syncdata.h>
#include "accountmanager.h"
#include "neochatroom.h"
#include "roommanager.h"
#include "server.h"
#include "testutils.h"
using namespace Quotient;
class ServerNoticesTest : public QObject
{
Q_OBJECT
private:
NeoChatConnection *connection = nullptr;
Server server;
private Q_SLOTS:
void initTestCase();
void test();
};
void ServerNoticesTest::initTestCase()
{
Connection::setRoomType<NeoChatRoom>();
server.start();
KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat"));
auto accountManager = new AccountManager(true);
QSignalSpy spy(accountManager, &AccountManager::connectionAdded);
connection = dynamic_cast<NeoChatConnection *>(accountManager->accounts()->front());
QVERIFY(connection);
auto roomId = server.createRoom(u"@user:localhost:1234"_s);
RoomManager::instance().setConnection(connection);
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());
auto room = dynamic_cast<NeoChatRoom *>(connection->room(roomId));
QVERIFY(room);
}
void ServerNoticesTest::test()
{
auto roomTreeModel = RoomManager::instance().roomTreeModel();
QCOMPARE(roomTreeModel->rowCount(roomTreeModel->index(NeoChatRoomType::ServerNotice, 0)), 0);
auto sortFilterRoomTreeModel = RoomManager::instance().sortFilterRoomTreeModel();
const auto roomId = server.createServerNoticesRoom(u"@user:localhost:1234"_s);
QSignalSpy syncSpy(connection, &Connection::syncDone);
QVERIFY(syncSpy.wait());
QVERIFY(syncSpy.wait());
const auto room = dynamic_cast<NeoChatRoom *>(connection->room(roomId));
QVERIFY(connection->room(roomId)->isServerNoticeRoom());
QCOMPARE(roomTreeModel->rowCount(roomTreeModel->index(NeoChatRoomType::ServerNotice, 0)), 1);
QCOMPARE(sortFilterRoomTreeModel->mapFromSource(roomTreeModel->indexForRoom(room)).parent().row(), 1 /* Below the normal room */);
server.sendEvent(roomId,
u"m.room.message"_s,
QJsonObject{
{u"body"_s, u"Foo"_s},
{u"format"_s, u"org.matrix.custom.html"_s},
{u"formatted_body"_s, u"Foo"_s},
{u"msgtype"_s, u"m.text"_s},
});
QVERIFY(syncSpy.wait());
QVERIFY(syncSpy.wait());
sortFilterRoomTreeModel->invalidate();
QCOMPARE(sortFilterRoomTreeModel->mapFromSource(roomTreeModel->indexForRoom(room)).parent().row(), 0);
room->markAllMessagesAsRead();
QCOMPARE(sortFilterRoomTreeModel->mapFromSource(roomTreeModel->indexForRoom(room)).parent().row(), 1 /* Below the normal room */);
}
QTEST_GUILESS_MAIN(ServerNoticesTest)
#include "servernoticestest.moc"

View File

@@ -1,7 +1,6 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com> // SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <QTest>
#include <Quotient/events/event.h> #include <Quotient/events/event.h>
#include <Quotient/syncdata.h> #include <Quotient/syncdata.h>
@@ -33,7 +32,7 @@ public:
if (!syncFileName.isEmpty()) { if (!syncFileName.isEmpty()) {
QFile testSyncFile; QFile testSyncFile;
testSyncFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + syncFileName); testSyncFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + syncFileName);
Q_UNUSED(testSyncFile.open(QIODevice::ReadOnly)); testSyncFile.open(QIODevice::ReadOnly);
const auto testSyncJson = QJsonDocument::fromJson(testSyncFile.readAll()); const auto testSyncJson = QJsonDocument::fromJson(testSyncFile.readAll());
Quotient::SyncRoomData roomData(id(), Quotient::JoinState::Join, testSyncJson.object()); Quotient::SyncRoomData roomData(id(), Quotient::JoinState::Join, testSyncJson.object());
update(std::move(roomData)); update(std::move(roomData));
@@ -47,7 +46,7 @@ inline Quotient::event_ptr_tt<EventT> loadEventFromFile(const QString &eventFile
if (!eventFileName.isEmpty()) { if (!eventFileName.isEmpty()) {
QFile testEventFile; QFile testEventFile;
testEventFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + eventFileName); testEventFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + eventFileName);
Q_UNUSED(testEventFile.open(QIODevice::ReadOnly)); testEventFile.open(QIODevice::ReadOnly);
auto testSyncJson = QJsonDocument::fromJson(testEventFile.readAll()).object(); auto testSyncJson = QJsonDocument::fromJson(testEventFile.readAll()).object();
return Quotient::loadEvent<EventT>(testSyncJson); return Quotient::loadEvent<EventT>(testSyncJson);
} }

View File

@@ -34,10 +34,6 @@ private Q_SLOTS:
void stripDisallowedTags(); void stripDisallowedTags();
void stripDisallowedAttributes(); void stripDisallowedAttributes();
void emptyCodeTags(); void emptyCodeTags();
void addStyle_data();
void addStyle();
void dontAddStyle_data();
void dontAddStyle();
void sendSimpleStringCase(); void sendSimpleStringCase();
void sendSingleParaMarkup(); void sendSingleParaMarkup();
@@ -75,9 +71,6 @@ private Q_SLOTS:
void componentOutput_data(); void componentOutput_data();
void componentOutput(); void componentOutput();
void updateSpoiler_data();
void updateSpoiler();
}; };
void TextHandlerTest::initTestCase() void TextHandlerTest::initTestCase()
@@ -96,26 +89,21 @@ void TextHandlerTest::initTestCase()
void TextHandlerTest::allowedAttributes() void TextHandlerTest::allowedAttributes()
{ {
auto theme = static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
const QString testInputString1 = u"<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"_s; const QString testInputString1 = u"<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"_s;
const QString testOutputString1S = u"<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"_s; const QString testOutputString1 = u"<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"_s;
const QString testOutputString1R = u"<span data-mx-spoiler style=\"color: transparent; background: %1;\"><font color=#FFFFFF>Test</font><span>"_s.arg(
theme->alternateBackgroundColor().name());
// Handle urls where the href has either single (') or double (") quotes. // Handle urls where the href has either single (') or double (") quotes.
const QString testInputString2 = u"<a href=\"https://kde.org\">link</a><a href='https://kde.org'>link</a>"_s; const QString testInputString2 = u"<a href=\"https://kde.org\">link</a><a href='https://kde.org'>link</a>"_s;
const QString testOutputString2S = u"<a href=\"https://kde.org\">link</a><a href='https://kde.org'>link</a>"_s; const QString testOutputString2 = u"<a href=\"https://kde.org\">link</a><a href='https://kde.org'>link</a>"_s;
const QString testOutputString2R =
u"<a href=\"https://kde.org\" style=\"text-decoration: none;\">link</a><a href='https://kde.org' style=\"text-decoration: none;\">link</a>"_s;
TextHandler testTextHandler; TextHandler testTextHandler;
testTextHandler.setData(testInputString1); testTextHandler.setData(testInputString1);
QCOMPARE(testTextHandler.handleSendText(), testOutputString1S); QCOMPARE(testTextHandler.handleSendText(), testOutputString1);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString1R); QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString1);
testTextHandler.setData(testInputString2); testTextHandler.setData(testInputString2);
QCOMPARE(testTextHandler.handleSendText(), testOutputString2S); QCOMPARE(testTextHandler.handleSendText(), testOutputString2);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString2R); QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString2);
} }
void TextHandlerTest::stripDisallowedTags() void TextHandlerTest::stripDisallowedTags()
@@ -158,56 +146,6 @@ void TextHandlerTest::emptyCodeTags()
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString); QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
} }
void TextHandlerTest::addStyle_data()
{
QTest::addColumn<QString>("testInputString");
QTest::addColumn<QString>("testOutputString");
QTest::newRow("link") << u"<a href=\"https://kde.org\">link</a>"_s << u"<a href=\"https://kde.org\" style=\"text-decoration: none;\">link</a>"_s;
QTest::newRow("table")
<< u"<table><tr><th>Company</th><th>Contact</th><th>Country</th></tr><tr><td>Alfreds Futterkiste</td><td>Maria Anders</td><td>Germany</td></tr><tr><td>Centro comercial Moctezuma</td><td>Francisco Chang</td><td>Mexico</td></tr></table>"_s
<< u"<table style=\"width: 100%; border-collapse: collapse; border: 1px; border-style: solid;\"><tr><th style=\"border: 1px solid black; padding: 3px;\">Company</th><th style=\"border: 1px solid black; padding: 3px;\">Contact</th><th style=\"border: 1px solid black; padding: 3px;\">Country</th></tr><tr><td style=\"border: 1px solid black; padding: 3px;\">Alfreds Futterkiste</td><td style=\"border: 1px solid black; padding: 3px;\">Maria Anders</td><td style=\"border: 1px solid black; padding: 3px;\">Germany</td></tr><tr><td style=\"border: 1px solid black; padding: 3px;\">Centro comercial Moctezuma</td><td style=\"border: 1px solid black; padding: 3px;\">Francisco Chang</td><td style=\"border: 1px solid black; padding: 3px;\">Mexico</td></tr></table>"_s;
auto theme = static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
QTest::newRow("spoiler") << u"<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"_s
<< u"<span data-mx-spoiler style=\"color: transparent; background: %1;\"><font color=#FFFFFF>Test</font><span>"_s.arg(
theme->alternateBackgroundColor().name());
}
void TextHandlerTest::addStyle()
{
QFETCH(QString, testInputString);
QFETCH(QString, testOutputString);
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}
void TextHandlerTest::dontAddStyle_data()
{
QTest::addColumn<QString>("testInputString");
QTest::addColumn<QString>("testOutputString");
QTest::newRow("link") << u"<a href=\"https://kde.org\">link</a>"_s << u"<a href=\"https://kde.org\">link</a>"_s;
QTest::newRow("table")
<< u"<table><tr><th>Company</th><th>Contact</th><th>Country</th></tr><tr><td>Alfreds Futterkiste</td><td>Maria Anders</td><td>Germany</td></tr><tr><td>Centro comercial Moctezuma</td><td>Francisco Chang</td><td>Mexico</td></tr></table>"_s
<< u"<table><tr><th>Company</th><th>Contact</th><th>Country</th></tr><tr><td>Alfreds Futterkiste</td><td>Maria Anders</td><td>Germany</td></tr><tr><td>Centro comercial Moctezuma</td><td>Francisco Chang</td><td>Mexico</td></tr></table>"_s;
QTest::newRow("spoiler") << u"<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"_s
<< u"<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"_s;
}
void TextHandlerTest::dontAddStyle()
{
QFETCH(QString, testInputString);
QFETCH(QString, testOutputString);
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}
void TextHandlerTest::sendSimpleStringCase() void TextHandlerTest::sendSimpleStringCase()
{ {
const QString testInputString = u"This data should just be left alone."_s; const QString testInputString = u"This data should just be left alone."_s;
@@ -400,8 +338,7 @@ void TextHandlerTest::receiveRichInPlainOut()
void TextHandlerTest::receivePlainTextIn() void TextHandlerTest::receivePlainTextIn()
{ {
const QString testInputString = u"<plain text in tag bracket>\nTest link https://kde.org."_s; const QString testInputString = u"<plain text in tag bracket>\nTest link https://kde.org."_s;
const QString testOutputStringRich = const QString testOutputStringRich = u"&lt;plain text in tag bracket&gt;<br>Test link <a href=\"https://kde.org\">https://kde.org</a>."_s;
u"&lt;plain text in tag bracket&gt;<br>Test link <a href=\"https://kde.org\" style=\"text-decoration: none;\">https://kde.org</a>."_s;
QString testOutputStringPlain = u"<plain text in tag bracket>\nTest link https://kde.org."_s; QString testOutputStringPlain = u"<plain text in tag bracket>\nTest link https://kde.org."_s;
// Make sure quotes are maintained in a plain string. // Make sure quotes are maintained in a plain string.
@@ -471,7 +408,7 @@ void TextHandlerTest::receivePlainStripMarkup()
void TextHandlerTest::receiveRichUserPill() void TextHandlerTest::receiveRichUserPill()
{ {
const QString testInputString = u"<p><a href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a></p>"_s; const QString testInputString = u"<p><a href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a></p>"_s;
const QString testOutputString = u"<b><a href=\"https://matrix.to/#/@alice:example.org\" style=\"text-decoration: none;\">@alice:example.org</a></b>"_s; const QString testOutputString = u"<b><a href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a></b>"_s;
TextHandler testTextHandler; TextHandler testTextHandler;
testTextHandler.setData(testInputString); testTextHandler.setData(testInputString);
@@ -523,23 +460,21 @@ void TextHandlerTest::receiveRichPlainUrl_data()
// so we can confirm consistent behaviour for complex urls. // so we can confirm consistent behaviour for complex urls.
QTest::addRow("link 1") 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"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\" style=\"text-decoration: none;\">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\" style=\"text-decoration: none;\">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 // 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 // contains what looks like and email. It was broken into 3 but needs to
// be just single link. // be just single link.
QTest::addRow("link 2") QTest::addRow("link 2")
<< u"https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/"_s << 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/\" style=\"text-decoration: none;\">https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/</a>"_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") QTest::addRow("email") << uR"(email@example.com <a href="mailto:email@example.com">Link already rich</a>)"_s
<< 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;
<< uR"(<a href="mailto:email@example.com" style="text-decoration: none;">email@example.com</a> <a href="mailto:email@example.com" style="text-decoration: none;">Link already rich</a>)"_s;
QTest::addRow("mxid") QTest::addRow("mxid")
<< u"@user:kde.org <a href=\"https://matrix.to/#/@user:kde.org\">Link already rich</a>"_s << 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\" style=\"text-decoration: none;\">@user:kde.org</a></b> <b><a href=\"https://matrix.to/#/@user:kde.org\" style=\"text-decoration: none;\">Link already rich</a></b>"_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 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;
<< u"a <b><a href=\"https://matrix.to/#/@user:kde.org\" style=\"text-decoration: none;\">@user:kde.org</a></b> b"_s;
} }
/** /**
@@ -661,35 +596,5 @@ void TextHandlerTest::componentOutput()
QCOMPARE(testTextHandler.textComponents(testInputString), testOutputComponents); QCOMPARE(testTextHandler.textComponents(testInputString), testOutputComponents);
} }
void TextHandlerTest::updateSpoiler_data()
{
QTest::addColumn<QString>("testInputString");
QTest::addColumn<QString>("testOutputString");
QTest::addColumn<bool>("spoilerRevealed");
auto theme = static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
QTest::newRow("same length") << u"<span data-mx-spoiler style=\"color: #123456; background: #123456;\">Test<span>"_s
<< u"<span data-mx-spoiler style=\"color: transparent; background: %1;\">Test<span>"_s.arg(
theme->alternateBackgroundColor().name())
<< false;
QTest::newRow("different length") << u"<span data-mx-spoiler style=\"color: short; background: looooooooooong;\">Test<span>"_s
<< u"<span data-mx-spoiler style=\"color: transparent; background: %1;\">Test<span>"_s.arg(
theme->alternateBackgroundColor().name())
<< false;
QTest::newRow("spoiler revealed")
<< u"<span data-mx-spoiler style=\"color: transparent; background: %1;\">Test<span>"_s.arg(theme->alternateBackgroundColor().name())
<< u"<span data-mx-spoiler style=\"color: %1; background: %2;\">Test<span>"_s.arg(theme->textColor().name(), theme->alternateBackgroundColor().name())
<< true;
}
void TextHandlerTest::updateSpoiler()
{
QFETCH(QString, testInputString);
QFETCH(QString, testOutputString);
QFETCH(bool, spoilerRevealed);
QCOMPARE(TextHandler::updateSpoilerText(this, testInputString, spoilerRevealed), testOutputString);
}
QTEST_MAIN(TextHandlerTest) QTEST_MAIN(TextHandlerTest)
#include "texthandlertest.moc" #include "texthandlertest.moc"

View File

@@ -161,7 +161,7 @@ void TimelineMessageModelTest::pendingEvent()
// different every time. // different every time.
QFile testSyncFile; QFile testSyncFile;
testSyncFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + u"test-pending-sync.json"_s); testSyncFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + u"test-pending-sync.json"_s);
QVERIFY(testSyncFile.open(QIODevice::ReadOnly)); testSyncFile.open(QIODevice::ReadOnly);
auto testSyncJson = QJsonDocument::fromJson(testSyncFile.readAll()); auto testSyncJson = QJsonDocument::fromJson(testSyncFile.readAll());
auto root = testSyncJson.object(); auto root = testSyncJson.object();
auto timeline = root["timeline"_L1].toObject(); auto timeline = root["timeline"_L1].toObject();

View File

@@ -3,12 +3,12 @@
add_definitions(-DDATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}" ) add_definitions(-DDATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}" )
qt_add_executable(timeline_memtest qt_add_executable(timeline-memtest
main.cpp main.cpp
) )
target_link_libraries(timeline_memtest PRIVATE neochatplugin Timelineplugin) target_link_libraries(timeline-memtest PRIVATE neochatplugin Timelineplugin)
target_link_libraries(timeline_memtest PUBLIC target_link_libraries(timeline-memtest PUBLIC
Qt::Core Qt::Core
Qt::Quick Qt::Quick
Qt::Qml Qt::Qml
@@ -16,13 +16,14 @@ target_link_libraries(timeline_memtest PUBLIC
Qt::QuickControls2 Qt::QuickControls2
Qt::Widgets Qt::Widgets
KF6::I18nQml KF6::I18nQml
KF6::Kirigami
QuotientQt6 QuotientQt6
LibNeoChat LibNeoChat
Timeline Timeline
) )
ecm_add_qml_module(timeline_memtest URI org.kde.neochat.timeline_memtest GENERATE_PLUGIN_SOURCE ecm_add_qml_module(timeline-memtest URI org.kde.neochat.timeline-memtest GENERATE_PLUGIN_SOURCE
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/timeline_memtest OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/timeline-memtest
QML_FILES QML_FILES
Main.qml Main.qml
SOURCES SOURCES

View File

@@ -28,7 +28,7 @@ int main(int argc, char **argv)
engine.rootContext()->setContextProperty(u"memTestTimelineModel"_s, memTestTimelineModel); engine.rootContext()->setContextProperty(u"memTestTimelineModel"_s, memTestTimelineModel);
engine.rootContext()->setContextProperty(u"messageFilterModel"_s, messageFilterModel); engine.rootContext()->setContextProperty(u"messageFilterModel"_s, messageFilterModel);
engine.loadFromModule("org.kde.neochat.timeline_memtest", "Main"); engine.loadFromModule("org.kde.neochat.timeline-memtest", "Main");
return app.exec(); return app.exec();
} }

View File

@@ -37,11 +37,7 @@ public:
if (!syncFileName.isEmpty()) { if (!syncFileName.isEmpty()) {
QFile testSyncFile; QFile testSyncFile;
testSyncFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + syncFileName); testSyncFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + syncFileName);
auto ok = testSyncFile.open(QIODevice::ReadOnly); testSyncFile.open(QIODevice::ReadOnly);
if (!ok) {
qWarning() << "Failed to open" << testSyncFile.fileName() << testSyncFile.errorString();
}
auto testSyncJson = QJsonDocument::fromJson(testSyncFile.readAll()).object(); auto testSyncJson = QJsonDocument::fromJson(testSyncFile.readAll()).object();
auto timelineJson = testSyncJson["timeline"_L1].toObject(); auto timelineJson = testSyncJson["timeline"_L1].toObject();
timelineJson["events"_L1] = multiplyEvents(timelineJson["events"_L1].toArray(), 100); timelineJson["events"_L1] = multiplyEvents(timelineJson["events"_L1].toArray(), 100);

File diff suppressed because it is too large Load Diff

View File

@@ -44,6 +44,7 @@ Name[sv]=NeoChat
Name[ta]=நியோச்சாட் Name[ta]=நியோச்சாட்
Name[tr]=NeoChat Name[tr]=NeoChat
Name[uk]=NeoChat Name[uk]=NeoChat
Name[x-test]=xxNeoChatxx
Name[zh_CN]=NeoChat Name[zh_CN]=NeoChat
Name[zh_TW]=NeoChat Name[zh_TW]=NeoChat
GenericName=Matrix Client GenericName=Matrix Client
@@ -87,6 +88,7 @@ GenericName[sv]=Matrix-klient
GenericName[ta]=Matrix வாங்கி GenericName[ta]=Matrix வாங்கி
GenericName[tr]=Matrix İstemcisi GenericName[tr]=Matrix İstemcisi
GenericName[uk]=Клієнт Matrix GenericName[uk]=Клієнт Matrix
GenericName[x-test]=xxMatrix Clientxx
GenericName[zh_CN]=Matrix 客户端 GenericName[zh_CN]=Matrix 客户端
GenericName[zh_TW]=Matrix 用戶端 GenericName[zh_TW]=Matrix 用戶端
Comment=Chat on Matrix Comment=Chat on Matrix
@@ -112,7 +114,6 @@ Comment[lv]=Tērzējiet „Matrix“ tīklā
Comment[nl]=Chat op Matrix Comment[nl]=Chat op Matrix
Comment[pl]=Rozmawiaj na Matriksie Comment[pl]=Rozmawiaj na Matriksie
Comment[pt_BR]=Bate papo na Matrix Comment[pt_BR]=Bate papo na Matrix
Comment[ro]=Discutați pe Matrix
Comment[ru]=Общение в Matrix Comment[ru]=Общение в Matrix
Comment[sa]=Matrix इत्यत्र गपशपं कुर्वन्तु Comment[sa]=Matrix इत्यत्र गपशपं कुर्वन्तु
Comment[sl]=Klepet na Matrixu Comment[sl]=Klepet na Matrixu
@@ -120,6 +121,7 @@ Comment[sv]=Chatta på Matrix
Comment[ta]=மேட்ரிக்ஸில் உரையாட உதவும் Comment[ta]=மேட்ரிக்ஸில் உரையாட உதவும்
Comment[tr]=Matrix üzerinde sohbet edin Comment[tr]=Matrix üzerinde sohbet edin
Comment[uk]=Спілкування у Matrix Comment[uk]=Спілкування у Matrix
Comment[x-test]=xxChat on Matrixxx
Comment[zh_CN]=在 Matrix 上聊天 Comment[zh_CN]=在 Matrix 上聊天
Comment[zh_TW]=在 Matrix 上聊天 Comment[zh_TW]=在 Matrix 上聊天
MimeType=x-scheme-handler/matrix; MimeType=x-scheme-handler/matrix;

View File

@@ -0,0 +1,28 @@
SPDX-FileCopyrightText: 2025 Tobias Fella <tobias.fella@kde.org>
SPDX-License-Identifier: BSD-2-Clause
From dbd1cefd0f07a6942aef450f8f3e082aa3b1cc25 Mon Sep 17 00:00:00 2001
From: Tobias Fella <tobias.fella@kde.org>
Date: Sun, 17 Aug 2025 20:04:04 +0200
Subject: [PATCH] Revert "Bump KF6 dependency version"
This reverts commit 18a6ea98232b3a734905fb18eebba9cf39bf5325.
---
CMakeLists.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 10fe66daa..cd063113d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -14,7 +14,7 @@ set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
-set(KF_MIN_VERSION "6.17")
+set(KF_MIN_VERSION "6.12")
set(QT_MIN_VERSION "6.5")
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)
--
2.50.1

View File

@@ -0,0 +1,123 @@
SPDX-FileCopyrightText: 2025 Tobias Fella <tobias.fella@kde.org>
SPDX-License-Identifier: BSD-2-Clause
From ca72345b8ee550be2172d8ac5e5dc9e4c2b508c9 Mon Sep 17 00:00:00 2001
From: Tobias Fella <tobias.fella@kde.org>
Date: Sun, 17 Aug 2025 20:00:08 +0200
Subject: [PATCH] Revert "Use new Kirigami builtin column resize handle"
This reverts commit de97275a387abcbca6fcb185bcbd1b69c30f5c66.
---
src/app/qml/Main.qml | 1 -
src/rooms/RoomListPage.qml | 70 +++++++++++++++++++++++++++++---------
2 files changed, 54 insertions(+), 17 deletions(-)
diff --git a/src/app/qml/Main.qml b/src/app/qml/Main.qml
index ea8955674..6eed271c1 100644
--- a/src/app/qml/Main.qml
+++ b/src/app/qml/Main.qml
@@ -45,7 +45,6 @@ Kirigami.ApplicationWindow {
showExisting: true
onConnectionChosen: root.load()
}
- columnView.columnResizeMode: pageStack.wideMode ? Kirigami.ColumnView.DynamicColumns : Kirigami.ColumnView.SingleColumn
globalToolBar.canContainHandles: true
globalToolBar {
style: Kirigami.ApplicationHeaderStyle.ToolBar
diff --git a/src/rooms/RoomListPage.qml b/src/rooms/RoomListPage.qml
index 2ac211fd5..f5586d789 100644
--- a/src/rooms/RoomListPage.qml
+++ b/src/rooms/RoomListPage.qml
@@ -17,22 +17,13 @@ import org.kde.neochat
Kirigami.Page {
id: root
- Kirigami.ColumnView.interactiveResizeEnabled: true
- Kirigami.ColumnView.minimumWidth: _private.collapsedSize + spaceDrawer.width + 1
- Kirigami.ColumnView.maximumWidth: _private.defaultWidth + spaceDrawer.width + 1
- Kirigami.ColumnView.onInteractiveResizingChanged: {
- if (!Kirigami.ColumnView.interactiveResizing && collapsed) {
- Kirigami.ColumnView.preferredWidth = root.Kirigami.ColumnView.minimumWidth;
- }
- }
- Kirigami.ColumnView.preferredWidth: _private.currentWidth + spaceDrawer.width + 1
- Kirigami.ColumnView.onPreferredWidthChanged: {
- if (width > _private.collapseWidth) {
- NeoChatConfig.collapsed = false;
- } else if (Kirigami.ColumnView.interactiveResizing) {
- NeoChatConfig.collapsed = true;
- }
- }
+ /**
+ * @brief The current width of the room list.
+ *
+ * @note Other objects can access the value but the private function makes sure
+ * that only the internal members can modify it.
+ */
+ readonly property int currentWidth: _private.currentWidth + spaceDrawer.width + 1
required property NeoChatConnection connection
@@ -40,6 +31,10 @@ Kirigami.Page {
signal search
+ onCurrentWidthChanged: pageStack.defaultColumnWidth = root.currentWidth
+ Component.onCompleted: pageStack.defaultColumnWidth = root.currentWidth
+
+
onCollapsedChanged: {
if (collapsed) {
RoomManager.sortFilterRoomTreeModel.filterText = "";
@@ -252,6 +247,49 @@ Kirigami.Page {
sourceComponent: Kirigami.Settings.isMobile ? exploreComponentMobile : userInfoDesktop
}
+ MouseArea {
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ parent: applicationWindow().overlay.parent
+
+ x: root.currentWidth - width / 2
+ width: Kirigami.Units.smallSpacing * 2
+ z: root.z + 1
+ enabled: RoomManager.hasOpenRoom && applicationWindow().width >= Kirigami.Units.gridUnit * 35
+ visible: enabled
+ cursorShape: Qt.SplitHCursor
+
+ property int _lastX
+
+ onPressed: mouse => {
+ _lastX = mouse.x;
+ }
+ onPositionChanged: mouse => {
+ if (_lastX == -1) {
+ return;
+ }
+ if (mouse.x > _lastX) {
+ // we moved to the right
+ if (_private.currentWidth < _private.collapseWidth && _private.currentWidth + (mouse.x - _lastX) >= _private.collapseWidth) {
+ // Here we get back directly to a more wide mode.
+ _private.currentWidth = _private.defaultWidth;
+ NeoChatConfig.collapsed = false;
+ } else if (_private.currentWidth >= _private.collapseWidth) {
+ // Increase page width
+ _private.currentWidth = Math.min(_private.defaultWidth, _private.currentWidth + (mouse.x - _lastX));
+ }
+ } else if (mouse.x < _lastX) {
+ const tmpWidth = _private.currentWidth - (_lastX - mouse.x);
+ if (tmpWidth < _private.collapseWidth) {
+ _private.currentWidth = Qt.binding(() => _private.collapsedSize);
+ NeoChatConfig.collapsed = true;
+ } else {
+ _private.currentWidth = tmpWidth;
+ }
+ }
+ }
+ }
+
Component {
id: userInfo
UserInfo {
--
2.50.1

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

@@ -1,122 +0,0 @@
<?xml version="1.0" ?>
<!DOCTYPE refentry PUBLIC "-//KDE//DTD DocBook XML V4.5-Based Variant V1.1//EN" "dtd/kdedbx45.dtd" [
<!ENTITY % Brazilian-Portuguese "INCLUDE">
]>
<!--
SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
SPDX-License-Identifier: CC-BY-SA-4.0
-->
<refentry lang="&language;">
<refentryinfo>
<title
>Manual do Usuário do NeoChat</title>
<author
><firstname
>Carl</firstname
><surname
>Schwan</surname
> <contrib
>NeoChat man page.</contrib
> <email
>carl@carlschwan.eu</email
></author>
<date
>01/11/2022</date>
<releaseinfo
>22.09</releaseinfo>
<productname
>NeoChat</productname>
</refentryinfo>
<refmeta>
<refentrytitle>
<command
>neochat</command>
</refentrytitle>
<manvolnum
>1</manvolnum>
</refmeta>
<refnamediv>
<refname
>neochat</refname>
<refpurpose
>Cliente para interação com o protocolo de mensagens Matrix.</refpurpose>
</refnamediv>
<!-- body begins here -->
<refsynopsisdiv id='synopsis'>
<cmdsynopsis
><command
>neochat</command
> <arg choice="opt"
><replaceable
>URI</replaceable
></arg
> </cmdsynopsis>
</refsynopsisdiv>
<refsect1 id="description">
<title
>Descrição</title>
<para
>O <command
>neochat</command
> é um aplicativo de bate-papo para o protocolo Matrix. Ele funciona tanto em computadores quanto em dispositivos móveis. </para>
</refsect1>
<refsect1 id="options"
><title
>Opções</title>
<variablelist>
<varlistentry>
<term
><option
>URI</option
></term>
<listitem>
<para
>O URI da matriz para um usuário ou uma sala. Por exemplo, matrix:u/usuário:exemplo.org e matrix:r/root:exemplo.org. Isso fará com que o NeoChat tente abrir a sala ou conversa especificada. </para>
</listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1 id="bug">
<title
>Relatar bugs</title>
<para
>Você pode reportar erros e solicitar novas funcionalidades em <ulink url="https://bugs.kde.org/enter_bug.cgi?product=NeoChat&amp;component=General"
>https://bugs.kde.org/enter_bug.cgi?product=NeoChat&amp;component=General</ulink
></para>
</refsect1>
<refsect1>
<title
>Veja também</title>
<simplelist>
<member
>Lista de perguntas frequentes sobre o Matrix <ulink url="https://matrix.org/faq/"
>https://matrix.org/faq/</ulink
> </member>
<member
>kf5options(7)</member>
<member
>qt5options(7)</member>
</simplelist>
</refsect1>
<refsect1 id="copyright"
><title
>Direitos autorais</title>
<para
>Direitos autorais &copy; 2020-2022 Tobias Fella </para>
<para
>Direitos autorais &copy; 2020-2022 Carl Schwan </para>
<para
>Licença: GNU General Public Versão 3 ou posterior <ulink url="https://www.gnu.org/licenses/gpl-3.0.html"
>https://www.gnu.org/licenses/gpl-3.0.html</ulink
>&gt;</para>
</refsect1>
</refentry>

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

@@ -24,8 +24,6 @@ add_library(neochat STATIC
models/notificationsmodel.h models/notificationsmodel.h
proxycontroller.cpp proxycontroller.cpp
proxycontroller.h proxycontroller.h
mediamanager.cpp
mediamanager.h
sharehandler.cpp sharehandler.cpp
sharehandler.h sharehandler.h
foreigntypes.h foreigntypes.h
@@ -48,8 +46,6 @@ if(ANDROID OR WIN32)
set_source_files_properties(qml/GlobalMenuStub.qml PROPERTIES set_source_files_properties(qml/GlobalMenuStub.qml PROPERTIES
QT_QML_SOURCE_TYPENAME GlobalMenu QT_QML_SOURCE_TYPENAME GlobalMenu
) )
else()
set(EXTRA_IMPORTS org.kde.purpose)
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
@@ -103,7 +99,7 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
qml/ReasonDialog.qml qml/ReasonDialog.qml
qml/NewPollDialog.qml qml/NewPollDialog.qml
qml/UserMenu.qml qml/UserMenu.qml
qml/MeetingDialog.qml qml/IncomingCallDialog.qml
DEPENDENCIES DEPENDENCIES
QtCore QtCore
QtQuick QtQuick
@@ -120,8 +116,8 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
org.kde.neochat.login org.kde.neochat.login
org.kde.neochat.chatbar org.kde.neochat.chatbar
org.kde.config org.kde.config
org.kde.purpose
org.kde.syntaxhighlighting org.kde.syntaxhighlighting
${EXTRA_IMPORTS}
) )
if(NOT ANDROID AND NOT WIN32) if(NOT ANDROID AND NOT WIN32)
@@ -153,7 +149,6 @@ target_include_directories(neochat-app PRIVATE ${CMAKE_BINARY_DIR})
target_link_libraries(neochat-app PRIVATE target_link_libraries(neochat-app PRIVATE
neochat neochat
KF6::IconThemes
) )
ecm_add_app_icon(NEOCHAT_ICON ICONS ${CMAKE_SOURCE_DIR}/128-logo.png) ecm_add_app_icon(NEOCHAT_ICON ICONS ${CMAKE_SOURCE_DIR}/128-logo.png)
@@ -204,9 +199,8 @@ target_link_libraries(neochat PUBLIC
KF6::ConfigGui KF6::ConfigGui
KF6::CoreAddons KF6::CoreAddons
KF6::SonnetCore KF6::SonnetCore
KF6::IconThemes
KF6::ItemModels KF6::ItemModels
KF6::I18nQml
KirigamiApp
QuotientQt6 QuotientQt6
Login Login
Rooms Rooms
@@ -214,6 +208,10 @@ target_link_libraries(neochat PUBLIC
Spaces Spaces
) )
if (TARGET KF6::Crash)
target_link_libraries(neochat PUBLIC KF6::Crash)
endif()
kconfig_target_kcfg_file(neochat FILE neochatconfig.kcfg CLASS_NAME NeoChatConfig MUTATORS GENERATE_PROPERTIES DEFAULT_VALUE_GETTERS PARENT_IN_CONSTRUCTOR SINGLETON GENERATE_MOC QML_REGISTRATION) kconfig_target_kcfg_file(neochat FILE neochatconfig.kcfg CLASS_NAME NeoChatConfig MUTATORS GENERATE_PROPERTIES DEFAULT_VALUE_GETTERS PARENT_IN_CONSTRUCTOR SINGLETON GENERATE_MOC QML_REGISTRATION)
if(NEOCHAT_FLATPAK) if(NEOCHAT_FLATPAK)
@@ -324,7 +322,6 @@ if(ANDROID)
"kt-restore-defaults-symbolic" "kt-restore-defaults-symbolic"
"user-symbolic" "user-symbolic"
"mark-location-symbolic" "mark-location-symbolic"
"amarok_playcount"
${KIRIGAMI_ADDONS_ICONS} ${KIRIGAMI_ADDONS_ICONS}
) )
@@ -344,6 +341,9 @@ if(TARGET KF6::DBusAddons AND NOT WIN32)
endif() endif()
if (TARGET KUnifiedPush) if (TARGET KUnifiedPush)
target_compile_definitions(neochat PUBLIC -DHAVE_KUNIFIEDPUSH)
target_link_libraries(neochat PUBLIC KUnifiedPush)
if (NOT ANDROID) if (NOT ANDROID)
configure_file(org.kde.neochat.service.in ${CMAKE_CURRENT_BINARY_DIR}/org.kde.neochat.service) configure_file(org.kde.neochat.service.in ${CMAKE_CURRENT_BINARY_DIR}/org.kde.neochat.service)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.kde.neochat.service DESTINATION ${KDE_INSTALL_DBUSSERVICEDIR}) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.kde.neochat.service DESTINATION ${KDE_INSTALL_DBUSSERVICEDIR})
@@ -353,8 +353,7 @@ endif()
install(TARGETS neochat-app ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) 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)
# krunner plugin must be the same as the app id for flatpak to export it install(FILES plasma-runner-neochat.desktop DESTINATION ${KDE_INSTALL_DATAROOTDIR}/krunner/dbusplugins)
install(FILES plasma-runner-neochat.desktop DESTINATION ${KDE_INSTALL_DATAROOTDIR}/krunner/dbusplugins RENAME org.kde.neochat.desktop)
endif() endif()
if (APPLE) if (APPLE)

View File

@@ -17,15 +17,18 @@
#include <Quotient/qt_connection_util.h> #include <Quotient/qt_connection_util.h>
#include <Quotient/settings.h> #include <Quotient/settings.h>
#include "accountmanager.h"
#include "callmanager.h"
#include "enums/roomsortparameter.h" #include "enums/roomsortparameter.h"
#include "general_logging.h"
#include "mediasizehelper.h" #include "mediasizehelper.h"
#include "models/actionsmodel.h" #include "models/actionsmodel.h"
#include "models/messagemodel.h" #include "models/messagemodel.h"
#include "models/roomlistmodel.h" #include "models/roomlistmodel.h"
#include "models/roomtreemodel.h" #include "models/roomtreemodel.h"
#include "neochatconfig.h" #include "neochatconfig.h"
#include "neochatconnection.h"
#include "neochatroom.h" #include "neochatroom.h"
#include "notificationsmanager.h"
#include "proxycontroller.h" #include "proxycontroller.h"
#include "roommanager.h" #include "roommanager.h"
@@ -35,6 +38,14 @@
#include "trayicon_sni.h" #include "trayicon_sni.h"
#endif #endif
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
#ifndef Q_OS_ANDROID
#include <QDBusConnection>
#include <QDBusInterface>
#include <QDBusMessage>
#endif
#endif
#ifdef HAVE_KUNIFIEDPUSH #ifdef HAVE_KUNIFIEDPUSH
#include <kunifiedpush/connector.h> #include <kunifiedpush/connector.h>
#endif #endif
@@ -62,6 +73,11 @@ Controller::Controller(QObject *parent)
{ {
Connection::setRoomType<NeoChatRoom>(); Connection::setRoomType<NeoChatRoom>();
CallManager::instance().setCallsEnabled(NeoChatConfig::calls());
connect(NeoChatConfig::self(), &NeoChatConfig::CallsChanged, this, []() {
CallManager::instance().setCallsEnabled(NeoChatConfig::calls());
});
Connection::setDirectChatEncryptionDefault(NeoChatConfig::preferUsingEncryption()); Connection::setDirectChatEncryptionDefault(NeoChatConfig::preferUsingEncryption());
connect(NeoChatConfig::self(), &NeoChatConfig::PreferUsingEncryptionChanged, this, [] { connect(NeoChatConfig::self(), &NeoChatConfig::PreferUsingEncryptionChanged, this, [] {
Connection::setDirectChatEncryptionDefault(NeoChatConfig::preferUsingEncryption()); Connection::setDirectChatEncryptionDefault(NeoChatConfig::preferUsingEncryption());
@@ -123,10 +139,8 @@ Controller::Controller(QObject *parent)
connect(NeoChatConfig::self(), &NeoChatConfig::SystemTrayChanged, this, &Controller::setQuitOnLastWindowClosed); connect(NeoChatConfig::self(), &NeoChatConfig::SystemTrayChanged, this, &Controller::setQuitOnLastWindowClosed);
#endif #endif
connect(QGuiApplication::instance(), &QCoreApplication::aboutToQuit, QGuiApplication::instance(), [this] { QObject::connect(QGuiApplication::instance(), &QCoreApplication::aboutToQuit, QGuiApplication::instance(), [this] {
#ifndef Q_OS_ANDROID
delete m_trayIcon; delete m_trayIcon;
#endif
NeoChatConfig::self()->save(); NeoChatConfig::self()->save();
}); });
@@ -192,15 +206,13 @@ void Controller::setAccountManager(AccountManager *manager)
m_accountManager = manager; m_accountManager = manager;
if (!m_accountManager) { if (m_accountManager) {
return; connect(m_accountManager, &AccountManager::errorOccured, this, &Controller::errorOccured);
connect(m_accountManager, &AccountManager::accountsLoadingChanged, this, &Controller::accountsLoadingChanged);
connect(m_accountManager, &AccountManager::connectionAdded, this, &Controller::initConnection);
connect(m_accountManager, &AccountManager::connectionDropped, this, &Controller::teardownConnection);
connect(m_accountManager, &AccountManager::activeConnectionChanged, this, &Controller::initActiveConnection);
} }
connect(m_accountManager, &AccountManager::errorOccured, this, &Controller::errorOccured);
connect(m_accountManager, &AccountManager::accountsLoadingChanged, this, &Controller::accountsLoadingChanged);
connect(m_accountManager, &AccountManager::connectionAdded, this, &Controller::initConnection);
connect(m_accountManager, &AccountManager::connectionDropped, this, &Controller::teardownConnection);
connect(m_accountManager, &AccountManager::activeConnectionChanged, this, &Controller::initActiveConnection);
} }
void Controller::initConnection(NeoChatConnection *connection) void Controller::initConnection(NeoChatConnection *connection)
@@ -256,8 +268,8 @@ bool Controller::supportSystemTray() const
#ifdef Q_OS_ANDROID #ifdef Q_OS_ANDROID
return false; return false;
#else #else
QStringList unsupportedPlatforms{u"GNOME"_s, u"Pantheon"_s}; auto de = QString::fromLatin1(qgetenv("XDG_CURRENT_DESKTOP"));
return !unsupportedPlatforms.contains(QString::fromLatin1(qgetenv("XDG_CURRENT_DESKTOP"))); return de != u"GNOME"_s && de != u"Pantheon"_s;
#endif #endif
} }
@@ -267,8 +279,11 @@ void Controller::setQuitOnLastWindowClosed()
if (supportSystemTray() && NeoChatConfig::self()->systemTray()) { if (supportSystemTray() && NeoChatConfig::self()->systemTray()) {
m_trayIcon = new TrayIcon(this); m_trayIcon = new TrayIcon(this);
m_trayIcon->show(); m_trayIcon->show();
} else if (m_trayIcon) { } else {
delete m_trayIcon; if (m_trayIcon) {
delete m_trayIcon;
m_trayIcon = nullptr;
}
} }
#endif #endif
} }
@@ -306,7 +321,8 @@ void Controller::listenForNotifications()
connect(timer, &QTimer::timeout, qGuiApp, &QGuiApplication::quit); connect(timer, &QTimer::timeout, qGuiApp, &QGuiApplication::quit);
connect(connector, &KUnifiedPush::Connector::messageReceived, [timer](const QByteArray &data) { connect(connector, &KUnifiedPush::Connector::messageReceived, [timer](const QByteArray &data) {
NotificationsManager::postPushNotification(data); instance().m_notificationsManager.postPushNotification(data);
timer->stop();
}); });
// Wait five seconds to see if we received any messages or this happened to be an erroneous activation. // Wait five seconds to see if we received any messages or this happened to be an erroneous activation.
@@ -324,7 +340,30 @@ void Controller::clearInvitationNotification(const QString &roomId)
void Controller::updateBadgeNotificationCount(int count) void Controller::updateBadgeNotificationCount(int count)
{ {
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
#ifndef Q_OS_ANDROID
// copied from Telegram desktop
const auto launcherUrl = "application://org.kde.neochat.desktop"_L1;
// Gnome requires that count is a 64bit integer
const qint64 counterSlice = std::min(count, 9999);
QVariantMap dbusUnityProperties;
if (counterSlice > 0) {
dbusUnityProperties["count"_L1] = counterSlice;
dbusUnityProperties["count-visible"_L1] = true;
} else {
dbusUnityProperties["count-visible"_L1] = false;
}
auto signal = QDBusMessage::createSignal("/com/canonical/unity/launcherentry/neochat"_L1, "com.canonical.Unity.LauncherEntry"_L1, "Update"_L1);
signal.setArguments({launcherUrl, dbusUnityProperties});
QDBusConnection::sessionBus().send(signal);
#endif // Q_OS_ANDROID
#else
qGuiApp->setBadgeNumber(count); qGuiApp->setBadgeNumber(count);
#endif // QT_VERSION_CHECK(6, 6, 0)
} }
bool Controller::isFlatpak() const bool Controller::isFlatpak() const
@@ -345,10 +384,7 @@ QString Controller::loadFileContent(const QString &path) const
{ {
QUrl url(path); QUrl url(path);
QFile file(url.isLocalFile() ? url.toLocalFile() : url.toString()); QFile file(url.isLocalFile() ? url.toLocalFile() : url.toString());
if (!file.open(QFile::ReadOnly)) { file.open(QFile::ReadOnly);
qCWarning(GENERAL) << "Failed to open file" << path;
return {};
}
return QString::fromLatin1(file.readAll()); return QString::fromLatin1(file.readAll());
} }

View File

@@ -110,9 +110,8 @@ private:
void initActiveConnection(NeoChatConnection *oldConnection, NeoChatConnection *newConnection); void initActiveConnection(NeoChatConnection *oldConnection, NeoChatConnection *newConnection);
QPointer<NeoChatConnection> m_connection; QPointer<NeoChatConnection> m_connection;
#ifndef Q_OS_ANDROID TrayIcon *m_trayIcon = nullptr;
QPointer<TrayIcon> m_trayIcon;
#endif
QString m_endpoint; QString m_endpoint;
QStringList m_shownImages; QStringList m_shownImages;

View File

@@ -33,10 +33,13 @@
#include <KWindowSystem> #include <KWindowSystem>
#endif #endif
#if __has_include("KCrash")
#include <KCrash>
#endif
#include <KIconTheme> #include <KIconTheme>
#include <KLocalizedQmlContext> #include <KLocalizedContext>
#include <KLocalizedString> #include <KLocalizedString>
#include <KirigamiApp>
#include "neochat-version.h" #include "neochat-version.h"
@@ -101,11 +104,8 @@ Q_DECL_EXPORT
#endif #endif
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
QNetworkProxyFactory::setUseSystemConfiguration(true);
// We currently need to do this ourselves,
// KirigamiApp currently called this after constructing the app which breaks icons on Windows.
KIconTheme::initTheme(); KIconTheme::initTheme();
QNetworkProxyFactory::setUseSystemConfiguration(true);
#ifdef HAVE_WEBVIEW #ifdef HAVE_WEBVIEW
QtWebView::initialize(); QtWebView::initialize();
@@ -113,10 +113,24 @@ int main(int argc, char *argv[])
QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGLRhi); QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGLRhi);
#endif #endif
KirigamiApp::App app(argc, argv); #ifdef Q_OS_ANDROID
KirigamiApp kirigamiApp; QGuiApplication app(argc, argv);
QQuickStyle::setStyle(u"org.kde.breeze"_s);
#else
QIcon::setFallbackThemeName("breeze"_L1);
QApplication app(argc, argv);
// Default to org.kde.desktop style unless the user forces another style
if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) {
QQuickStyle::setStyle(u"org.kde.desktop"_s);
}
#endif
#ifdef Q_OS_WINDOWS #ifdef Q_OS_WINDOWS
if (AttachConsole(ATTACH_PARENT_PROCESS)) {
freopen("CONOUT$", "w", stdout);
freopen("CONOUT$", "w", stderr);
}
QApplication::setStyle(u"breeze"_s); QApplication::setStyle(u"breeze"_s);
QFont font(u"Segoe UI Emoji"_s); QFont font(u"Segoe UI Emoji"_s);
font.setPointSize(10); font.setPointSize(10);
@@ -163,6 +177,10 @@ int main(int argc, char *argv[])
KAboutData::setApplicationData(about); KAboutData::setApplicationData(about);
QGuiApplication::setWindowIcon(QIcon::fromTheme(u"org.kde.neochat"_s)); QGuiApplication::setWindowIcon(QIcon::fromTheme(u"org.kde.neochat"_s));
#if __has_include("KCrash")
KCrash::initialize();
#endif
Connection::setEncryptionDefault(true); Connection::setEncryptionDefault(true);
Connection::setDirectChatEncryptionDefault(true); Connection::setDirectChatEncryptionDefault(true);
@@ -187,7 +205,7 @@ int main(int argc, char *argv[])
parser.addOption(testOption); parser.addOption(testOption);
#ifdef HAVE_KUNIFIEDPUSH #ifdef HAVE_KUNIFIEDPUSH
QCommandLineOption dbusActivatedOption(u"dbus-activated"_s); QCommandLineOption dbusActivatedOption(u"dbus-activated"_s, i18n("Internal usage only."));
dbusActivatedOption.setFlags(QCommandLineOption::Flag::HiddenFromHelp); dbusActivatedOption.setFlags(QCommandLineOption::Flag::HiddenFromHelp);
parser.addOption(dbusActivatedOption); parser.addOption(dbusActivatedOption);
#endif #endif
@@ -201,14 +219,8 @@ int main(int argc, char *argv[])
#ifdef HAVE_KUNIFIEDPUSH #ifdef HAVE_KUNIFIEDPUSH
if (parser.isSet(dbusActivatedOption)) { if (parser.isSet(dbusActivatedOption)) {
#ifdef HAVE_KDBUSADDONS // We want to be replaceable by the main client
// We *don't* want to use KDBusService here. I don't know why, but it makes activation super unreliable. We don't really need it anyway. KDBusService service(KDBusService::Replace);
if (!QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.neochat"))) {
// Gracefully fail if NeoChat is already running
qWarning() << "NeoChat already running, not sending push notifications.";
return 0;
}
#endif
#ifdef HAVE_RUNNER #ifdef HAVE_RUNNER
// If we are built with KRunner and KUnifiedPush support, we need to do something special. // If we are built with KRunner and KUnifiedPush support, we need to do something special.
@@ -267,7 +279,7 @@ int main(int argc, char *argv[])
}); });
#endif #endif
KLocalization::setupLocalizedContext(&engine); engine.rootContext()->setContextObject(new KLocalizedContext(&engine));
engine.setNetworkAccessManagerFactory(new NetworkAccessManagerFactory()); engine.setNetworkAccessManagerFactory(new NetworkAccessManagerFactory());
if (parser.isSet("ignore-ssl-errors"_L1)) { if (parser.isSet("ignore-ssl-errors"_L1)) {
@@ -282,9 +294,7 @@ int main(int argc, char *argv[])
engine.addImageProvider(u"blurhash"_s, new BlurhashImageProvider); engine.addImageProvider(u"blurhash"_s, new BlurhashImageProvider);
if (!kirigamiApp.start("org.kde.neochat", "Main", &engine)) { engine.loadFromModule("org.kde.neochat", "Main");
return -1;
}
if (!parser.positionalArguments().isEmpty() && !parser.isSet("share"_L1)) { if (!parser.positionalArguments().isEmpty() && !parser.isSet("share"_L1)) {
RoomManager::instance().setUrlArgument(parser.positionalArguments()[0]); RoomManager::instance().setUrlArgument(parser.positionalArguments()[0]);

View File

@@ -58,7 +58,7 @@ QVariant NotificationsModel::data(const QModelIndex &index, int role) const
QHash<int, QByteArray> NotificationsModel::roleNames() const QHash<int, QByteArray> NotificationsModel::roleNames() const
{ {
return { return {
{TextRole, "notificationText"}, {TextRole, "text"},
{RoomIdRole, "roomId"}, {RoomIdRole, "roomId"},
{AuthorName, "authorName"}, {AuthorName, "authorName"},
{AuthorAvatar, "authorAvatar"}, {AuthorAvatar, "authorAvatar"},

View File

@@ -42,6 +42,7 @@ Name[sv]=NeoChat
Name[ta]=நியோச்சாட் Name[ta]=நியோச்சாட்
Name[tr]=NeoChat Name[tr]=NeoChat
Name[uk]=NeoChat Name[uk]=NeoChat
Name[x-test]=xxNeoChatxx
Name[zh_CN]=NeoChat Name[zh_CN]=NeoChat
Name[zh_TW]=NeoChat Name[zh_TW]=NeoChat
DesktopEntry=org.kde.neochat DesktopEntry=org.kde.neochat
@@ -86,6 +87,7 @@ Comment[sv]=En klient för matrix, det decentraliserade kommunikationsprotokolle
Comment[ta]=மையமில்லா தகவல் பரிமாற்ற நெறிமுறையான மேட்ரிக்ஸுக்கான செயலி Comment[ta]=மையமில்லா தகவல் பரிமாற்ற நெறிமுறையான மேட்ரிக்ஸுக்கான செயலி
Comment[tr]=Merkezi olmayan iletişim protokolü Matrix için bir istemci Comment[tr]=Merkezi olmayan iletişim protokolü Matrix için bir istemci
Comment[uk]=Клієнт matrix, децентралізованого протоколу обміну даними Comment[uk]=Клієнт matrix, децентралізованого протоколу обміну даними
Comment[x-test]=xxA client for matrix, the decentralized communication protocolxx
Comment[zh_CN]=分布式通讯协议 Matrix 的客户端 Comment[zh_CN]=分布式通讯协议 Matrix 的客户端
Comment[zh_TW]=去中心化通訊協定 Matrix 的用戶端 Comment[zh_TW]=去中心化通訊協定 Matrix 的用戶端
@@ -132,6 +134,7 @@ Name[sv]=Nytt meddelande
Name[ta]=புதிய செய்தி Name[ta]=புதிய செய்தி
Name[tr]=Yeni İleti Name[tr]=Yeni İleti
Name[uk]=Нове повідомлення Name[uk]=Нове повідомлення
Name[x-test]=xxNew messagexx
Name[zh_CN]=新消息 Name[zh_CN]=新消息
Name[zh_TW]=新訊息 Name[zh_TW]=新訊息
Comment=There is a new message Comment=There is a new message
@@ -174,6 +177,7 @@ Comment[sv]=Det finns ett nytt meddelande
Comment[ta]=ஒரு புதிய செய்தி உள்ளது Comment[ta]=ஒரு புதிய செய்தி உள்ளது
Comment[tr]=Yeni bir ileti var Comment[tr]=Yeni bir ileti var
Comment[uk]=Надійшло нове повідомлення Comment[uk]=Надійшло нове повідомлення
Comment[x-test]=xxThere is a new messagexx
Comment[zh_CN]=有新消息 Comment[zh_CN]=有新消息
Comment[zh_TW]=有新的訊息 Comment[zh_TW]=有新的訊息
Action=Popup Action=Popup
@@ -211,7 +215,6 @@ Name[pa]=ਨਵਾਂ ਸੱਦਾ
Name[pl]=Nowe zaproszenie Name[pl]=Nowe zaproszenie
Name[pt]=Novo Convite Name[pt]=Novo Convite
Name[pt_BR]=Novo convite Name[pt_BR]=Novo convite
Name[ro]=Invitație nouă
Name[ru]=Новое приглашение Name[ru]=Новое приглашение
Name[sa]=नवीन आमन्त्रणम् Name[sa]=नवीन आमन्त्रणम्
Name[sl]=Novo povabilo Name[sl]=Novo povabilo
@@ -219,6 +222,7 @@ Name[sv]=Ny inbjudan
Name[ta]=புதிய அழைப்பிதழ் Name[ta]=புதிய அழைப்பிதழ்
Name[tr]=Yeni Davet Name[tr]=Yeni Davet
Name[uk]=Нове запрошення Name[uk]=Нове запрошення
Name[x-test]=xxNew Invitationxx
Name[zh_CN]=新邀请 Name[zh_CN]=新邀请
Name[zh_TW]=新邀請 Name[zh_TW]=新邀請
Comment=There is a new invitation to a room Comment=There is a new invitation to a room
@@ -253,14 +257,14 @@ Comment[pa]=ਰੂਮ ਲਈ ਨਵਾਂ ਸੱਦਾ ਹੈ
Comment[pl]=Dostępna jest nowe zaproszenie do pokoju Comment[pl]=Dostępna jest nowe zaproszenie do pokoju
Comment[pt]=Existe um novo convite para uma sala Comment[pt]=Existe um novo convite para uma sala
Comment[pt_BR]=Existe um novo convite para uma sala Comment[pt_BR]=Existe um novo convite para uma sala
Comment[ro]=E o nouă invitație la o cameră
Comment[ru]=Доступно новое приглашение в комнату Comment[ru]=Доступно новое приглашение в комнату
Comment[sa]=कक्षस्य नूतनं निमन्त्रणम् अस्ति Comment[sa]=कक्षस्य नूतनं निमन्त्रणम् अस्ति
Comment[sl]=Tam je novo povabilo v sobo Comment[sl]=Tam je novo povabilo v sobo
Comment[sv]=Det finns en ny inbjudan till ett rum Comment[sv]=Det finns en ny inbjudan till ett rum
Comment[ta]=ஓர் அரங்கிற்கான புதிய அழைப்பிதழ் உள்ளது Comment[ta]=ஓர் அரங்கிற்கான புதிய அழைப்பிதழ் உள்ளது
Comment[tr]=Bir odaya yeni bir davet var Comment[tr]=Bir odaya yeni bir davetiye var
Comment[uk]=У кімнаті нове запрошення Comment[uk]=У кімнаті нове запрошення
Comment[x-test]=xxThere is a new invitation to a roomxx
Comment[zh_CN]=有新的聊天室邀请 Comment[zh_CN]=有新的聊天室邀请
Comment[zh_TW]=有新的加入聊天室邀請 Comment[zh_TW]=有新的加入聊天室邀請
Action=Popup Action=Popup
@@ -292,7 +296,6 @@ Name[nl]=Gedeelde
Name[nn]=Del Name[nn]=Del
Name[pl]=Udostępnij Name[pl]=Udostępnij
Name[pt_BR]=Compartilhar Name[pt_BR]=Compartilhar
Name[ro]=Partajare
Name[ru]=Публикация Name[ru]=Публикация
Name[sa]=संविभागः Name[sa]=संविभागः
Name[sl]=Deli Name[sl]=Deli
@@ -300,6 +303,7 @@ Name[sv]=Dela
Name[ta]=பகிர் Name[ta]=பகிர்
Name[tr]=Paylaş Name[tr]=Paylaş
Name[uk]=Оприлюднення Name[uk]=Оприлюднення
Name[x-test]=xxSharexx
Name[zh_CN]=分享 Name[zh_CN]=分享
Name[zh_TW]=分享 Name[zh_TW]=分享
Comment=The result of sharing a piece of content Comment=The result of sharing a piece of content
@@ -327,7 +331,6 @@ Comment[nl]=Het resultaat van het delen van een stukje inhoud
Comment[nn]=Resultatet av deling av innhald Comment[nn]=Resultatet av deling av innhald
Comment[pl]=Wynik udostępniania kawałka treści Comment[pl]=Wynik udostępniania kawałka treści
Comment[pt_BR]=O resultado de compartilhar um conteúdo Comment[pt_BR]=O resultado de compartilhar um conteúdo
Comment[ro]=Rezultatul partajării unei bucăți de conținut
Comment[ru]=Результат публикации данных Comment[ru]=Результат публикации данных
Comment[sa]=सामग्रीखण्डस्य साझाकरणस्य परिणामः Comment[sa]=सामग्रीखण्डस्य साझाकरणस्य परिणामः
Comment[sl]=Rezultat deljenega kosa vsebine Comment[sl]=Rezultat deljenega kosa vsebine
@@ -335,6 +338,7 @@ Comment[sv]=Resultatet av att dela innehåll
Comment[ta]=எதையோ பகிர்ந்த‍தன் விளைவு Comment[ta]=எதையோ பகிர்ந்த‍தன் விளைவு
Comment[tr]=Bir parça içerik paylaşımının sonucu Comment[tr]=Bir parça içerik paylaşımının sonucu
Comment[uk]=Результат оприлюднення даних Comment[uk]=Результат оприлюднення даних
Comment[x-test]=xxThe result of sharing a piece of contentxx
Comment[zh_CN]=分享一个内容得到的结果 Comment[zh_CN]=分享一个内容得到的结果
Comment[zh_TW]=分享一份內容之後的結果 Comment[zh_TW]=分享一份內容之後的結果
Action=Popup Action=Popup

View File

@@ -66,10 +66,6 @@
</entry> </entry>
</group> </group>
<group name="Timeline"> <group name="Timeline">
<entry name="FontScale" type="double">
<label>Scaling factor for font sizes</label>
<default>1.0</default>
</entry>
<entry name="ShowAvatarInTimeline" type="bool"> <entry name="ShowAvatarInTimeline" type="bool">
<label>Show avatar in the timeline</label> <label>Show avatar in the timeline</label>
<default>true</default> <default>true</default>

View File

@@ -60,8 +60,9 @@ void NotificationsManager::startNotificationJob(QPointer<NeoChatConnection> conn
} }
if (!m_connActiveJob.contains(connection->user()->id())) { if (!m_connActiveJob.contains(connection->user()->id())) {
auto job = connection->callApi<GetNotificationsJob>();
m_connActiveJob.append(connection->user()->id()); m_connActiveJob.append(connection->user()->id());
connection->callApi<GetNotificationsJob>().onResult([this, connection](const auto &job) { connect(job, &BaseJob::success, this, [this, job, connection]() {
m_connActiveJob.removeAll(connection->user()->id()); m_connActiveJob.removeAll(connection->user()->id());
processNotificationJob(connection, job, !m_oldNotifications.contains(connection->user()->id())); processNotificationJob(connection, job, !m_oldNotifications.contains(connection->user()->id()));
}); });
@@ -216,12 +217,12 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
} }
}); });
notification->setTitle(room->displayName());
QString entry; QString entry;
if (room->isDirectChat()) { if (sender == room->displayName()) {
notification->setTitle(sender);
entry = text.toHtmlEscaped(); entry = text.toHtmlEscaped();
} else { } else {
notification->setTitle(room->displayName());
entry = i18n("%1: %2", sender, text.toHtmlEscaped()); entry = i18n("%1: %2", sender, text.toHtmlEscaped());
} }
@@ -253,9 +254,7 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
notification->setReplyAction(std::move(replyAction)); notification->setReplyAction(std::move(replyAction));
} }
if (Controller::instance().accounts()->rowCount() > 1) { notification->setHint(u"x-kde-origin-name"_s, room->localMember().id());
notification->setHint(u"x-kde-origin-name"_s, room->localMember().id());
}
notification->sendEvent(); notification->sendEvent();
} }
@@ -349,9 +348,7 @@ void NotificationsManager::doPostInviteNotification(QPointer<NeoChatRoom> room)
m_invitations.remove(room->id()); m_invitations.remove(room->id());
}); });
if (Controller::instance().accounts()->rowCount() > 1) { notification->setHint(u"x-kde-origin-name"_s, room->localMember().id());
notification->setHint(u"x-kde-origin-name"_s, room->localMember().id());
}
notification->sendEvent(); notification->sendEvent();
} }
@@ -392,7 +389,7 @@ void NotificationsManager::postPushNotification(const QByteArray &message)
#ifdef HAVE_KIO #ifdef HAVE_KIO
auto openAction = notification->addAction(i18n("Open NeoChat")); auto openAction = notification->addAction(i18n("Open NeoChat"));
connect(openAction, &KNotificationAction::activated, notification, [=]() { connect(openAction, &KNotificationAction::activated, this, [=]() {
QString properId = roomId; QString properId = roomId;
properId = properId.replace(u"#"_s, QString()); properId = properId.replace(u"#"_s, QString());
properId = properId.replace(u"!"_s, QString()); properId = properId.replace(u"!"_s, QString());
@@ -406,6 +403,8 @@ void NotificationsManager::postPushNotification(const QByteArray &message)
connect(notification, &KNotification::closed, qGuiApp, &QGuiApplication::quit); connect(notification, &KNotification::closed, qGuiApp, &QGuiApplication::quit);
notification->sendEvent(); notification->sendEvent();
m_notifications.insert(roomId, {json["ts"_L1].toVariant().toLongLong(), notification});
} else { } else {
qWarning() << "Skipping unsupported push notification" << type; qWarning() << "Skipping unsupported push notification" << type;
} }

View File

@@ -53,7 +53,7 @@ public:
/** /**
* @brief Display a native notification for the given push notification. * @brief Display a native notification for the given push notification.
*/ */
static void postPushNotification(const QByteArray &message); void postPushNotification(const QByteArray &message);
/** /**
* @brief Handle the notifications for the given connection. * @brief Handle the notifications for the given connection.

View File

@@ -43,6 +43,7 @@ Name[sv]=NeoChat
Name[ta]=நியோச்சாட் Name[ta]=நியோச்சாட்
Name[tr]=NeoChat Name[tr]=NeoChat
Name[uk]=NeoChat Name[uk]=NeoChat
Name[x-test]=xxNeoChatxx
Name[zh_CN]=NeoChat Name[zh_CN]=NeoChat
Name[zh_TW]=NeoChat Name[zh_TW]=NeoChat
Comment=Find rooms in NeoChat Comment=Find rooms in NeoChat
@@ -75,7 +76,6 @@ Comment[nn]=Finn rom i NeoChat
Comment[pl]=Znajdź pokoje w NeoChat Comment[pl]=Znajdź pokoje w NeoChat
Comment[pt]=Procurar salas no NeoChat Comment[pt]=Procurar salas no NeoChat
Comment[pt_BR]=Encontrar salas no NeoChat Comment[pt_BR]=Encontrar salas no NeoChat
Comment[ro]=Găsește camere în NeoChat
Comment[ru]=Поиск комнат NeoChat Comment[ru]=Поиск комнат NeoChat
Comment[sa]=NeoChat इत्यत्र कक्ष्याः अन्वेषणं कुर्वन्तु Comment[sa]=NeoChat इत्यत्र कक्ष्याः अन्वेषणं कुर्वन्तु
Comment[sl]=Najdi sobe v NeoChatu Comment[sl]=Najdi sobe v NeoChatu
@@ -83,6 +83,7 @@ Comment[sv]=Sök efter rum i NeoChat
Comment[ta]=நியோச்சாட்டில் அரங்குகளை கண்டுபிடிக்கும் Comment[ta]=நியோச்சாட்டில் அரங்குகளை கண்டுபிடிக்கும்
Comment[tr]=NeoChatte odalar bulun Comment[tr]=NeoChatte odalar bulun
Comment[uk]=Пошук кімнат у NeoChat Comment[uk]=Пошук кімнат у NeoChat
Comment[x-test]=xxFind rooms in NeoChatxx
Comment[zh_CN]=在 NeoChat 查找聊天室 Comment[zh_CN]=在 NeoChat 查找聊天室
Comment[zh_TW]=在 NeoChat 尋找聊天室 Comment[zh_TW]=在 NeoChat 尋找聊天室
X-KDE-ServiceTypes=Plasma/Runner X-KDE-ServiceTypes=Plasma/Runner

View File

@@ -88,7 +88,7 @@ KirigamiComponents.ConvergentContextMenu {
Kirigami.Action { Kirigami.Action {
text: i18nc("@action:inmenu", "Verify This Device") text: i18nc("@action:inmenu", "Verify This Device")
icon.name: "security-low" icon.name: "security-low"
visible: !root.connection.isVerifiedSession visible: !root.connection.isVerifiedSession()
onTriggered: { onTriggered: {
root.connection.startSelfVerification(); root.connection.startSelfVerification();
const dialog = Qt.createComponent("org.kde.kirigami", "PromptDialog").createObject(QQC2.Overlay.overlay, { const dialog = Qt.createComponent("org.kde.kirigami", "PromptDialog").createObject(QQC2.Overlay.overlay, {

View File

@@ -61,7 +61,7 @@ Kirigami.Dialog {
} }
onClicked: { onClicked: {
((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat.login', 'WelcomePage'), {}, { pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat.login', 'WelcomePage'), {}, {
title: i18nc("@title:window", "Login") title: i18nc("@title:window", "Login")
}); });
root.close(); root.close();
@@ -95,8 +95,8 @@ Kirigami.Dialog {
accountView.decrementCurrentIndex(); accountView.decrementCurrentIndex();
} }
} }
Keys.onEnterPressed: ((accountView.currentItem ?? accountView.footerItem) as Delegates.RoundedItemDelegate).clicked() Keys.onEnterPressed: (accountView.currentItem as Delegates.RoundedItemDelegate).clicked()
Keys.onReturnPressed: ((accountView.currentItem ?? accountView.footerItem) as Delegates.RoundedItemDelegate).clicked() Keys.onReturnPressed: (accountView.currentItem as Delegates.RoundedItemDelegate).clicked()
onVisibleChanged: { onVisibleChanged: {
for (let i = 0; i < accountView.count; i++) { for (let i = 0; i < accountView.count; i++) {

View File

@@ -21,7 +21,6 @@ Delegates.RoundedItemDelegate {
signal contextMenuRequested signal contextMenuRequested
signal selected signal selected
activeFocusOnTab: true
padding: Kirigami.Units.largeSpacing padding: Kirigami.Units.largeSpacing
QQC2.ToolTip.visible: hovered QQC2.ToolTip.visible: hovered

View File

@@ -48,7 +48,7 @@ Components.AbstractMaximizeComponent {
implicitWidth: Kirigami.Units.iconSizes.medium implicitWidth: Kirigami.Units.iconSizes.medium
implicitHeight: Kirigami.Units.iconSizes.medium implicitHeight: Kirigami.Units.iconSizes.medium
name: root.author.displayName name: root.author.name ?? root.author.displayName
source: root.author.avatarUrl source: root.author.avatarUrl
color: root.author.color color: root.author.color
} }
@@ -57,7 +57,7 @@ Components.AbstractMaximizeComponent {
QQC2.Label { QQC2.Label {
id: userLabel id: userLabel
text: root.author.displayName text: root.author.name ?? root.author.displayName
color: root.author.color color: root.author.color
font.weight: Font.Bold font.weight: Font.Bold
elide: Text.ElideRight elide: Text.ElideRight
@@ -91,7 +91,6 @@ Components.AbstractMaximizeComponent {
color: Kirigami.Theme.textColor color: Kirigami.Theme.textColor
font.family: "monospace" font.family: "monospace"
font.pointSize: Kirigami.Theme.defaultFont.pointSize * NeoChatConfig.fontScale
Kirigami.SpellCheck.enabled: false Kirigami.SpellCheck.enabled: false
@@ -150,6 +149,4 @@ Components.AbstractMaximizeComponent {
color: Kirigami.Theme.backgroundColor color: Kirigami.Theme.backgroundColor
} }
} }
onOpened: forceActiveFocus()
} }

View File

@@ -6,15 +6,16 @@ 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.neochat
ColumnLayout { ColumnLayout {
id: root id: root
required property string emoji property alias emoji: emojiLabel.text
required property string description property alias description: descriptionLabel.text
QQC2.Label { QQC2.Label {
text: root.emoji id: emojiLabel
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredWidth: Kirigami.Units.iconSizes.huge Layout.preferredWidth: Kirigami.Units.iconSizes.huge
Layout.preferredHeight: Kirigami.Units.iconSizes.huge Layout.preferredHeight: Kirigami.Units.iconSizes.huge
@@ -24,7 +25,7 @@ ColumnLayout {
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 4 font.pointSize: Kirigami.Theme.defaultFont.pointSize * 4
} }
QQC2.Label { QQC2.Label {
text: root.description id: descriptionLabel
Layout.fillWidth: true Layout.fillWidth: true
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter

View File

@@ -17,6 +17,9 @@ RowLayout {
Repeater { Repeater {
id: repeater id: repeater
delegate: EmojiItem {} delegate: EmojiItem {
emoji: modelData.emoji
description: modelData.description
}
} }
} }

View File

@@ -17,7 +17,7 @@ ApplicationWindow {
property real longitude: NaN property real longitude: NaN
property string asset property string asset
property var author property var author
property LiveLocationsModel liveLocationModel: null property QtObject liveLocationModel: null
flags: Qt.FramelessWindowHint | Qt.WA_TranslucentBackground flags: Qt.FramelessWindowHint | Qt.WA_TranslucentBackground
visibility: Qt.WindowFullScreen visibility: Qt.WindowFullScreen
@@ -59,7 +59,7 @@ ApplicationWindow {
Connections { Connections {
target: mapView.map target: mapView.map
function onCopyrightLinkActivated(link: string) { function onCopyrightLinkActivated() {
Qt.openUrlExternally(link); Qt.openUrlExternally(link);
} }
} }

View File

@@ -22,12 +22,12 @@ Labs.MenuBar {
Labs.MenuItem { Labs.MenuItem {
icon.name: "list-add-user" icon.name: "list-add-user"
text: i18nc("@action:inmenu", "Find User") text: i18nc("@action:inmenu", "Find your Friends")
enabled: root.connection enabled: root.connection
onTriggered: root.appWindow.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UserSearchPage'), { onTriggered: root.appWindow.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UserSearchPage'), {
connection: root.connection connection: root.connection
}, { }, {
title: i18nc("@title", "Find User") title: i18nc("@title", "Find your friends")
}) })
} }
Labs.MenuItem { Labs.MenuItem {

View File

@@ -3,10 +3,8 @@
import QtQuick import QtQuick
import org.kde.kirigami as Kirigami
import org.kde.neochat import org.kde.neochat
Item { Item {
required property NeoChatConnection connection required property NeoChatConnection connection
required property Kirigami.ApplicationWindow appWindow
} }

View File

@@ -3,54 +3,29 @@
// SPDX-License-Identifier: LGPL-2.0-or-later // SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as QQC2 import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
RowLayout { QQC2.Control {
id: root id: root
property string text property string text
onTextChanged: { visible: !root.text.startsWith("https://matrix.to/") && root.text.length > 0
// This is done so the text doesn't disappear for a split second while in the opacity transition Kirigami.Theme.colorSet: Kirigami.Theme.View
if (root.text.length > 0) {
urlLabel.text = root.text z: 20
}
Accessible.ignored: true
contentItem: QQC2.Label {
text: root.text.startsWith("https://matrix.to/") ? "" : root.text
elide: Text.ElideRight
Accessible.description: i18nc("@info screenreader", "The currently selected link")
} }
z: 99 background: Rectangle {
spacing: 0 color: Kirigami.Theme.backgroundColor
opacity: (!root.text.startsWith("https://matrix.to/") && root.text.length > 0) ? 1 : 0
visible: opacity > 0
Behavior on opacity {
OpacityAnimator {
duration: Kirigami.Units.shortDuration
easing.type: Easing.InOutQuad
}
}
QQC2.Control {
Kirigami.Theme.colorSet: Kirigami.Theme.View
Accessible.ignored: true
contentItem: QQC2.Label {
id: urlLabel
elide: Text.ElideRight
}
background: Kirigami.ShadowedRectangle {
corners.topRightRadius: Kirigami.Units.cornerRadius
color: Kirigami.Theme.backgroundColor
border {
color: Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, Kirigami.Theme.frameContrast)
width: 1
}
}
} }
} }

View File

@@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: 2025 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.kirigamiaddons.components as Components
import org.kde.neochat.libneochat
FormCard.FormCardPage {
id: root
title: i18nc("@title:dialog", "Incoming Call")
FormCard.FormCard {
topPadding: Kirigami.Units.largeSpacing
FormCard.AbstractFormDelegate {
contentItem: Components.Avatar {
name: CallManager.room.displayName
source: CallManager.room.avatarMediaUrl
}
}
FormCard.FormTextDelegate {
text: i18nc("@info", "%1 is calling you.", CallManager.callingMember.htmlSafeDisplayName)
}
FormCard.FormButtonDelegate {
text: i18nc("@action:button", "Accept Call")
onClicked: console.warn("unimplemented")
}
FormCard.FormButtonDelegate {
text: i18nc("@action:button", "Decline Call")
onClicked: CallManager.declineCall()
}
}
}

View File

@@ -2,8 +2,6 @@
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com> // SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Controls as QQC2 import QtQuick.Controls as QQC2
import QtQuick.Layouts import QtQuick.Layouts

View File

@@ -66,7 +66,7 @@ Kirigami.Dialog {
text: i18nc("@action:button", "Join room") text: i18nc("@action:button", "Join room")
icon.name: "irc-join-channel" icon.name: "irc-join-channel"
onClicked: { onClicked: {
RoomManager.resolveResource(root.room, "join_confirmed"); RoomManager.resolveResource(root.room, "join");
root.close(); root.close();
} }
} }

View File

@@ -4,7 +4,6 @@
import QtQuick import QtQuick
import QtQuick.Controls as QQC2 import QtQuick.Controls as QQC2
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Window
import QtQml import QtQml
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
@@ -51,22 +50,6 @@ Kirigami.Page {
sourceComponent: message sourceComponent: message
} }
}, },
State {
name: "waitingForKey"
when: root.session.state === KeyVerificationSession.WAITINGFORKEY
PropertyChanges {
target: stateLoader
sourceComponent: message
}
},
State {
name: "waitingForAccept"
when: root.session.state === KeyVerificationSession.WAITINGFORACCEPT
PropertyChanges {
target: stateLoader
sourceComponent: message
}
},
State { State {
name: "waitingForMac" name: "waitingForMac"
when: root.session.state === KeyVerificationSession.WAITINGFORMAC when: root.session.state === KeyVerificationSession.WAITINGFORMAC
@@ -144,9 +127,7 @@ Kirigami.Page {
case KeyVerificationSession.WAITINGFORREADY: case KeyVerificationSession.WAITINGFORREADY:
case KeyVerificationSession.INCOMING: case KeyVerificationSession.INCOMING:
case KeyVerificationSession.WAITINGFORMAC: case KeyVerificationSession.WAITINGFORMAC:
case KeyVerificationSession.WAITINGFORKEY: return "security-medium-symbolic";
case KeyVerificationSession.WAITINGFORACCEPT:
return "security-medium-symbolic";
case KeyVerificationSession.DONE: case KeyVerificationSession.DONE:
return "security-high"; return "security-high";
default: default:
@@ -160,19 +141,13 @@ Kirigami.Page {
case KeyVerificationSession.INCOMING: case KeyVerificationSession.INCOMING:
return i18n("Incoming key verification request from device **%1**", root.session.remoteDeviceId); return i18n("Incoming key verification request from device **%1**", root.session.remoteDeviceId);
case KeyVerificationSession.WAITINGFORMAC: case KeyVerificationSession.WAITINGFORMAC:
return i18n("Waiting for other party to send us keys.");
case KeyVerificationSession.WAITINGFORKEY:
return i18n("Waiting for other party to confirm our keys.");
case KeyVerificationSession.WAITINGFORACCEPT:
return i18n("Waiting for other party to verify."); return i18n("Waiting for other party to verify.");
case KeyVerificationSession.DONE: case KeyVerificationSession.DONE:
return i18n("Successfully verified device **%1**", root.session.remoteDeviceId); return i18n("Successfully verified device **%1**", root.session.remoteDeviceId)
default: default:
return ""; return "";
} }
} }
isDone: root.session.state === KeyVerificationSession.DONE
onDone: root.closeDialog()
} }
} }

View File

@@ -45,8 +45,6 @@ Components.AbstractMaximizeComponent {
} }
] ]
onOpened: forceActiveFocus()
PositionSource { PositionSource {
id: positionSource id: positionSource

View File

@@ -83,6 +83,16 @@ Kirigami.ApplicationWindow {
} }
} }
Connections {
target: CallManager
property IncomingCallDialog dialog
function onIsRingingChanged(): void {
if (CallManager.isRinging) {
root.pageStack.pushDialogLayer(Qt.createComponent("org.kde.neochat", "IncomingCallDialog"));
}
}
}
Loader { Loader {
active: Kirigami.Settings.hasPlatformMenuBar && !Kirigami.Settings.isMobile active: Kirigami.Settings.hasPlatformMenuBar && !Kirigami.Settings.isMobile
sourceComponent: GlobalMenu { sourceComponent: GlobalMenu {
@@ -100,8 +110,7 @@ Kirigami.ApplicationWindow {
function onCurrentRoomChanged() { function onCurrentRoomChanged() {
if (RoomManager.currentRoom && root.pageStack.depth <= 1 && root.initialized && Kirigami.Settings.isMobile) { if (RoomManager.currentRoom && root.pageStack.depth <= 1 && root.initialized && Kirigami.Settings.isMobile) {
let roomPage = pageStack.push(Qt.createComponent('org.kde.neochat', 'RoomPage')); let roomPage = root.pageStack.layers.push(Qt.createComponent('org.kde.neochat', 'RoomPage'));
roomPage.forceActiveFocus();
roomPage.backRequested.connect(event => { roomPage.backRequested.connect(event => {
RoomManager.clearCurrentRoom(); RoomManager.clearCurrentRoom();
}); });
@@ -234,7 +243,7 @@ Kirigami.ApplicationWindow {
RoomListPage { RoomListPage {
id: roomList id: roomList
onSearch: root.quickSwitcher.open() onSearch: quickSwitcher.open()
connection: root.connection connection: root.connection
@@ -268,17 +277,6 @@ Kirigami.ApplicationWindow {
} }
} }
Connections {
target: root.connection
function onLoggedOut(): void {
root.pageStack.clear();
let page = root.pageStack.push(Qt.createComponent('org.kde.neochat.login', 'WelcomePage'), {
showExisting: true,
}) as WelcomePage;
page.connectionChosen.connect(() => root.load())
}
}
Connections { Connections {
target: AccountRegistry target: AccountRegistry
function onRowsRemoved() { function onRowsRemoved() {
@@ -359,11 +357,7 @@ Kirigami.ApplicationWindow {
user: user, user: user,
connection: root.connection, connection: root.connection,
}) as UserDetailDialog; }) as UserDetailDialog;
// FIXME: The reason why we don't want the focusedWindowItem for the room null case (aka QR codes) is because it will parent it to the soon-to-be-destroyed window item. dialog.parent = QmlUtils.focusedWindowItem(); // Kirigami Dialogs overwrite the parent, so we need to set it again
// But this won't be a problem if we turn it into a Kirigami.Dialog or some other in-scene item, which it really should be.
if (room != null) {
dialog.parent = QmlUtils.focusedWindowItem(); // Kirigami Dialogs overwrite the parent, so we need to set it again
}
dialog.open(); dialog.open();
} }

View File

@@ -21,7 +21,7 @@ Kirigami.Dialog {
/** /**
* @brief Thrown when a user is selected. * @brief Thrown when a user is selected.
*/ */
signal userSelected(string userId) signal userSelected
title: i18nc("@title", "User ID") title: i18nc("@title", "User ID")
@@ -38,7 +38,7 @@ Kirigami.Dialog {
text: i18n("OK") text: i18n("OK")
icon.name: "dialog-ok" icon.name: "dialog-ok"
onTriggered: { onTriggered: {
root.userSelected(userIdText.text) root.connection.requestDirectChat(userIdText.text);
root.accept(); root.accept();
} }
} }

View File

@@ -1,22 +0,0 @@
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick
import org.kde.kirigami as Kirigami
Kirigami.PromptDialog {
id: root
required property bool hasExistingMeeting
title: hasExistingMeeting ? i18nc("@title", "Join Meeting") : i18nc("@title", "Start Meeting")
subtitle: hasExistingMeeting ? i18nc("@info:label", "You are about to join a Jitsi meeting in your web browser.") : i18nc("@info:label", "You are about to start a new Jitsi meeting in your web browser.")
standardButtons: Kirigami.Dialog.Cancel
customFooterActions: Kirigami.Action {
icon.name: "camera-video-symbolic"
text: hasExistingMeeting ? i18nc("@action:button Join the Jitsi meeting", "Join") : i18nc("@action:button Start a new Jitsi meeting", "Start")
onTriggered: root.accept()
}
}

View File

@@ -34,8 +34,8 @@ Kirigami.Page {
enabled: root.model enabled: root.model
target: root.room target: root.room
function onChanged(): void { function onChanged(): void {
root.contentJson = root.model.stateEventContentJson(root.type, root.stateKey); root.contentJson = model.stateEventContentJson(root.type, root.stateKey);
root.sourceText = root.model.stateEventJson(root.type, root.stateKey); root.sourceText = model.stateEventJson(root.type, root.stateKey);
} }
} }
@@ -46,8 +46,8 @@ Kirigami.Page {
text: i18nc("@action As in 'edit the state of this room'", "Edit state") text: i18nc("@action As in 'edit the state of this room'", "Edit state")
icon.name: "document-edit" icon.name: "document-edit"
visible: root.allowEdit visible: root.allowEdit
enabled: root.room.canSendState(root.type) && (!root.stateKey.startsWith("@") || root.stateKey === root.room.connection.localUserId) && root.type !== "m.room.create" enabled: room.canSendState(root.type) && (!root.stateKey.startsWith("@") || root.stateKey === root.room.connection.localUserId) && root.type !== "m.room.create"
onTriggered: (root.Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent("org.kde.neochat", "EditStateDialog"), { onTriggered: pageStack.pushDialogLayer(Qt.createComponent("org.kde.neochat", "EditStateDialog"), {
room: root.room, room: root.room,
type: root.type, type: root.type,
stateKey: root.stateKey, stateKey: root.stateKey,

View File

@@ -1,8 +1,6 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com> // SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
pragma ComponentBehavior: Bound
import QtCore as Core import QtCore as Core
import QtQuick import QtQuick
import QtQuick.Controls as QQC2 import QtQuick.Controls as QQC2
@@ -23,13 +21,13 @@ Components.AlbumMaximizeComponent {
*/ */
required property NeoChatRoom currentRoom required property NeoChatRoom currentRoom
readonly property string currentEventId: model.data(model.index((content as ListView).currentIndex, 0), TimelineMessageModel.EventIdRole) readonly property string currentEventId: model.data(model.index(content.currentIndex, 0), TimelineMessageModel.EventIdRole)
readonly property var currentAuthor: model.data(model.index((content as ListView).currentIndex, 0), TimelineMessageModel.AuthorRole) readonly property var currentAuthor: model.data(model.index(content.currentIndex, 0), TimelineMessageModel.AuthorRole)
readonly property var currentTime: model.data(model.index((content as ListView).currentIndex, 0), TimelineMessageModel.TimeRole) readonly property var currentTime: model.data(model.index(content.currentIndex, 0), TimelineMessageModel.TimeRole)
readonly property var currentProgressInfo: model.data(model.index((content as ListView).currentIndex, 0), TimelineMessageModel.ProgressInfoRole) readonly property var currentProgressInfo: model.data(model.index(content.currentIndex, 0), TimelineMessageModel.ProgressInfoRole)
actions: [ actions: [
ShareAction { ShareAction {
@@ -61,28 +59,28 @@ Components.AlbumMaximizeComponent {
downloadAction: Components.DownloadAction { downloadAction: Components.DownloadAction {
onTriggered: { onTriggered: {
root.currentRoom.downloadFile(root.currentEventId, Core.StandardPaths.writableLocation(Core.StandardPaths.CacheLocation) + "/" + root.currentEventId.replace(":", "_").replace("/", "_").replace("+", "_") + root.currentRoom.fileNameToDownload(root.currentEventId)); currentRoom.downloadFile(root.currentEventId, Core.StandardPaths.writableLocation(Core.StandardPaths.CacheLocation) + "/" + root.currentEventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(root.currentEventId));
} }
} }
playAction: Kirigami.Action { playAction: Kirigami.Action {
onTriggered: { onTriggered: {
MediaManager.startPlayback(); MediaManager.startPlayback();
(root.currentItem as Components.VideoMaximizeDelegate).play(); currentItem.play();
} }
} }
Connections { Connections {
target: MediaManager target: MediaManager
function onPlaybackStarted() { function onPlaybackStarted() {
if ((root.currentItem as Components.VideoMaximizeDelegate).playbackState === MediaPlayer.PlayingState) { if (currentItem.playbackState === MediaPlayer.PlayingState) {
(root.currentItem as Components.VideoMaximizeDelegate).pause(); currentItem.pause();
} }
} }
} }
Connections { Connections {
target: root.currentRoom target: currentRoom
function onFileTransferProgress(id, progress, total) { function onFileTransferProgress(id, progress, total) {
if (id == root.currentEventId) { if (id == root.currentEventId) {
@@ -122,10 +120,10 @@ Components.AlbumMaximizeComponent {
onOpened: forceActiveFocus() onOpened: forceActiveFocus()
onItemRightClicked: RoomManager.viewEventMenu(root.currentEventId, root.currentRoom) onItemRightClicked: RoomManager.viewEventMenu(root.currentEventId, root.currentRoom, root.currentAuthor)
onSaveItem: { onSaveItem: {
var dialog = saveAsDialog.createObject(QQC2.Overlay.overlay) as Dialogs.FileDialog; var dialog = saveAsDialog.createObject(QQC2.Overlay.overlay);
dialog.selectedFile = currentRoom.fileNameToDownload(root.currentEventId); dialog.selectedFile = currentRoom.fileNameToDownload(root.currentEventId);
dialog.open(); dialog.open();
} }
@@ -148,7 +146,7 @@ Components.AlbumMaximizeComponent {
if (!selectedFile) { if (!selectedFile) {
return; return;
} }
root.currentRoom.downloadFile(root.currentEventId, selectedFile); currentRoom.downloadFile(root.currentEventId, selectedFile);
} }
} }
} }

View File

@@ -116,7 +116,7 @@ Kirigami.Dialog {
optionModel.set(optionDelegate.index, {optionText: text}) optionModel.set(optionDelegate.index, {optionText: text})
optionModel.allValuesSetChanged() optionModel.allValuesSetChanged()
} }
placeholderText: i18nc("@placeholder", "Enter option") placeholderText: i18n("Enter option")
} }
QQC2.ToolButton { QQC2.ToolButton {
display: QQC2.AbstractButton.IconOnly display: QQC2.AbstractButton.IconOnly

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